Search code examples
python-3.xaws-lambdaaws-cdkaws-cdk-custom-resource

How to trigger off a custom resource lambda in a different account


I have an app stack that fires off a custom resource lambda on every deploy. Currently the custom resource lambda is in the same app stack and both are running in the same account. Both are built in Python 3.8

This is how its structured now where everything is in one stack running in the same account.

cust_res_lambda = _lambda.Function(
    self, 'crLambda',
    runtime=_lambda.Runtime.PYTHON_3_8,
    code=_lambda.Code.from_asset('..'),
    handler='lambda.lambda_handler',
    function_name='customResourceLambda',
    layers=[..,..], 
)

cust_res_lambda.add_to_role_policy(_iam.PolicyStatement(
    effect=_iam.Effect.ALLOW,
    actions=[
        'dynamodb:Query',
        'dynamodb:GetItem',
        'dynamodb:GetRecords',
        'dynamodb:PutItem',
        'dynamodb:UpdateItem',
        'dynamodb:BatchGetItem',
    ],
    resources=['arn:aws:dynamodb:XregionX:Xaccount-numX:table/stances']
))

res_provider = cr.Provider(
    self,'crProvider',
    on_event_handler= cust_res_lambda
)


now=datetime.now()
time = now.strftime("%H:%M:%S") # forces the execution of custom resource at every stack run.
CustomResource(self, 'cust_res',service_token= res_provider.service_token,properties={"prop1":"aa","prop2":"bb","res_id":time })

Looking for some documentation guidance around how to get the custom resource lambda deployed in a central account A, while the app stack gets deployed in accounts B,C,D? I will shift the lambda into its own stack that is only deployed in account A. The ask is due to the lambda needing to update a DynamoDB in the central account A and accounts B, C, D where the app stack will be deployed are not allowed to touch any resources in account A other than this custom resource lambda.

EDIT: There are some security restrictions why it may not be allowed to have the lambda run in accounts B, C, D in this particular case. Also, we don't know in advance all the other target accounts that this stack will be deployed in. The lambda needs to run from central account A.


Solution

  • If the only concern is the ability to modify a DynamoDB table in another account, you can keep the current setup you have with an instance of the custom resource in each account, but grant permissions to allow the lambda to assume a role in Account 'A' using cross-account access.

    So, without modifying your current setup, you can create a new stack in account A that grants assume-role permissions to all of the roles used by the lambda in the other accounts.

    It might look something like this:

    # ...
    class CentralStack(Stack):
        # this stack gets created once in the central account
        # has the dynamodb table other accounts need to modify
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.the_table = Table(...)
            # ...
    
    
    class AppStack(Stack):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.cust_lambda_role = iam.Role(...)
            # give permission to the lambda to assume a role in account A
            self.cust_lambda_role.add_to_policy(
                iam.PolicyStatement(
                    actions=['sts:AssumeRole'],
                    effect=iam.Effect.ALLOW,
                    resources=['arn:aws:iam::A:role/my-central-xacct-role'],
                )
            )
            cust_res_lambda = _lambda.Function(..., role=self.cust_lambda_role)
            # ...
    
    # instantiate the central account main stack
    central_stack = CentralStack(app, env=cdk.Environment(account='A', region=MY_REGION))
    
    app_stacks: List[AppStack] = []
    
    # create multiple application stacks in different accounts
    for account in ['B', 'C', 'D']:
        app_stack = AppStack(app, env=cdk.Environment(account=account, region=MY_REGION))
        app_stacks.append(app_stack)
    
    
    class CrossAccountRoleStack(Stack):
        def __init__(self, *args, central_stack, app_stacks, **kwargs):
            super().__init__(*args, **kwargs)
            app_roles = [stack.cust_lambda_role for stack in app_stacks]
            app_role_principals = iam.CompositePrincipal(*app_roles)
            # create a role that can be assumed by lambdas in the app stack/accounts
            cust_resource_x_acct_role = iam.Role(
                self,
                'x-acct-role',
                assumed_by=app_role_principals,
                role_name='my-central-xacct-role',
            )
            # grant this role access to the table
            central_stack.the_table.grant_full_access(cust_resource_x_acct_role)
    
    # create the cross-account role in the central account
    # that allows each of the app stacks to assume the role in account A
    xacct_role_stack = CrossAccountRoleStack(
        app,
        central_stack=central_stack,
        app_stacks=app_stacks,
        env=cdk.Environment(account='A', region=MY_REGION),
    )
    
    # ...
    app.synth()
    

    Then your actual lambda function code that runs in each application account (e.g., accounts B,C,D) can call the sts assume-role operation to obtain credentials for the role in the central account A and have access to the dynamoDB table in account A.