Skip to content

✨ Add EKS Pod Identities to AWSManagedControlPlane #4906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,35 @@ spec:
description: Partition is the AWS security partition being used. Defaults
to "aws"
type: string
podIdentityAssociations:
description: |-
PodIdentityAssociations represent Kubernetes Service Accounts mapping to AWS IAM Roles without IRSA, using EKS Pod Identity.
This requires using the AWS EKS Addon for Pod Identity.
items:
description: PodIdentityAssociation represents an association between
a Kubernetes Service Account in a namespace, and an AWS IAM role
which allows the service principal `pods.eks.amazonaws.com` in
its trust policy.
properties:
roleARN:
description: RoleARN is the ARN of an IAM role which the Service
Account can assume.
type: string
serviceAccountName:
description: ServiceAccountName is the name of the kubernetes
Service Account within the namespace
type: string
serviceAccountNamespace:
default: default
description: ServiceAccountNamespace is the kubernetes namespace,
which the kubernetes Service Account resides in. Defaults
to "default" namespace.
type: string
required:
- serviceAccountName
- serviceAccountNamespace
type: object
type: array
region:
description: The AWS Region the cluster lives in.
type: string
Expand Down
1 change: 1 addition & 0 deletions controlplane/eks/api/v1beta1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func (r *AWSManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error {
}
dst.Spec.VpcCni.Disable = r.Spec.DisableVPCCNI
dst.Spec.Partition = restored.Spec.Partition
dst.Spec.PodIdentityAssociations = restored.Spec.PodIdentityAssociations

return nil
}
Expand Down
1 change: 1 addition & 0 deletions controlplane/eks/api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ type AWSManagedControlPlaneSpec struct { //nolint: maligned
// +optional
Addons *[]Addon `json:"addons,omitempty"`

// PodIdentityAssociations represent Kubernetes Service Accounts mapping to AWS IAM Roles without IRSA, using EKS Pod Identity.
// This requires using the AWS EKS Addon for Pod Identity.
// +optional
PodIdentityAssociations []PodIdentityAssociation `json:"podIdentityAssociations,omitempty"`

// IdentityProviderconfig is used to specify the oidc provider config
// to be attached with this eks cluster
// +optional
Expand Down
31 changes: 29 additions & 2 deletions controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -63,8 +64,10 @@ func (r *AWSManagedControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error
// +kubebuilder:webhook:verbs=create;update,path=/validate-controlplane-cluster-x-k8s-io-v1beta2-awsmanagedcontrolplane,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanes,versions=v1beta2,name=validation.awsmanagedcontrolplanes.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-controlplane-cluster-x-k8s-io-v1beta2-awsmanagedcontrolplane,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanes,versions=v1beta2,name=default.awsmanagedcontrolplanes.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1

var _ webhook.Defaulter = &AWSManagedControlPlane{}
var _ webhook.Validator = &AWSManagedControlPlane{}
var (
_ webhook.Defaulter = &AWSManagedControlPlane{}
_ webhook.Validator = &AWSManagedControlPlane{}
)

func parseEKSVersion(raw string) (*version.Version, error) {
v, err := version.ParseGeneric(raw)
Expand Down Expand Up @@ -95,6 +98,7 @@ func (r *AWSManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
allErrs = append(allErrs, r.validateNetwork()...)
allErrs = append(allErrs, r.validatePrivateDNSHostnameTypeOnLaunch()...)
allErrs = append(allErrs, r.validServiceAccountName()...)

if len(allErrs) == 0 {
return nil, nil
Expand Down Expand Up @@ -129,6 +133,7 @@ func (r *AWSManagedControlPlane) ValidateUpdate(old runtime.Object) (admission.W
allErrs = append(allErrs, r.validateKubeProxy()...)
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
allErrs = append(allErrs, r.validatePrivateDNSHostnameTypeOnLaunch()...)
allErrs = append(allErrs, r.validServiceAccountName()...)

if r.Spec.Region != oldAWSManagedControlplane.Spec.Region {
allErrs = append(allErrs,
Expand Down Expand Up @@ -403,6 +408,28 @@ func (r *AWSManagedControlPlane) validatePrivateDNSHostnameTypeOnLaunch() field.
return allErrs
}

func (r *AWSManagedControlPlane) validServiceAccountName() field.ErrorList {
var allErrs field.ErrorList

if r.Spec.PodIdentityAssociations != nil {
for i, association := range r.Spec.PodIdentityAssociations {
associationPath := field.NewPath("spec", "podIdentityAssociations").Index(i)
if association.ServiceAccountName == "" {
allErrs = append(allErrs, field.Required(associationPath.Child("serviceAccountName"), "serviceAccountName is required"))
}

// kubernetes uses ValidateServiceAccountName internally, which maps to IsDNS1123Subdomain
// https://github.com/kubernetes/apimachinery/blob/d794766488ac2892197a7cc8d0b4b46b0edbda80/pkg/api/validation/generic.go#L68

if validationErrs := validation.IsDNS1123Subdomain(association.ServiceAccountName); len(validationErrs) > 0 {
allErrs = append(allErrs, field.Invalid(associationPath.Child("serviceAccountName"), association.ServiceAccountName, fmt.Sprintf("serviceAccountName is invalid: %v", validationErrs)))
}
}
}

return allErrs
}

func (r *AWSManagedControlPlane) validateNetwork() field.ErrorList {
var allErrs field.ErrorList

Expand Down
7 changes: 7 additions & 0 deletions controlplane/eks/api/v1beta2/conditions_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ const (
EKSAddonsConfiguredFailedReason = "EKSAddonsConfiguredFailed"
)

const (
// EKSPodIdentityAssociationConfiguredCondition condition reports on the successful reconciliation of EKS pod identity associations.
EKSPodIdentityAssociationConfiguredCondition clusterv1.ConditionType = "EKSPodIdentityAssociationConfigured"
// EKSPodIdentityAssociationFailedReason is used to report failures while reconciling the EKS pod identity associations.
EKSPodIdentityAssociationFailedReason = "EKSPodIdentityAssociationConfigurationFailed"
)

const (
// EKSIdentityProviderConfiguredCondition condition reports on the successful association of identity provider config.
EKSIdentityProviderConfiguredCondition clusterv1.ConditionType = "EKSIdentityProviderConfigured"
Expand Down
24 changes: 18 additions & 6 deletions controlplane/eks/api/v1beta2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,10 @@ var (
EKSTokenMethodAWSCli = EKSTokenMethod("aws-cli")
)

var (
// DefaultEKSControlPlaneRole is the name of the default IAM role to use for the EKS control plane
// if no other role is supplied in the spec and if iam role creation is not enabled. The default
// can be created using clusterawsadm or created manually.
DefaultEKSControlPlaneRole = fmt.Sprintf("eks-controlplane%s", iamv1.DefaultNameSuffix)
)
// DefaultEKSControlPlaneRole is the name of the default IAM role to use for the EKS control plane
// if no other role is supplied in the spec and if iam role creation is not enabled. The default
// can be created using clusterawsadm or created manually.
var DefaultEKSControlPlaneRole = fmt.Sprintf("eks-controlplane%s", iamv1.DefaultNameSuffix)

// IAMAuthenticatorConfig represents an aws-iam-authenticator configuration.
type IAMAuthenticatorConfig struct {
Expand Down Expand Up @@ -279,3 +277,17 @@ type OIDCIdentityProviderConfig struct {
// +optional
Tags infrav1.Tags `json:"tags,omitempty"`
}

// PodIdentityAssociation represents an association between a Kubernetes Service Account in a namespace, and an AWS IAM role which allows the service principal `pods.eks.amazonaws.com` in its trust policy.
type PodIdentityAssociation struct {
// ServiceAccountName is the name of the kubernetes Service Account within the namespace
// +kubebuilder:validation:Required
ServiceAccountName string `json:"serviceAccountName"`
// ServiceAccountNamespace is the kubernetes namespace, which the kubernetes Service Account resides in. Defaults to "default" namespace.
// +kubebuilder:validation:Required
// +kubebuilder:default=default
ServiceAccountNamespace string `json:"serviceAccountNamespace"`
// RoleARN is the ARN of an IAM role which the Service Account can assume.
// +kubebuilder:validation:Required
RoleARN string `json:"roleARN,omitempty"`
}
20 changes: 20 additions & 0 deletions controlplane/eks/api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,8 @@ func mockedCallsForMissingEverything(ec2Rec *mocks.MockEC2APIMockRecorder, subne
Name: aws.String("tag-key"),
Values: aws.StringSlice([]string{"sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"}),
},
}})).Return(&ec2.DescribeRouteTablesOutput{
},
})).Return(&ec2.DescribeRouteTablesOutput{
RouteTables: []*ec2.RouteTable{
{
Routes: []*ec2.Route{
Expand Down Expand Up @@ -469,7 +470,8 @@ func mockedCallsForMissingEverything(ec2Rec *mocks.MockEC2APIMockRecorder, subne
Name: aws.String("state"),
Values: aws.StringSlice([]string{ec2.VpcStatePending, ec2.VpcStateAvailable}),
},
}}), gomock.Any()).Return(nil).MinTimes(1).MaxTimes(2)
},
}), gomock.Any()).Return(nil).MinTimes(1).MaxTimes(2)

ec2Rec.DescribeAddressesWithContext(context.TODO(), gomock.Eq(&ec2.DescribeAddressesInput{
Filters: []*ec2.Filter{
Expand Down Expand Up @@ -901,6 +903,12 @@ func mockedEKSCluster(g *WithT, eksRec *mock_eksiface.MockEKSAPIMockRecorder, ia
ClusterName: aws.String("test-cluster"),
}).Return(&eks.ListAddonsOutput{}, nil)

eksRec.ListPodIdentityAssociationsWithContext(context.TODO(), gomock.Eq(&eks.ListPodIdentityAssociationsInput{
ClusterName: aws.String("test-cluster"),
})).Return(&eks.ListPodIdentityAssociationsOutput{
Associations: []*eks.PodIdentityAssociationSummary{},
}, nil)

awsNodeRec.ReconcileCNI(gomock.Any()).Return(nil)
kubeProxyRec.ReconcileKubeProxy(gomock.Any()).Return(nil)
iamAuthenticatorRec.ReconcileIAMAuthenticator(gomock.Any()).Return(nil)
Expand Down
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY_PREFIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Creating a cluster](./topics/eks/creating-a-cluster.md)
- [Using EKS Console](./topics/eks/eks-console.md)
- [Using EKS Addons](./topics/eks/addons.md)
- [Using EKS Pod Identity Associations](./topics/eks/pod-identity-associations.md)
- [Enabling Encryption](./topics/eks/encryption.md)
- [Cluster Upgrades](./topics/eks/cluster-upgrades.md)
- [ROSA Support](./topics/rosa/index.md)
Expand Down
88 changes: 88 additions & 0 deletions docs/book/src/topics/eks/pod-identity-associations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# EKS Pod Identity Associations

[EKS Pod Identity Associations](https://aws.amazon.com/blogs/containers/introducing-amazon-eks-add-ons/) can be used with EKS clusters created using Cluster API Provider AWS.

## Prerequisites

### Setting up the IAM role in AWS

Outside of CAPI/CAPA, you must first create an IAM Role which allows the `pods.eks.amazonaws.com` service principal in the trust policy. EKS Identities trust relationships must also include the `sts:TagSession` permission (on top of the `sts:AssumeRole` permission).

This is a sample trust policy which allows a kubernetes service account to assume this role. We'll call the role `capi-test-role` in the next steps.

```yaml
{
"Version": "2012-10-17",
"Statement":
[
{
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"],
},
],
}
```

### Installing the EKS Pod Identity Agent

The EKS Pod Identity Agent can be installed as a Managed Add-on through the AWS Console, or through CAPA.
To install the addon through CAPA, add it to `AWSManagedControlPlane`. Please ensure that the version is up to date, according to the [addons section](addons.md).

```yaml
# [...]
kind: AWSManagedControlPlane
spec:
# [...]
addons:
# [...]
- conflictResolution: overwrite
name: eks-pod-identity-agent
version: v1.1.0-eksbuild.1
```

You can verify that this is running on your kubernetes cluster with `kubectl get deploy -A | grep eks`

## Mapping a service account to an IAM role

Now that you have created a role `capi-test-role` in AWS, and have added the EKS agent to your cluster, we must add the following to our `AWSManagedControlPlane` under `.spec.podIdentityAssociations`

```yaml
# [...]
kind: AWSManagedControlPlane
spec:
# [...]
podIdentityAssociations:
- serviceAccountNamespace: default
serviceAccountName: myserviceaccount
roleARN: arn:aws:iam::012345678901:role/capi-test-role
```

- `.serviceAccountNamespace` and `.serviceAccountName` refer to the [`ServiceAccount`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) object in the Kubernetes cluster
- `.roleARN` is the AWS ARN for the IAM role you created in step 1 (named `capi-test-role` in this tutorial). Make sure to copy this exactly from your AWS console (`IAM > Roles`).

To use the same IAM role across multiple service accounts/namespaces, you must create multiple associations.

A full CAPA example of everything mentioned above, including 2 role mappings, is shown below:

```yaml
kind: AWSManagedControlPlane
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
metadata:
name: "capi-managed-test"
spec:
region: "eu-west-2"
sshKeyName: "capi-management"
version: "v1.27.0"
addons:
- conflictResolution: overwrite
name: eks-pod-identity-agent
version: v1.1.0-eksbuild.1
podIdentityAssociations:
- serviceAccountNamespace: default
serviceAccountName: myserviceaccount
roleARN: arn:aws:iam::012345678901:role/capi-test-role
- serviceAccountNamespace: another-namespace
serviceAccountName: another-service-account
roleARN: arn:aws:iam::012345678901:role/capi-test-role
```
2 changes: 1 addition & 1 deletion pkg/cloud/services/eks/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (s *Service) reconcileAddons(ctx context.Context) error {
// Get available addon names for the cluster
addonNames, err := s.listAddons(eksClusterName)
if err != nil {
s.Error(err, "failed listing addons")
s.scope.Error(err, "failed listing addons")
return fmt.Errorf("listing eks addons: %w", err)
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/cloud/services/eks/eks.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func (s *Service) ReconcileControlPlane(ctx context.Context) error {
}
conditions.MarkTrue(s.scope.ControlPlane, ekscontrolplanev1.EKSAddonsConfiguredCondition)

// EKS Pod Identity Associations
if err := s.reconcilePodIdentities(ctx); err != nil {
conditions.MarkFalse(s.scope.ControlPlane, ekscontrolplanev1.EKSPodIdentityAssociationConfiguredCondition, ekscontrolplanev1.EKSPodIdentityAssociationFailedReason, clusterv1.ConditionSeverityError, err.Error())
return errors.Wrap(err, "failed reconciling eks pod identity associations")
}
conditions.MarkTrue(s.scope.ControlPlane, ekscontrolplanev1.EKSPodIdentityAssociationConfiguredCondition)

// EKS Identity Provider
if err := s.reconcileIdentityProvider(ctx); err != nil {
conditions.MarkFalse(s.scope.ControlPlane, ekscontrolplanev1.EKSIdentityProviderConfiguredCondition, ekscontrolplanev1.EKSIdentityProviderConfiguredFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
Expand Down
Loading