aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.66k stars 3.92k forks source link

How to create CustomResources in a loop? #26357

Closed zentavr closed 1 year ago

zentavr commented 1 year ago

Describe the bug

I have a code like this:

import { Construct } from 'constructs';

export class EksClusterDeployment extends Construct {
    eksCluster: eks.Cluster;
    const nodeGroupTags: { [key: string]: any } = {};

    constructor(scope: Construct, id: string, props: EksClusterDeploymentProps) {
        this.eksCluster = new eks.Cluster(.......);

        // In the real life this array gets calculated, depends which subnets the operator had defined.
        availabilityZones = ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1f'];

        availabilityZones.forEach(function (az){
            let ng = this.eksCluster.addNodegroupCapacity(id + az.slice(-1), modOptions);

            // Compose the tags
            nodeGroupTags["k8s.io/cluster-autoscaler/node-template/label/failure-domain.beta.kubernetes.io/region"] = cdk.Stack.of(cls).region;
            nodeGroupTags["k8s.io/cluster-autoscaler/node-template/label/failure-domain.beta.kubernetes.io/zone"] = az;
            nodeGroupTags["k8s.io/cluster-autoscaler/node-template/label/topology.ebs.csi.aws.com/zone"] = az;
            nodeGroupTags["k8s.io/cluster-autoscaler/node-template/label/topology.kubernetes.io/region"] = cdk.Stack.of(cls).region;
            nodeGroupTags["k8s.io/cluster-autoscaler/node-template/label/topology.kubernetes.io/zone"] = az;

            // If I put console.log(nodeGroupTags) here - the output would be unique per iteration and this is I can see in my local console

            // Create custom resource which updates ASG tags
            new NodegroupASGModifier(cls, id + az.slice(-1), {
                cluster: cls.eksCluster,
                nodegroup: ng,
                tags: nodeGroupTags
            });
            // Creation and modification of MNG+ASG is done
        });
    }
}

export class NodegroupASGModifier extends Construct {
    constructor(scope: Construct, id: string, props: NodegroupASGModifierProps) {
        super(scope, id);

        const onEventHandler = new lambda.Function(this, id + '-onEventHandler', {
            timeout: cdk.Duration.seconds(60),
            handler: 'index.on_event',
            runtime: lambda.Runtime.PYTHON_3_10,
            code: lambda.Code.fromAsset(path.join(__dirname, '../..', 'resources/lambdas/elk-adds-extratags-to-asg'), {
                //bundling: {
                //    image: lambda.Runtime.PYTHON_3_10.bundlingImage
                //}
            })
        });
        const nodegroupName = (props.nodegroup.node.defaultChild as eks.CfnNodegroup).getAtt('NodegroupName').toString();

        // Allow to describe the Nodegroup and modify Autoscaling group
        onEventHandler.addToRolePolicy(new iam.PolicyStatement({
            actions: ['eks:DescribeNodegroup'],
            resources: [
                cdk.Stack.of(this).formatArn({
                    resource: 'nodegroup',
                    service: 'eks',
                    resourceName: `${props.cluster.clusterName}/${nodegroupName}/*`
                })
            ]
        }));
        onEventHandler.addToRolePolicy(new iam.PolicyStatement({
            actions: [
                'autoscaling:DeleteTags',
                'autoscaling:CreateOrUpdateTags',
                'autoscaling:DescribeTags',
                'autoscaling:UpdateAutoScalingGroup',
            ],
            resources: ['*']
        }));

        // Creating CustomResource Provider
        const provider = new cr.Provider(this, id + '-ASGModProvider', {
            onEventHandler: onEventHandler,
            logRetention: logs.RetentionDays.TWO_WEEKS
        });

        console.log('===================================')
        console.log('onEventHandler: ' + id + '-onEventHandler')
        console.log('provider: ' + id + '-ASGModProvider');
        console.log('NodegroupASGModifierRes: ' + id + '--NodegroupASGModifier');
        console.log('Fixing NodeGroup: ' + nodegroupName.toString());
        console.log('Nodegroup: ' + props.nodegroup.nodegroupName)
        console.log(props.tags)
        console.log('===================================')
        // Invoke Custom Resource
        const NodegroupASGModifierRes = new cdk.CustomResource(this, id + '-NodegroupASGModifier', {
            serviceToken: provider.serviceToken,
            properties: {
                clusterName: props.cluster.clusterName,
                nodegroupName,
                tags: props.tags
            }
        });
    }
}

The problem is that when I check the generated template in cdk.out of in AWS Console, I can see that all the Custom Resources got the same parameters (tags in my case), while console.log() which I'd put across the code prints the unique values.

Expected Behavior

I would like to create unique custom resources in obj.forEach()

Current Behavior

When I have something inside obj.forEach() which needs to be created by CDK, it's not created properly.

Reproduction Steps

The code sample was provided in the block above

Possible Solution

No response

Additional Information/Context

No response

CDK CLI Version

2.87.0 (build 9fca790)

Framework Version

2.87.0

Node.js Version

v18.16.0

OS

MacOS

Language

Typescript

Language Version

No response

Other information

No response

zentavr commented 1 year ago

Do I need to use something from CloudFormation intrinsic functions?

The idea of adding the tags to autoscaling groups was found here. (Authored by @pahud)

pahud commented 1 year ago

Thank you for bringing up my previous sample. I appreciate that.

Looks like you are trying to create individual NG for the 5 AZs(['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1f']), each NG has multiple custom tags applied. In this case I would prefer a single custom resource that takes care all tags for all NGs. That might be cleaner and easier to maintain.

But I am wondering what makes you to create NG for each az? I assume a NG should be cross-az?

As this issue is not a bug or feature request but general discussion, I am moving this to discussion but I would be happy to continue discuss your use case with you.