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

CodePipeline policy with LambdaInvokeAction wrongly assumes that ImportValue of a Lambda is the ARN but it is the logical ID #3876

Closed khornberg closed 4 years ago

khornberg commented 5 years ago

:bug: Bug Report

What is the problem?

A CodePipeline with a LambdaInvokeAction wrongly assumes that ImportValue of a Lambda is the ARN but it is the logical ID. The pipeline policy also wrongly assumes the import value.

Reproduction Steps

Clone https://github.com/khornberg/cdk-bugs Install pip packages (assuming one has the npm package already) Run cdk synth --path-metadata false --version-reporting false --app "python3 pipeline-lambda-invoke-action-bug.py"

Verbose Log

https://github.com/khornberg/cdk-bugs/blob/master/output.yaml

Pipeline action Lines 78-82 are wrong and will fail

                  Fn::Select:
                    - 6
                    - Fn::Split:
                        - ":"
                        - Fn::ImportValue: SomeFunction

Policy Lines 135-138

          - Action: lambda:InvokeFunction
            Effect: Allow
            Resource:
              Fn::ImportValue: SomeFunction

The action is created thus

                LambdaInvokeAction(
                    action_name="LambdaInvokeAction",
                    run_order=1,
                    lambda_=Function.from_function_arn(
                        self, "function", core.Fn.import_value("SomeFunction")
                    ),
                )

Environment

khornberg commented 5 years ago

If I do something like Function.from_function_arn(self, 'x', core.Fn.get_att("SomeFunction", "Arn")) I get a synth error like jsii.errors.JSIIError: Expected Scalar, got {"$jsii.byref":"@aws-cdk/core.Intrinsic@10015"}

However, core.Fn.import_value does not result in a synth error

rhboyd commented 5 years ago

Where are you exporting the lambda’s logical ID that this stack is importing?

rhboyd commented 5 years ago

Okay. There’s a few things here. Fn.get_att(“someFunction”, “Arn”) would throw an error if you didn’t have a function with the logical ID of “someFunction” in the same stack (which is highly unlikely in a CDK stack).

Fn.Import_value(“SomeFunction”) will look for a CFN Export in the same account/region.

My assumption is that you are exporting a Lambda’s logical ID in a different stack when you should probably be exporting the Lambda’s Arn instead.

Happy to walk you though it if you can share your CDK or YAML

khornberg commented 5 years ago

You are correct that the import value is in a different stack.

Outputs:
  ValidateVersionBump:
    Value:
      Ref: ValidateVersionBumpLambdaFunction
    Description: Name of the lambda function used in CodePipeline to disable ServiceCatalog updates
    Export:
      Name: ValidateVersionBumpLambda

I have a working CF template that defines the CodePipeline. I am trying recreate it using CDK.

This actions works and it also part of a separate stack.

            Actions:
              - Name: Validate-Version-Bump
                RunOrder: 1
                ActionTypeId:
                  Category: Invoke
                  Owner: AWS
                  Provider: Lambda
                  Version: '1'
                InputArtifacts:
                  - Name: Artifact
                Configuration:
                  FunctionName:
                    Fn::ImportValue: ValidateVersionBumpLambda

I have monkey patched the CDK output to fix the perceived errors above and the stack works. I guess I am expecting a fairly similar mapping of concepts between CloudFormation and CDK. If I am able to import the logical id of a function and it works in CF, then I expect to be able to do the same in CDK.

Given what you have said, is there a way to get the arn from a logical id import in CDK?

rhboyd commented 5 years ago

I’m not sure that would work. If your other stack was pure cloudformation (instead of CDK) it would still just import the Lambda’s name and not it’s ARN. I would change your export to export the ARN instead of the Name.

rhboyd commented 5 years ago
Outputs:
  ValidateVersionBump:
    Value:
      !GetAtt ValidateVersionBumpLambdaFunction.Arn
    Description: Name of the lambda function used in CodePipeline to disable ServiceCatalog updates
    Export:
      Name: ValidateVersionBumpLambda
rhboyd commented 5 years ago

You could also manually reconstruct the Arn from the Function’s Name (the current export) and the environment variables (AccountId, Region, etc...)

khornberg commented 5 years ago

I’m not sure that would work.

I'm not sure either but it does ¯_(ツ)_/¯

Perhaps because the Configuration is a json field and CloudFormation knows how to sort it out

cf. https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements

rhboyd commented 5 years ago

Can you share your CDK code? If the Pipeline accepts a Function Name in regular cloudformation, the same thing should work in CDK. In your original issue you used from_function_arn() but then you imported a value that represented the function name, not the arn. You want either from_function_attributes() or one of the lookup methods.

rhboyd commented 5 years ago

Did some more digging. It looks like the Lambda module should have either a Lambda.from_name() method added or from_attributes() should accept a Lambda Function name as an attribute.

rhboyd commented 5 years ago

This should work for you. Unfortunately, until the Lambda Module adds the needed functions, you will have to reconstruct the Arn from the name, so that the Pipeline Construct can extract it.

from aws_cdk import core
from aws_cdk import core
from aws_cdk.aws_codepipeline import Artifact
from aws_cdk.aws_codepipeline import Pipeline
from aws_cdk.aws_codepipeline import StageProps
from aws_cdk.aws_codepipeline_actions import GitHubSourceAction
from aws_cdk.aws_codepipeline_actions import LambdaInvokeAction
from aws_cdk.aws_iam import Role
from aws_cdk.aws_lambda import Function
from aws_cdk.aws_s3 import Bucket

class FunctionImportStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        bucket = Bucket.from_bucket_name(
            self, "artifacts", core.Fn.import_value("CodeArtifactsBucket")
        )
        pipeline_role = Role.from_role_arn(
            self, "pipeline", core.Fn.import_value("CodePipelineRole")
        )

        lambda_arn = "arn:{partition}:lambda:{region}:{account_id}:function:{name}".format(
            partition=core.Aws.PARTITION,
            region=core.Aws.REGION,
            account_id=core.Aws.ACCOUNT_ID,
            name=core.Fn.import_value("SomeFunction")
        )
        pipeline = Pipeline(
            self,
            "Pipeline",
            artifact_bucket=bucket,
            role=pipeline_role,
            stages=[
                StageProps(
                    stage_name="Source",
                    actions=[
                        GitHubSourceAction(
                            action_name="Source",
                            run_order=1,
                            oauth_token=core.SecretValue("something"),
                            output=Artifact(artifact_name="SourceArtifact"),
                            owner="me",
                            repo="repo",
                            branch="master",
                        )
                    ],
                )
            ],
        )
        pipeline.add_stage(
            stage_name="Fails",
            actions=[
                LambdaInvokeAction(
                    action_name="LambdaInvokeAction",
                    run_order=1,
                    lambda_= Function.from_function_arn(self, "function", lambda_arn)
                )
            ]
        )
rhboyd commented 5 years ago

Actually, this would even work too, since it appears to ignore the rest of the arn anyways.

pipeline.add_stage(
            stage_name="Fails",
            actions=[
                LambdaInvokeAction(
                    action_name="LambdaInvokeAction",
                    run_order=1,
                    lambda_= Function.from_function_arn(self, "function", "arn:::::function:{}".format(core.Fn.import_value("SomeFunction")))
                )
            ]
        )
khornberg commented 5 years ago

Thanks @rhboyd

lambda_arn = "arn:{partition}:lambda:{region}:{account_id}:function:{name}".format(
            partition=core.Aws.PARTITION,
            region=core.Aws.REGION,
            account_id=core.Aws.ACCOUNT_ID,
            name=core.Fn.import_value("SomeFunction")
        )

worked but "arn:::::function:{}".format(core.Fn.import_value("SomeFunction")) did not because that ends up as invalid in the policy document created by CDK.

skinny85 commented 4 years ago

@khornberg is there anything else we can help you with this issue?

khornberg commented 4 years ago

No I've since moved on