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.56k stars 3.87k forks source link

[lambda] Circular dependency when trying to add policy to invoke itself #11020

Closed suspiration closed 2 years ago

suspiration commented 3 years ago

Reproduction Steps

const myLambda = new lambda.Function(this,'myLambda, {
...
    });
const myLambdaInvokePolicyStatement = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [ 'lambda:InvokeFunction' ],
      resources: [ myLambda.functionArn ]
   });
myLambda.addToRolePolicy(myLambdaInvokePolicyStatement);

What did you expect to happen?

CDK to be able to add the policy so my lambda can invoke itself.

What actually happened?

CDK is throwing ValidationError.

Stack failed: Error [ValidationError]: Circular dependency between resources: [...]

Environment

Other


This is :bug: Bug Report

michaelwiles commented 3 years ago

When using cdk a lot of things are done for you. This is because there is a place to put these "leg ups".

Cdk has a quick way for adding permissions to its constructs and the way to add the permission you need is like this:

myLambda.grantInvoke(myLambda)

Do that and you should get a policy linked with the lambda that has the relevant permission.

nija-at commented 3 years ago

Can you explain what you are trying to do here? Are you trying to get the lambda to invoke itself?

suspiration commented 3 years ago

@michaelwiles myLambda.grantInvoke(myLambda) also have the issue of throwing Circular dependency error.

@nija-at yes, I'm trying to grant permission so my lambda can invoke itself. Current workaround is to grant invoke permission to '*', but ideally should only grant permission to what it needs to avoid a potential security hole.

michaelwiles commented 3 years ago

Following code works for me:

        lamda = lambda_.Function(
            self,
            "MyLambda",
            runtime=lambda_.Runtime.PYTHON_3_8,
            code=lambda_.Code.asset(out),
            handler="handler/lambda_handler",
        )
        lamda.grant_invoke(lamda)

It adds the relevant policy to the lambda's policy.

Here is the synth output:

MessageListenerServiceRoleE8660397:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    Metadata:
      aws:cdk:path: rodrigo-iot-listener-michael/MessageListener/ServiceRole/Resource
  MessageListenerServiceRoleDefaultPolicy6678E9E8:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: lambda:InvokeFunction
            Effect: Allow
            Resource:
              Fn::GetAtt:
                - MessageListener0F3FFC02
                - Arn
        Version: "2012-10-17"
      PolicyName: MessageListenerServiceRoleDefaultPolicy6678E9E8
      Roles:
        - Ref: MessageListenerServiceRoleE8660397
    Metadata:
      aws:cdk:path: rodrigo-iot-listener-michael/MessageListener/ServiceRole/DefaultPolicy/Resource
  MessageListener0F3FFC02:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Ref: AssetParametersucket3EA2F342
        S3Key:
          Fn::Join:
            - ""
            - - Fn::Select:
                  - 0
                  - Fn::Split:
                      - "||"
                      - Ref: AssetParametersVersionKey390381AE
              - Fn::Select:
                  - 1
                  - Fn::Split:
                      - "||"
                      - Ref: 3VersionKey390381AE
      Handler: hanlder/lambda_handler
      Role:
        Fn::GetAtt:
          - MessageListenerServiceRoleE8660397
          - Arn
      Runtime: python3.8
suspiration commented 3 years ago

@michaelwiles , cdk synth also works for me, but cdk deploy throws the Circular Dependency error. Does deploy work for you as well?

const myLambda = new lambda.Function(this, 'myLambda', {
  functionName:'myLambda',
  runtime: lambda.Runtime.NODEJS_12_X,
  code: lambda.Code.fromAsset(myLambdaPath),
  handler: 'src/index.handler',
});
myLambda.grantInvoke(myLambda);
michaelwiles commented 3 years ago

okay - that makes sense. So it's not actually a cdk issue but a cloudformation issue...

That makes a lot more sense.

On Tue, 27 Oct 2020 at 09:22, Alan Mak notifications@github.com wrote:

@michaelwiles https://github.com/michaelwiles , cdk synth also works for me, but cdk deploy throws the Circular Dependency error. Does deploy work for you as well?

const myLambda = new lambda.Function(this, 'myLambda', { functionName:'myLambda, runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromAsset(myLambdaPath), handler: 'src/index.handler', }); myLambda.grantInvoke(myLambda);

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/aws/aws-cdk/issues/11020#issuecomment-717043090, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAODX5CS6NTXIZORPPEC7B3SMZYKJANCNFSM4S2DUXZA .

-- see my blog: http://analysis102.blogspot.com http://audiblethoughts.blogspot.com http://outsideofficehours.blogspot.com

nija-at commented 3 years ago

This is because the lambda permission node adds a GetAtt on the lambda function that is yet to be created. So the "Permission" cannot be created until the "Function" is created and the permission cannot be created without the "Function ARN" being available.

You can use the following workaround -

fn.role!.addToPrincipalPolicy(new PolicyStatement({
  actions: [ 'lambda:Invoke' ],
  resources: [ this.formatArn({
    service: 'lambda',
    resource: 'function',
    resourceName: 'myLambda',
  }) ],
}));
chidifrank commented 3 years ago

This is because the lambda permission node adds a GetAtt on the lambda function that is yet to be created. So the "Permission" cannot be created until the "Function" is created and the permission cannot be created without the "Function ARN" being available.

You can use the following workaround -

fn.role!.addToPrincipalPolicy(new PolicyStatement({
  actions: [ 'lambda:Invoke' ],
  resources: [ this.formatArn({
    service: 'lambda',
    resource: 'function',
    resourceName: 'myLambda',
  }) ],
}));

Thanks that helped!

shellscape commented 3 years ago

@nija-at any chance this could get a second look? We're at six months open and the workarounds are only effective in certain situations.

rkeene commented 3 years ago

I'm not sure the workaround posted by @nija-at makes any sense here, since the name of the function isn't known within the CDK Stack, because CloudFormation comes up with it when processing the Stack, there's no value you could supply for the resourceName parameter that will be correct.

More concretely, It generates the following in the CloudFormation IAM policy statement:

{
    "Action": "lambda:Invoke",
    "Effect": "Allow",
    "Resource": {
        "Fn::Join": [
            "",
            [
                "arn:",
                {
                    "Ref": "AWS::Partition"
                },
                ":lambda:",
                {
                    "Ref": "AWS::Region"
                },
                ":",
                {
                    "Ref": "AWS::AccountId"
                },
                ":function/myLambda"
            ]
        ]
    }
}

However, the CDK AWS Lambda Function created by CloudFormation will never be named "myLambda" (since that wouldn't be unique; This wouldn't even be the name of the CloudFormation Resource). The actual name of the Lambda Function won't be known until after the lambda is instantiated, so the value here must be symbolic.

NicoVIII commented 3 years ago

The problem seems to be that with this dependency CloudFormation needs to create the lambda before the ServiceRole and like always the ServiceRole before the lambda like someone stated before.

We worked around that by introducing an additional policy into this circle:

import * as iam from '@aws-cdk/aws-iam';

[..]

const statement = new iam.PolicyStatement({
    actions: ['lambda:InvokeFunction'],
    resources: [ myLambda.functionArn ]
});
const policy = new iam.Policy(this, 'myLambda_policy', {
    statements: [statement]
});
policy.attachToRole(<iam.IRole> myLambda.role);

This works for us. I would guess that CloudFormation can then create it like this: ServiceRole -> Lambda -> Policy -> (Attach Policy to Role)

github-actions[bot] commented 2 years ago

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

Shloosh commented 1 year ago

Frustratingly, this still results in a circular dependency error if you use: role.addManagedPolicy(policy) Nico's solution only works if you instead attach the policy to the role: policy.attachToRole(role);

These should be functionally equivalent, but they're not for some reason.

alexiswl commented 1 year ago

Frustratingly, this still results in a circular dependency error if you use:

@Shloosh Yes and no.

It should be noted that a new policy is created here, not just a nested policy statement.

The initial invocation of the policyStatement is a statement that's created inside the permissions section of the lambda function. If it needs to reference itself, one can see why that might cause issues from within CloudFormation.

For the case of the new policy, it can exists first in its own right (and can be seen on its own from within the IAM console), and then be attached to the lambda function.

If you compare the outputs of the two cloud formation stacks - through cdk synth - with stack one being (PolicyStatement in lambda function) and stack two being (stand alone Policy) this makes a lot more sense

pags commented 10 months ago

The problem seems to be that with this dependency CloudFormation needs to create the lambda before the ServiceRole and like always the ServiceRole before the lambda like someone stated before.

We worked around that by introducing an additional policy into this circle:

import * as iam from '@aws-cdk/aws-iam';

[..]

const statement = new iam.PolicyStatement({
    actions: ['lambda:InvokeFunction'],
    ressources: [ myLambda.functionArn ]
});
const policy = new iam.Policy(this, 'myLambda_policy', {
    statements: [statement]
}
policy.attachToRole(<iam.IRole> myLambda.role);

This works for us. I would guess that CloudFormation can then create it like this: ServiceRole -> Lambda -> Policy -> (Attach Policy to Role)

There's a bad typo here: ressources => resources

NicoVIII commented 10 months ago

Thanks, I fixed the typo in the code which illustrates the concept.

wolfman-numba1 commented 10 months ago

The problem seems to be that with this dependency CloudFormation needs to create the lambda before the ServiceRole and like always the ServiceRole before the lambda like someone stated before.

We worked around that by introducing an additional policy into this circle:

import * as iam from '@aws-cdk/aws-iam';

[..]

const statement = new iam.PolicyStatement({
    actions: ['lambda:InvokeFunction'],
    resources: [ myLambda.functionArn ]
});
const policy = new iam.Policy(this, 'myLambda_policy', {
    statements: [statement]
}
policy.attachToRole(<iam.IRole> myLambda.role);

This works for us. I would guess that CloudFormation can then create it like this: ServiceRole -> Lambda -> Policy -> (Attach Policy to Role)

I think there's a missing bracket at the end of the initialisation of the const variable policy if you want to make an update there as well. 👍