aws-cdk icon indicating copy to clipboard operation
aws-cdk copied to clipboard

Pipeline: Value of CfnOutput is different if used in a PolicyStatement vs a property of `envFromCfnOutputs`

Open jhummel opened this issue 3 years ago • 13 comments

Describe the bug

I'm creating a bucket in a Pipeline stage and saving the bucketName property as a CfnOutput. I use that output in a stage poststep in a shell command (using envFromCfnOutputs) and to generate a new PolicyStatement. The value of the name in the policy statement is different than the value of the ENV variable in the shell.

Expected Behavior

The values should be the same no matter where the output is used.

Current Behavior

The CfnOutput displays different values depending on where used.

Reproduction Steps

Create a new bucket:

export class MyStack extends Stack {
    public readonly bucketName: CfnOutput

    constructor(scope: Construct, id: string, props: StackProps) {
        const bucket = new Bucket(this, 'myBucket', {
            removalPolicy: RemovalPolicy.DESTROY,
            autoDeleteObjects: true,
        });

        this.bucketName = new CfnOutput(this, 'bucketName', {
            value: bucket.bucketName
        });
    } 
}

'Hoist' the value up the pipeline stage:

export class MyStage extends Stage {
    public readonly bucketName: CfnOutput;

    constructor(scope: Construct, id: string, props: StageProps) {
        super(scope, id, props);         
        const stack = new MyStack(this, 'myStack', props);
        this.bucketName = stack.bucketName
    }
}

Create the pipeline and use the stage, add a poststep

export class MyPipeline extends Stack {
    constructor(scope: Construct, id: string, props: StackProps) {
        super(scope, id, props);

        const source = CodePipelineSource.connection(...);
        const stage = new MyStage(this, 'stage');
        const synth = new CodeBuildStep('Synth', {
            input: source,
            commands: ['npm ci', 'npm run build', 'npm run cdk synth']
        });

        // generate a post step that builds my frontend and moves to the bucket
        // I have to do this later, because I use some other values such as the URL
       // to an API gateway in my React Code, so I can't use bucketDeploy in the stage

        const frontend = this.getFrontendStep(source, stage.bucketName);

        const pipeline = new CodePipeline(this, 'pipeline', {
            pipelineName: 'myPipeline',
            synth
        });

        const pipelineStage = pipeline.addStage(stage);
        pipelineStage.addPost(frontend);
    }

    getFrontendStep(source: CodePipelineSource, bucketName: CfnOutput) {
        // notice the echo shell command this value is CORRECT
        // the CfnOutput value in the policy statement is INCORRECT

        return new CodeBuildStep('frontend', {
            input: source,
            commands: [
                 'echo $AWS_FRONTEND_BUCKET_NAME',
                 'cd frontend',
                 'npm ci',
                 'npm run build',
                 'aws s3 sync ./dist/ s3://$AWS_FRONTEND_BUCKET_NAME/ --delete'
            ],
            rolePolicyStatements: [
                new PolicyStatement({
                    effect: Effect.ALLOW,
                    actions: ["S3:*"],
                    resources: [
                        `arn:aws:s3:::${bucketName.value}`,
                        `arn:aws:s3:::${bucketName.value}/*`
                   ]
               })
           ],
           primaryOutputDirectory: 'frontend/dist',
           envFromCfnOutputs: {
               AWS_FRONTEND_BUCKET_NAME: bucketName
           }
       })
    }
}

Possible Solution

No response

Additional Information/Context

No response

CDK CLI Version

2.37.0

Framework Version

No response

Node.js Version

16.16.0

OS

Mac

Language

Typescript

Language Version

Typescript 4.7.4

Other information

No response

jhummel avatar Aug 17 '22 16:08 jhummel

I'm seeing this in my CloudFormation template for my action environment variable:

{\"name\":\"TEST_ENV\",\"type\":\"PLAINTEXT\",\"value\":\"#{TestStackTestStageBucketStack350312A4.MyOutput}\"}

When you try to access this environment variable does it show something similar?

peterwoodworth avatar Aug 18 '22 23:08 peterwoodworth

This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

github-actions[bot] avatar Aug 21 '22 00:08 github-actions[bot]

@peterwoodworth Yes. In my Pipeline template I see:

EnvironmentVariables": "[{"name":"AWS_API_BASE_URL","type":"PLAINTEXT","value":"#{DemoEnvTestPipelineStackInfrastructuredemoenv99E6F651.apiurl}"},{"name":"AWS_FRONTEND_BUCKET_NAME","type":"PLAINTEXT","value":"#{DemoEnvTestPipelineStackInfrastructuredemoenv99E6F651.bucketname}"}]"

The ENV variable is the correct one however, It's when I use it in the step outside of the ENV variable it is incorrect.

jhummel avatar Aug 22 '22 21:08 jhummel

Can you describe exactly where and what is incorrect? Is it the usage in policystatement that is incorrect?

peterwoodworth avatar Aug 22 '22 21:08 peterwoodworth

Exactly, the value in the ENV variable is correct, but the value in the policystatement is incorrect

// The echo shell commands array value is CORRECT
// the CfnOutput (bucket.value) in the policy statement is INCORRECT

        new CodeBuildStep('frontend', {
            input: source,
            commands: [
                 'echo $AWS_FRONTEND_BUCKET_NAME',
                 'cd frontend',
                 'npm ci',
                 'npm run build',
                 'aws s3 sync ./dist/ s3://$AWS_FRONTEND_BUCKET_NAME/ --delete'
            ],
            rolePolicyStatements: [
                new PolicyStatement({
                    effect: Effect.ALLOW,
                    actions: ["S3:*"],
                    resources: [
                        `arn:aws:s3:::${bucketName.value}`,
                        `arn:aws:s3:::${bucketName.value}/*`
                   ]
               })
           ],
           primaryOutputDirectory: 'frontend/dist',
           envFromCfnOutputs: {
               AWS_FRONTEND_BUCKET_NAME: bucketName
           }
       })

jhummel avatar Aug 23 '22 13:08 jhummel

Little more context, here's what I see in the CloudFormation template for that policyStatement:

    {
       "Action": "S3:*",
       "Effect": "Allow",
       "Resource": [
        "arn:aws:s3:::infrastructure-demo-etfrontendbucketa9f7bddb43ed5f2da912",
        "arn:aws:s3:::infrastructure-demo-etfrontendbucketa9f7bddb43ed5f2da912/*"
       ]
    }

Not sure where that bucket name is coming from, I actually can't seem to find a bucket with that name in s3. The actual name of the bucket, and the name that is set as an ENV variable in my shell commands is:

    arn:aws:s3:::infrastructure-demo-dudademoenvbucketfronten-1mxkm3csw7hud

jhummel avatar Aug 23 '22 16:08 jhummel

The CloudFormation way to reference a value of another CfnOutput is to use ImportValue

https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.Fn.html#static-importwbrvaluesharedvaluetoimport

If you use this where you are trying to pass the value into the PolicyStatement does it provide an accurate value?

peterwoodworth avatar Aug 27 '22 01:08 peterwoodworth

This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

github-actions[bot] avatar Aug 29 '22 04:08 github-actions[bot]

I've actually tried this in the past. I always end up with an error 'DemoEnvTestPipelineStack No export named <EXPORT_NAME> found' Here's how the code was updated:

The Stack:

export class MyStack extends Stack {
    public readonly bucketName: CfnOutput

    constructor(scope: Construct, id: string, props: StackProps) {
        const bucket = new Bucket(this, 'myBucket', {
            removalPolicy: RemovalPolicy.DESTROY,
            autoDeleteObjects: true,
        });

        this.bucketName = new CfnOutput(this, 'bucketName', {
            value: bucket.bucketName,
            exportName: 'frontend-bucket'
        });
    } 
}

The Relevant update to the pipeline:

new CodeBuildStep('frontend', {
            input: source,
            commands: [
                 'echo $AWS_FRONTEND_BUCKET_NAME',
                 'cd frontend',
                 'npm ci',
                 'npm run build',
                 'aws s3 sync ./dist/ s3://$AWS_FRONTEND_BUCKET_NAME/ --delete'
            ],
            rolePolicyStatements: [
                new PolicyStatement({
                    effect: Effect.ALLOW,
                    actions: ["S3:*"],
                    resources: [
                        `arn:aws:s3:::${Fn.importValue('frontend-bucket')}`,
                        `arn:aws:s3:::${Fn.importValue('frontend-bucket')}/*`
                   ]
               })
           ],
           primaryOutputDirectory: 'frontend/dist',
           envFromCfnOutputs: {
               AWS_FRONTEND_BUCKET_NAME: bucketName
           }
       })

In this case, I'd get the error: 'DemoEnvTestPipelineStack No export named frontend-bucket found'. Do I have to make any changes to my stage? Somehow 'lift' that export name up to the stage so the pipeline can see it?

jhummel avatar Aug 29 '22 16:08 jhummel

Any ideas here? Doing more research, I think maybe the 'no export named' problem is because the pipeline creates the bucket in another account. Is there some way to get the importValue working for a cross-account resource?

jhummel avatar Sep 02 '22 18:09 jhummel

I am having this issue too. In my case I had to enable cdk.PhysicalName.GENERATE_IF_NEEDED.

(...)
    const siteBucketAccessRole = new iam.Role(this, "SiteBucketAccessRole", {
      roleName: cdk.PhysicalName.GENERATE_IF_NEEDED,
(...)

Then the problem is the same. At rolePolicyStatements the ARN of the function gets messed up but is fine at envFromCfnOutputs.

tinti avatar Feb 20 '24 22:02 tinti

I believe the unit test is not getting the name change.

https://github.com/aws/aws-cdk/blob/c17879dd8dab526695e69c6faf8345292634ba24/packages/aws-cdk-lib/core/test/cross-environment-token.test.ts#L94-L127

tinti avatar Feb 20 '24 22:02 tinti

I believe I understood why it works on envFromCfnOutputs but not on rolePolicyStatements. By looking at the template.yaml one can see that the values from envFromCfnOutputs are being loaded in pipeline stages by outputs from other stages (like at "runtime"). rolePolicyStatements needs it at "compile" time.

tinti avatar Feb 20 '24 22:02 tinti