diff --git a/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go b/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go index 049de10431..add074a8c0 100644 --- a/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go +++ b/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go @@ -51,8 +51,8 @@ func (t Template) controllersPolicyRoleAttachments() []string { return attachments } -func (t Template) controllersTrustPolicy() *iamv1.PolicyDocument { - policyDocument := ec2AssumeRolePolicy() +func (t Template) controllersTrustPolicy(eksEnabled bool) *iamv1.PolicyDocument { + policyDocument := ec2AssumeRolePolicy(eksEnabled) policyDocument.Statement = append(policyDocument.Statement, t.Spec.ClusterAPIControllers.TrustStatements...) return policyDocument } diff --git a/cmd/clusterawsadm/cloudformation/bootstrap/control_plane.go b/cmd/clusterawsadm/cloudformation/bootstrap/control_plane.go index 06cdff6a55..15ee33fcaf 100644 --- a/cmd/clusterawsadm/cloudformation/bootstrap/control_plane.go +++ b/cmd/clusterawsadm/cloudformation/bootstrap/control_plane.go @@ -40,7 +40,7 @@ func (t Template) controlPlanePolicies() []cfn_iam.Role_Policy { } func (t Template) controlPlaneTrustPolicy() *iamv1.PolicyDocument { - policyDocument := ec2AssumeRolePolicy() + policyDocument := ec2AssumeRolePolicy(false) policyDocument.Statement = append(policyDocument.Statement, t.Spec.ControlPlane.TrustStatements...) return policyDocument } diff --git a/cmd/clusterawsadm/cloudformation/bootstrap/node.go b/cmd/clusterawsadm/cloudformation/bootstrap/node.go index a17db15ad2..5e04f7bfa7 100644 --- a/cmd/clusterawsadm/cloudformation/bootstrap/node.go +++ b/cmd/clusterawsadm/cloudformation/bootstrap/node.go @@ -39,7 +39,7 @@ func (t Template) nodePolicies() []cfn_iam.Role_Policy { } func (t Template) nodeTrustPolicy() *iamv1.PolicyDocument { - policyDocument := ec2AssumeRolePolicy() + policyDocument := ec2AssumeRolePolicy(false) policyDocument.Statement = append(policyDocument.Statement, t.Spec.Nodes.TrustStatements...) return policyDocument } diff --git a/cmd/clusterawsadm/cloudformation/bootstrap/template.go b/cmd/clusterawsadm/cloudformation/bootstrap/template.go index c4eb4cbff7..24adad2476 100644 --- a/cmd/clusterawsadm/cloudformation/bootstrap/template.go +++ b/cmd/clusterawsadm/cloudformation/bootstrap/template.go @@ -146,7 +146,7 @@ func (t Template) RenderCloudFormation() *cloudformation.Template { template.Resources[AWSIAMRoleControllers] = &cfn_iam.Role{ RoleName: t.NewManagedName("controllers"), - AssumeRolePolicyDocument: t.controllersTrustPolicy(), + AssumeRolePolicyDocument: t.controllersTrustPolicy(!t.Spec.EKS.Disable), Policies: t.controllersRolePolicy(), Tags: converters.MapToCloudFormationTags(t.Spec.ClusterAPIControllers.Tags), } @@ -218,8 +218,12 @@ func (t Template) RenderCloudFormation() *cloudformation.Template { return template } -func ec2AssumeRolePolicy() *iamv1.PolicyDocument { - return AssumeRolePolicy(iamv1.PrincipalService, []string{"ec2.amazonaws.com"}) +func ec2AssumeRolePolicy(withEKS bool) *iamv1.PolicyDocument { + principalIDs := []string{"ec2.amazonaws.com"} + if withEKS { + principalIDs = append(principalIDs, "pods.eks.amazonaws.com") + } + return AssumeRolePolicy(iamv1.PrincipalService, principalIDs) } // AWSArnAssumeRolePolicy will assume Policies using PolicyArns. diff --git a/cmd/clusterawsadm/cmd/controller/controller.go b/cmd/clusterawsadm/cmd/controller/controller.go index 31e018d432..1d07ae1136 100644 --- a/cmd/clusterawsadm/cmd/controller/controller.go +++ b/cmd/clusterawsadm/cmd/controller/controller.go @@ -44,6 +44,7 @@ func RootCmd() *cobra.Command { newCmd.AddCommand(credentials.UpdateCredentialsCmd()) newCmd.AddCommand(credentials.PrintCredentialsCmd()) newCmd.AddCommand(rollout.RolloutControllersCmd()) + newCmd.AddCommand(credentials.UseEKSPodIdentityCmd()) return newCmd } diff --git a/cmd/clusterawsadm/cmd/controller/credentials/use_pod_identity.go b/cmd/clusterawsadm/cmd/controller/credentials/use_pod_identity.go new file mode 100644 index 0000000000..3acb79b4cd --- /dev/null +++ b/cmd/clusterawsadm/cmd/controller/credentials/use_pod_identity.go @@ -0,0 +1,173 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package credentials + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/spf13/cobra" + + "sigs.k8s.io/cluster-api/cmd/clusterctl/cmd" +) + +// UseEKSPodIdentityCmd is a CLI command that will enable using EKS pod identity for CAPA. +func UseEKSPodIdentityCmd() *cobra.Command { + clusterName := "" + region := "" + namespace := "" + serviceAccount := "" + roleName := "" + + newCmd := &cobra.Command{ + Use: "use-pod-identity", + Short: "Enable EKS pod identiy with CAPA", + Long: cmd.LongDesc(` + Updates CAPA running in an EKS cluster to use EKS pod identity + `), + Example: cmd.Examples(` + clusterawsadm controller use-pod-identity --cluster-name cluster1 + `), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return usePodIdentity(region, clusterName, namespace, serviceAccount, roleName) + }, + } + + newCmd.Flags().StringVarP(®ion, "region", "r", "", "The AWS region containing the EKS cluster") + newCmd.Flags().StringVarP(&clusterName, "cluster-name", "n", "", "The name of the EKS management cluster") + newCmd.Flags().StringVar(&namespace, "namespace", "capa-system", "The namespace of CAPA controller") + newCmd.Flags().StringVar(&serviceAccount, "service-account", "capa-controller-manager", "The service account for the CAPA controller") + 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.") + + newCmd.MarkFlagRequired("cluster-name") + + return newCmd +} + +func usePodIdentity(region, clusterName, namespace, serviceAccount, roleName string) error { + cfg := aws.Config{} + if region != "" { + cfg.Region = aws.String(region) + } + + sess, err := session.NewSessionWithOptions(session.Options{ + SharedConfigState: session.SharedConfigEnable, + Config: cfg, + }) + if err != nil { + return fmt.Errorf("failed creating aws session: %w", err) + } + + roleArn, err := getRoleArn(sess, roleName) + if err != nil { + return err + } + + eksClient := eks.New(sess) + + listInput := &eks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(clusterName), + Namespace: aws.String(namespace), + } + + listOutput, err := eksClient.ListPodIdentityAssociations(listInput) + if err != nil { + return fmt.Errorf("listing existing pod identity associations for cluster %s in namespace %s: %w", clusterName, namespace, err) + } + + for _, association := range listOutput.Associations { + if *association.ServiceAccount == serviceAccount { + needsUpdate, err := podIdentityNeedsUpdate(eksClient, association, roleName) + if err != nil { + return err + } + if !needsUpdate { + fmt.Printf("EKS pod association for service account %s already exists, no action taken\n", serviceAccount) + } + + return updatePodIdentity(eksClient, association, roleName) + } + } + + fmt.Printf("Creating pod association for service account %s.....\n", serviceAccount) + + createInpuut := &eks.CreatePodIdentityAssociationInput{ + ClusterName: &clusterName, + Namespace: &namespace, + RoleArn: &roleArn, + ServiceAccount: &serviceAccount, + } + + output, err := eksClient.CreatePodIdentityAssociation(createInpuut) + if err != nil { + return fmt.Errorf("failed to create pod identity association: %w", err) + } + + fmt.Printf("Created pod identity association (%s)\n", *output.Association.AssociationId) + + return nil +} + +func podIdentityNeedsUpdate(client *eks.EKS, association *eks.PodIdentityAssociationSummary, roleArn string) (bool, error) { + input := &eks.DescribePodIdentityAssociationInput{ + AssociationId: association.AssociationId, + ClusterName: association.ClusterName, + } + + output, err := client.DescribePodIdentityAssociation(input) + if err != nil { + return false, fmt.Errorf("failed describing pod identity association: %w", err) + } + + return *output.Association.RoleArn != roleArn, nil +} + +func updatePodIdentity(client *eks.EKS, association *eks.PodIdentityAssociationSummary, roleArn string) error { + input := &eks.UpdatePodIdentityAssociationInput{ + AssociationId: association.AssociationId, + ClusterName: association.ClusterName, + RoleArn: &roleArn, + } + + _, err := client.UpdatePodIdentityAssociation(input) + if err != nil { + return fmt.Errorf("failed updating pod identity association: %w", err) + } + + fmt.Printf("Updated pod identity to use role %s\n", roleArn) + + return nil +} + +func getRoleArn(sess *session.Session, roleName string) (string, error) { + client := iam.New(sess) + + input := &iam.GetRoleInput{ + RoleName: &roleName, + } + + output, err := client.GetRole(input) + if err != nil { + return "", fmt.Errorf("failed looking up role %s: %w", roleName, err) + } + + return *output.Role.Arn, nil +} diff --git a/docs/book/src/topics/eks/eks-pod-identity.md b/docs/book/src/topics/eks/eks-pod-identity.md new file mode 100644 index 0000000000..80919769e3 --- /dev/null +++ b/docs/book/src/topics/eks/eks-pod-identity.md @@ -0,0 +1,32 @@ +# Using EKS Pod Identity for CAPA Controller + +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. + +## Pre-requisites + +- Management cluster must be an EKS cluster +- AWS environment variables set for your account + +## Steps + +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. + +> NOTE: If your management cluster is a "self-managed" CAPI cluster then its possible to install the addon via the **EKSManagedControlPlane**. + +2. Create an EKS pod identity association for CAPA by running the following (replacing **** with the name of your EKS cluster): + +```bash +clusterawsadm controller use-pod-identity --cluster-name +``` + +3. Ensure any credentials set for the controller are removed (a.k.a zeroed out): + +```bash +clusterawsadm controller zero-credentials --namespace=capa-system +``` + +4. Force CAPA to restart so that the AWS credentials are injected: + +```bash +clusterawsadm controller rollout-controller --kubeconfig=kubeconfig --namespace=capa-system +``` \ No newline at end of file diff --git a/docs/book/src/topics/eks/index.md b/docs/book/src/topics/eks/index.md index 9312cc4eaa..2bc44fc400 100644 --- a/docs/book/src/topics/eks/index.md +++ b/docs/book/src/topics/eks/index.md @@ -36,4 +36,5 @@ And a number of new templates are available in the templates folder for creating * [Using EKS Console](eks-console.md) * [Using EKS Addons](addons.md) * [Enabling Encryption](encryption.md) -* [Cluster Upgrades](cluster-upgrades.md) \ No newline at end of file +* [Cluster Upgrades](cluster-upgrades.md) +* [Using EKS Pod Identity for controller credentials](eks-pod-identity.md) \ No newline at end of file