Configuring an Amazon EKS Kubernetes service account to assume an IAM role with Pulumi
A core feature of Amazon EKS is the ability to assume an AWS IAM role with a Kubernetes service account. This requires that the service account is annotated with the IAM role's ARN and a trust policy attached to the IAM role that contains the service account's name as well as information about the Kubernetes cluster's OIDC provider.
With Pulumi, we can delegate the tedious work of creating the trust policy (specified in the AssumeRolePolicyDocument and thus also known as the AssumeRolePolicy) and the service account annotation to a utility function, keeping the main program focused on the core resources.
Here's a complete best practice example for setting up a utility function to associate an Amazon EKS Kubernetes service account with an AWS IAM role.
Since we need to specify the trust policy when creating the IAM role, which in turn contains information about our service account, we first create the service account in our main program. Then, we pass this resource object to the utility function:
import pulumi_kubernetes as k8s
k8s_provider = k8s.provider(
"k8s-provider",
enable_server_side_apply=True, # required for patching
)
service_account = k8s.core.v1.ServiceAccount(
"my-service-account",
metadata=k8s.meta.v1.ObjectMetaArgs(
name="my-sa",
namespace="my-namespace",
),
opts=pulumi.ResourceOptions(provider=k8s_provider),
)
iam_role, sa_annotation = create_iam_role_for_service_account(
"my-service-account-iam-role",
service_account=service_account,
oidc_provider_arn=...,
k8s_opts=pulumi.ResourceOptions(provider=k8s_provider),
tags={"managedBy": "Pulumi"},
)
The create_iam_role_for_service_account
function looks as follows:
import pulumi
import pulumi_aws as aws
import pulumi_kubernetes as k8s
def create_iam_role_for_service_account(
resource_name: str,
*,
service_account: k8s.core.v1.ServiceAccount,
oidc_provider_arn: str | pulumi.Output[str],
k8s_opts: pulumi.ResourceOptions,
**iam_role_kwargs,
) -> tuple[aws.iam.Role, k8s.core.v1.ServiceAccountPatch]:
# ensure that oidc_provider_arn is a pulumi.Output
oidc_provider_arn = pulumi.Output.from_input(oidc_provider_arn)
iam_policy_document = aws.iam.get_policy_document_output(
statements=[
aws.iam.GetPolicyDocumentStatementArgs(
actions=["sts:AssumeRoleWithWebIdentity"],
conditions=[
aws.iam.GetPolicyDocumentStatementConditionArgs(
test="StringEquals",
values=["sts.amazonaws.com"],
variable=oidc_provider_arn.apply(
lambda arn: arn.split("/", 1)[1] + ":aud"
),
),
aws.iam.GetPolicyDocumentStatementConditionArgs(
test="StringEquals",
variable=oidc_provider_arn.apply(
lambda arn: arn.split("/", 1)[1] + ":sub"
),
values=[
pulumi.Output.concat(
"system:serviceaccount:",
service_account.metadata.namespace,
":",
service_account.metadata.name,
)
],
),
],
principals=[
aws.iam.GetPolicyDocumentStatementPrincipalArgs(
type="Federated",
identifiers=[oidc_provider_arn],
)
],
),
]
)
# prevent the user from accidentally overwriting args
iam_role_kwargs.pop("resource_name", None)
iam_role_kwargs.pop("assume_role_policy", None)
iam_role = aws.iam.Role(
resource_name,
assume_role_policy=iam_policy_document.json,
name=pulumi.Output.concat(
service_account.metadata.name, "-role"
),
**iam_role_kwargs,
)
service_account_annotation = k8s.core.v1.ServiceAccountPatch(
f"{resource_name}-sa-annotation",
metadata=k8s.meta.v1.ObjectMetaPatchArgs(
name=service_account.metadata.name,
namespace=service_account.metadata.namespace,
annotations={
"eks.amazonaws.com/role-arn": iam_role.arn,
},
),
opts=k8s_opts,
)
return iam_role, service_account_annotation
Instead of the catch-all iam_role_kwargs
you can explicitly specify (parts of) the arguments of aws.iam.Role
in the signature of create_iam_role_for_service_account
.
I generally find it preferable to not use inline_policy
and managed_policy_arns
on the aws.iam.Role
but to work with aws.iam.RolePolicy
and aws.iam.RolePolicyAttachment
in the main program. This provides greater transparency and makes it easier to reason about permissions.
Resources that rely on assuming the AWS IAM role through the service account have to not only depend on any relevant policy attachments, but also on the service_account_annotation
returned by the create_iam_role_for_service_account()
function:
service_account = k8s.core.v1.ServiceAccount(
"my-service-account",
# ...
)
iam_role, sa_annotation = create_iam_role_for_service_account(
"my-role",
service_account=service_account,
# ...
)
some_policy = aws.iam.RolePolicy(
"my-policy",
role=iam_role.id,
# ...
)
other_resource = k8s.apps.v1.Deployment(
"my-deployment",
# ...
opts=pulumi.ResourceOptions(
depends_on=[sa_annotation, some_policy]
),
)
Subscribe to my newsletter
Read articles from Kilian Kluge directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kilian Kluge
Kilian Kluge
My journey into software and infrastructure engineering started in a physics research lab, where I discovered the merits of loose coupling and adherence to standards the hard way. I like automated testing, concise documentation, and hunting complex bugs.