Any Pulumi golang based example for setting AWS EK...
# golang
r
Any Pulumi golang based example for setting AWS EKS IRSA?
b
I just did this yesterday. I’ll share it when I’m at a computer in a little bit - probably 30-60 minutes.
Creating the IRSA resources:
Copy code
func (tenant *Tenant) CreateIRSA() error {
	ctx := tenant.pulumiCtx

	oidcUrlRef := tenant.infraStack.GetStringOutput(pulumi.String("eks-oidc-url"))
	oidcArnRef := tenant.infraStack.GetStringOutput(pulumi.String("eks-oidc-arn"))

	serviceAccountName := fmt.Sprintf("tenant-%s-s3-sa", tenant.name)
	roleName := fmt.Sprintf("tenant-%s-s3-role", tenant.name)
	policyName := fmt.Sprintf("tenant-%s-s3-policy", tenant.name)

	allow := "Allow"
	assumeRolePolicy := pulumi.All(tenant.namespace.Metadata.Name(), oidcUrlRef, oidcArnRef).ApplyT(func(args []interface{}) (string, error) {
		namespaceName := args[0].(*string)
		oidcUrl := args[1].(string)
		oidcArn := args[2].(string)
		source, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
			Statements: []iam.GetPolicyDocumentStatement{
				{
					Actions: []string{
						"sts:AssumeRoleWithWebIdentity",
					},
					Conditions: []iam.GetPolicyDocumentStatementCondition{
						{
							Test:     "StringEquals",
							Values:   []string{fmt.Sprintf("system:serviceaccount:%s:%s", *namespaceName, serviceAccountName)},
							Variable: fmt.Sprintf("%s:sub", strings.TrimPrefix(oidcUrl, "https://")),
						},
					},
					Effect: &allow,
					Principals: []iam.GetPolicyDocumentStatementPrincipal{
						{
							Identifiers: []string{oidcArn},
							Type:        "Federated",
						},
					},
				},
			},
		}, nil)
		if err != nil {
			return "", err
		}
		return source.Json, nil
	})

	serviceRole, err := iam.NewRole(ctx, roleName, &iam.RoleArgs{
		AssumeRolePolicy: assumeRolePolicy,
	})
	if err != nil {
		return err
	}

	infraBucket := tenant.infraStack.GetStringOutput(pulumi.String("bucket"))
	s3PolicyDoc := pulumi.All(infraBucket).ApplyT(func(args []interface{}) (string, error) {
		bucket := args[0].(string)

		bucketArn := fmt.Sprintf("arn:aws:s3:::%s", bucket)
		auditLogPrefix := fmt.Sprintf("tenants/logs/audit/tenant=%s", tenant.name)
		analyticsLogPrefix := fmt.Sprintf("tenants/logs/analytics/tenant=%s", tenant.name)
		fsAuditLogPrefix := fmt.Sprintf("tenants/logs/fs-audit/tenant=%s", tenant.name)
		operationaLogPrefix := fmt.Sprintf("tenants/logs/operational/tenant=%s", tenant.name)
		source, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
			Statements: []iam.GetPolicyDocumentStatement{
				// Some base bucket policies
				{
					Actions: []string{
						"s3:HeadBucket",
					},
					Resources: []string{
						bucketArn,
					},
					Effect: &allow,
				},
				// Allowing listing the log dirs
				{
					Actions: []string{
						"s3:ListBucket",
					},
					Resources: []string{
						bucketArn,
					},
					Conditions: []iam.GetPolicyDocumentStatementCondition{
						{
							Test: "StringLike",
							Values: []string{
								fmt.Sprintf("%s", auditLogPrefix),
								fmt.Sprintf("%s/*", auditLogPrefix),
								fmt.Sprintf("%s", analyticsLogPrefix),
								fmt.Sprintf("%s/*", analyticsLogPrefix),
								fmt.Sprintf("%s", fsAuditLogPrefix),
								fmt.Sprintf("%s/*", fsAuditLogPrefix),
								fmt.Sprintf("%s", operationaLogPrefix),
								fmt.Sprintf("%s/*", operationaLogPrefix),
							},
							Variable: "s3:prefix",
						},
					},
					Effect: &allow,
				},
				// Allow read/write/delete on the logging buckets
				{
					Actions: []string{
						"s3:DeleteObject",
						"s3:GetEncryptionConfiguration",
						"s3:GetObject",
						"s3:GetObjectTagging",
						"s3:PutObject",
					},
					Resources: []string{
						fmt.Sprintf("%s/%s", bucketArn, auditLogPrefix),
						fmt.Sprintf("%s/%s/*", bucketArn, auditLogPrefix),
						fmt.Sprintf("%s/%s", bucketArn, analyticsLogPrefix),
						fmt.Sprintf("%s/%s/*", bucketArn, analyticsLogPrefix),
						fmt.Sprintf("%s/%s", bucketArn, fsAuditLogPrefix),
						fmt.Sprintf("%s/%s/*", bucketArn, fsAuditLogPrefix),
						fmt.Sprintf("%s/%s", bucketArn, operationaLogPrefix),
						fmt.Sprintf("%s/%s/*", bucketArn, operationaLogPrefix),
					},
					Effect: &allow,
				},
				// Only allow reading the lake bucket
				{
					Actions: []string{
						"s3:GetObject",
						"s3:GetObjectTagging",
						"s3:HeadBucket",
						"s3:ListBucket",
					},
					Resources: []string{
						"arn:aws:s3:::okera-lake",
						"arn:aws:s3:::okera-lake/*",
					},
					Effect: &allow,
				},
			},
		}, nil)
		if err != nil {
			return "", err
		}
		return source.Json, nil
	})
	s3Policy, err := iam.NewPolicy(ctx, policyName, &iam.PolicyArgs{
		Description: pulumi.Sprintf("S3 permissions for tenant %s", tenant.name),
		Policy:      s3PolicyDoc,
		Tags: pulumi.StringMap{
			"tenant": pulumi.String(tenant.name),
		},
	})
	if err != nil {
		return err
	}

	_, err = iam.NewRolePolicyAttachment(ctx, fmt.Sprintf("%s-attachment", policyName), &iam.RolePolicyAttachmentArgs{
		PolicyArn: s3Policy.Arn,
		Role:      serviceRole,
	})
	if err != nil {
		return err
	}

	serviceAccount, err := corev1.NewServiceAccount(ctx, serviceAccountName, &corev1.ServiceAccountArgs{
		Metadata: &metav1.ObjectMetaArgs{
			Namespace: tenant.namespace.Metadata.Name(),
			Name:      pulumi.String(serviceAccountName),
			Annotations: pulumi.StringMap{
				"<http://eks.amazonaws.com/role-arn|eks.amazonaws.com/role-arn>": serviceRole.Arn,
			},
		},
	})
	if err != nil {
		return err
	}

	tenant.serviceAccount = serviceAccount

	return nil
}
And then using it:
Copy code
...

				Spec: &corev1.PodSpecArgs{
					ServiceAccountName: tenant.serviceAccount.Metadata.Name(),
...
A few notes: •
tenant.infraStack
is a
StackReference
where I exported some values • In this case I needed to create a specific S3 policy, which is why it is a bit more verbose, but in theory you could use one of the managed S3 policies. Hope this helps!
r
@bored-table-20691 Thank you very much for sharing the example!
How do you export oidc provier url? I can do this: ctx.Export("ekd-oidc-url", cluster.Core.OidcProvider()), but url is an output in OidcProvider which itself is an output. How do I flatten the nested output?
b
You’re using
pulumi-eks
to create a cluster, yes?
You should be able to just reference the URL member of
OidcProvider()
(it returns an
iam.OpenIdConnectProvider
)
In my case I’m not using
pulumi-eks
, so it looks a bit different (I can’t use it due to some bug 😞 ):
Copy code
eksCluster, err := eks.LookupCluster(ctx, &eks.LookupClusterArgs{
		Name: okeraCfg.Require("eks"),
	})
	if err != nil {
		return err
	}
	awsAccount := okeraCfg.Require("aws-account")
	oidcUrl := eksCluster.Identities[0].Oidcs[0].Issuer
	oidcArn := fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", awsAccount, strings.TrimPrefix(oidcUrl, "https://"))

	kubeconfig := kube.GenerateKubeconfig(
		eksCluster.Endpoint,
		eksCluster.CertificateAuthority.Data,
		eksCluster.Name)

	ctx.Export("kubeconfig", pulumi.ToSecret(kubeconfig))
	ctx.Export("eks-oidc-url", pulumi.String(oidcUrl))
	ctx.Export("eks-oidc-arn", pulumi.String(oidcArn))
r
@bored-table-20691 yes, I'm using pulumi-eks. The OidcProvider() returns iam.OpenIdConnectProviderOutput rather than iam.OpenIdConnectProvider. Hence Url is a nested Output. We would need a way to flatten the nested Output. Much like I used to do this in Scala to flatten Future[Future[]]. Let me see if I can use your approach as a workaround. Again, much appreciated for the help.
b
@ripe-shampoo-80285 is there an issue with Url being a nested output?
r
Yeah, not sure how to export Url in this case. For example, I cannot access Url like this: cluster.Core.OidcProvider().Url I tried to do the following and it doesn't work either (no error, but no eks-oidc-url in the outputs either). cluster.Core.OidcProvider().ApplyT(func(p *iam.OpenIdConnectProvider) *iam.OpenIdConnectProvider { ctx.Export("eks-oidc-url", p.Url) ctx.Export("eks-oidc-arn", p.Arn) return p })
b
Yeah I’d have to try, but I don’t have an example with
pulumi-eks
right now.
r
Thanks Itay, you have been super helpful. Really appreciate! I am using your approach as workaround for the time being, it works!
b
No problem, glad it works. It’s been a process to piece out these examples - a combination of finding Go examples, converting TypeScript ones to go, and then just auto-complete 🙂
b
@bored-table-20691 sorry to resurrect this thread. EKS reccomends you to annotate the
aws-node
service account in the
kube-system
namespace when enabling the CNI plugin. Have you tried doing this?
b
@bright-sandwich-93783 no worries at all. I unfortunately (or maybe fortunately?) have never had to do that. What’s the link to the AWS docs for this?