Skip to content

Commit 02e6fa4

Browse files
committed
feat: eks pod identity support for controllers
This adds support for using EKS pod identity for the CAPA controller when the management cluster is an EKS cluster Signed-off-by: Richard Case <[email protected]>
1 parent 85759ce commit 02e6fa4

File tree

8 files changed

+219
-8
lines changed

8 files changed

+219
-8
lines changed

cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func (t Template) controllersPolicyRoleAttachments() []string {
5151
return attachments
5252
}
5353

54-
func (t Template) controllersTrustPolicy() *iamv1.PolicyDocument {
55-
policyDocument := ec2AssumeRolePolicy()
54+
func (t Template) controllersTrustPolicy(eksEnabled bool) *iamv1.PolicyDocument {
55+
policyDocument := ec2AssumeRolePolicy(eksEnabled)
5656
policyDocument.Statement = append(policyDocument.Statement, t.Spec.ClusterAPIControllers.TrustStatements...)
5757
return policyDocument
5858
}

cmd/clusterawsadm/cloudformation/bootstrap/control_plane.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (t Template) controlPlanePolicies() []cfn_iam.Role_Policy {
4040
}
4141

4242
func (t Template) controlPlaneTrustPolicy() *iamv1.PolicyDocument {
43-
policyDocument := ec2AssumeRolePolicy()
43+
policyDocument := ec2AssumeRolePolicy(false)
4444
policyDocument.Statement = append(policyDocument.Statement, t.Spec.ControlPlane.TrustStatements...)
4545
return policyDocument
4646
}

cmd/clusterawsadm/cloudformation/bootstrap/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (t Template) nodePolicies() []cfn_iam.Role_Policy {
3939
}
4040

4141
func (t Template) nodeTrustPolicy() *iamv1.PolicyDocument {
42-
policyDocument := ec2AssumeRolePolicy()
42+
policyDocument := ec2AssumeRolePolicy(false)
4343
policyDocument.Statement = append(policyDocument.Statement, t.Spec.Nodes.TrustStatements...)
4444
return policyDocument
4545
}

cmd/clusterawsadm/cloudformation/bootstrap/template.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (t Template) RenderCloudFormation() *cloudformation.Template {
146146

147147
template.Resources[AWSIAMRoleControllers] = &cfn_iam.Role{
148148
RoleName: t.NewManagedName("controllers"),
149-
AssumeRolePolicyDocument: t.controllersTrustPolicy(),
149+
AssumeRolePolicyDocument: t.controllersTrustPolicy(!t.Spec.EKS.Disable),
150150
Policies: t.controllersRolePolicy(),
151151
Tags: converters.MapToCloudFormationTags(t.Spec.ClusterAPIControllers.Tags),
152152
}
@@ -218,8 +218,12 @@ func (t Template) RenderCloudFormation() *cloudformation.Template {
218218
return template
219219
}
220220

221-
func ec2AssumeRolePolicy() *iamv1.PolicyDocument {
222-
return AssumeRolePolicy(iamv1.PrincipalService, []string{"ec2.amazonaws.com"})
221+
func ec2AssumeRolePolicy(withEKS bool) *iamv1.PolicyDocument {
222+
principalIDs := []string{"ec2.amazonaws.com"}
223+
if withEKS {
224+
principalIDs = append(principalIDs, "pods.eks.amazonaws.com")
225+
}
226+
return AssumeRolePolicy(iamv1.PrincipalService, principalIDs)
223227
}
224228

225229
// AWSArnAssumeRolePolicy will assume Policies using PolicyArns.

cmd/clusterawsadm/cmd/controller/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func RootCmd() *cobra.Command {
4444
newCmd.AddCommand(credentials.UpdateCredentialsCmd())
4545
newCmd.AddCommand(credentials.PrintCredentialsCmd())
4646
newCmd.AddCommand(rollout.RolloutControllersCmd())
47+
newCmd.AddCommand(credentials.UseEKSPodIdentityCmd())
4748

4849
return newCmd
4950
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package credentials
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/aws/aws-sdk-go/aws"
23+
"github.com/aws/aws-sdk-go/aws/session"
24+
"github.com/aws/aws-sdk-go/service/eks"
25+
"github.com/aws/aws-sdk-go/service/iam"
26+
"github.com/spf13/cobra"
27+
28+
"sigs.k8s.io/cluster-api/cmd/clusterctl/cmd"
29+
)
30+
31+
// UseEKSPodIdentityCmd is a CLI command that will enable using EKS pod identity for CAPA.
32+
func UseEKSPodIdentityCmd() *cobra.Command {
33+
clusterName := ""
34+
region := ""
35+
namespace := ""
36+
serviceAccount := ""
37+
roleName := ""
38+
39+
newCmd := &cobra.Command{
40+
Use: "use-pod-identity",
41+
Short: "Enable EKS pod identiy with CAPA",
42+
Long: cmd.LongDesc(`
43+
Updates CAPA running in an EKS cluster to use EKS pod identity
44+
`),
45+
Example: cmd.Examples(`
46+
clusterawsadm controller use-pod-identity --cluster-name cluster1
47+
`),
48+
Args: cobra.NoArgs,
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
return usePodIdentity(region, clusterName, namespace, serviceAccount, roleName)
51+
},
52+
}
53+
54+
newCmd.Flags().StringVarP(&region, "region", "r", "", "The AWS region containing the EKS cluster")
55+
newCmd.Flags().StringVarP(&clusterName, "cluster-name", "n", "", "The name of the EKS management cluster")
56+
newCmd.Flags().StringVar(&namespace, "namespace", "capa-system", "The namespace of CAPA controller")
57+
newCmd.Flags().StringVar(&serviceAccount, "service-account", "capa-controller-manager", "The service account for the CAPA controller")
58+
newCmd.Flags().StringVar(&roleName, "role-name", "controllers.cluster-api-provider-aws.sigs.k8s.io", "The name of the CAPA controller role. If you have used a prefix or suffix this will need to be changed.")
59+
60+
newCmd.MarkFlagRequired("cluster-name")
61+
62+
return newCmd
63+
}
64+
65+
func usePodIdentity(region, clusterName, namespace, serviceAccount, roleName string) error {
66+
cfg := aws.Config{}
67+
if region != "" {
68+
cfg.Region = aws.String(region)
69+
}
70+
71+
sess, err := session.NewSessionWithOptions(session.Options{
72+
SharedConfigState: session.SharedConfigEnable,
73+
Config: cfg,
74+
})
75+
if err != nil {
76+
return fmt.Errorf("failed creating aws session: %w", err)
77+
}
78+
79+
roleArn, err := getRoleArn(sess, roleName)
80+
if err != nil {
81+
return err
82+
}
83+
84+
eksClient := eks.New(sess)
85+
86+
listInput := &eks.ListPodIdentityAssociationsInput{
87+
ClusterName: aws.String(clusterName),
88+
Namespace: aws.String(namespace),
89+
}
90+
91+
listOutput, err := eksClient.ListPodIdentityAssociations(listInput)
92+
if err != nil {
93+
return fmt.Errorf("listing existing pod identity associations for cluster %s in namespace %s: %w", clusterName, namespace, err)
94+
}
95+
96+
for _, association := range listOutput.Associations {
97+
if *association.ServiceAccount == serviceAccount {
98+
needsUpdate, err := podIdentityNeedsUpdate(eksClient, association, roleName)
99+
if err != nil {
100+
return err
101+
}
102+
if !needsUpdate {
103+
fmt.Printf("EKS pod association for service account %s already exists, no action taken\n", serviceAccount)
104+
}
105+
106+
return updatePodIdentity(eksClient, association, roleName)
107+
}
108+
}
109+
110+
fmt.Printf("Creating pod association for service account %s.....\n", serviceAccount)
111+
112+
createInpuut := &eks.CreatePodIdentityAssociationInput{
113+
ClusterName: &clusterName,
114+
Namespace: &namespace,
115+
RoleArn: &roleArn,
116+
ServiceAccount: &serviceAccount,
117+
}
118+
119+
output, err := eksClient.CreatePodIdentityAssociation(createInpuut)
120+
if err != nil {
121+
return fmt.Errorf("failed to create pod identity association: %w", err)
122+
}
123+
124+
fmt.Printf("Created pod identity association (%s)\n", *output.Association.AssociationId)
125+
126+
return nil
127+
}
128+
129+
func podIdentityNeedsUpdate(client *eks.EKS, association *eks.PodIdentityAssociationSummary, roleArn string) (bool, error) {
130+
input := &eks.DescribePodIdentityAssociationInput{
131+
AssociationId: association.AssociationId,
132+
ClusterName: association.ClusterName,
133+
}
134+
135+
output, err := client.DescribePodIdentityAssociation(input)
136+
if err != nil {
137+
return false, fmt.Errorf("failed describing pod identity association: %w", err)
138+
}
139+
140+
return *output.Association.RoleArn != roleArn, nil
141+
}
142+
143+
func updatePodIdentity(client *eks.EKS, association *eks.PodIdentityAssociationSummary, roleArn string) error {
144+
input := &eks.UpdatePodIdentityAssociationInput{
145+
AssociationId: association.AssociationId,
146+
ClusterName: association.ClusterName,
147+
RoleArn: &roleArn,
148+
}
149+
150+
_, err := client.UpdatePodIdentityAssociation(input)
151+
if err != nil {
152+
return fmt.Errorf("failed updating pod identity association: %w", err)
153+
}
154+
155+
fmt.Printf("Updated pod identity to use role %s\n", roleArn)
156+
157+
return nil
158+
}
159+
160+
func getRoleArn(sess *session.Session, roleName string) (string, error) {
161+
client := iam.New(sess)
162+
163+
input := &iam.GetRoleInput{
164+
RoleName: &roleName,
165+
}
166+
167+
output, err := client.GetRole(input)
168+
if err != nil {
169+
return "", fmt.Errorf("failed looking up role %s: %w", roleName, err)
170+
}
171+
172+
return *output.Role.Arn, nil
173+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Using EKS Pod Identity for CAPA Controller
2+
3+
You can use [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) to supply the credentials for the CAPA controller when the management is in EKS. This is an alternative to using the static boostrap credentials or IRSA.
4+
5+
## Pre-requisites
6+
7+
- Management cluster must be an EKS cluster
8+
- AWS environment variables set for your account
9+
10+
## Steps
11+
12+
1. Install the **Amazon EKS Pod Identity Agent** EKS addon into the cluster. This can be done using the AWS console or using the AWS cli.
13+
14+
> NOTE: If your management cluster is a "self-managed" CAPI cluster then its possible to install the addon via the **EKSManagedControlPlane**.
15+
16+
2. Create an EKS pod identity association for CAPA by running the following (replacing **<clustername>** with the name of your EKS cluster):
17+
18+
```bash
19+
clusterawsadm controller use-pod-identity --cluster-name <clustername>
20+
```
21+
22+
3. Ensure any credentials set for the controller are removed (a.k.a zeroed out):
23+
24+
```bash
25+
clusterawsadm controller zero-credentials --namespace=capa-system
26+
```
27+
28+
4. Force CAPA to restart so that the AWS credentials are injected:
29+
30+
```bash
31+
clusterawsadm controller rollout-controller --kubeconfig=kubeconfig --namespace=capa-system
32+
```

docs/book/src/topics/eks/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ And a number of new templates are available in the templates folder for creating
3636
* [Using EKS Console](eks-console.md)
3737
* [Using EKS Addons](addons.md)
3838
* [Enabling Encryption](encryption.md)
39-
* [Cluster Upgrades](cluster-upgrades.md)
39+
* [Cluster Upgrades](cluster-upgrades.md)
40+
* [Using EKS Pod Identity for controller credentials](eks-pod-identity.md)

0 commit comments

Comments
 (0)