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.57k stars 3.88k forks source link

api_gateway: Circular dependency between KMS key and ServiceRole for gateway AuthHandler #22047

Open kornicameister opened 2 years ago

kornicameister commented 2 years ago

Describe the bug

Why

I wanted to have a single KMS key used for entire deployment stage (no requirements to have it other way around) that is used in multiple other stacks defining different functionalities.

Approach 1

Why I think it failed with permissions but key was

Because keys imported via https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_kms/Key.html#aws_cdk.aws_kms.Key.from_key_arn do no contain a reference to key originally created in stack A hence calls like https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_kms/Key.html#aws_cdk.aws_kms.Key.add_to_resource_policy do not really work

Approach 2

Approach 3

Note: that fails in my application, but works well in example. 

It's essentially an approach 2 but I have created an alias for the imported key.

Traceback (most recent call last):
  File "/Users/kornicameister/dev-ay/app/app.py", line 8, in <module>
    PipelineStack(
  File "/Users/kornicameister/.pyenv/versions/app/lib/python3.10/site-packages/jsii/_runtime.py", line 86, in __call__
    inst = super().__call__(*args, **kwargs)
  File "/Users/kornicameister/dev-ay/app/ay/pipeline.py", line 236, in __init__
    pipeline.add_stage(
  File "/Users/kornicameister/.pyenv/versions/app/lib/python3.10/site-packages/aws_cdk/pipelines/__init__.py", line 3534, in add_stage
    return typing.cast("StageDeployment", jsii.invoke(self, "addStage", [stage, options]))
  File "/Users/kornicameister/.pyenv/versions/app/lib/python3.10/site-packages/jsii/_kernel/__init__.py", line 148, in wrapped
    return _recursize_dereference(kernel, fn(kernel, *args, **kwargs))
  File "/Users/kornicameister/.pyenv/versions/app/lib/python3.10/site-packages/jsii/_kernel/__init__.py", line 386, in invoke
    response = self.provider.invoke(
  File "/Users/kornicameister/.pyenv/versions/app/lib/python3.10/site-packages/jsii/_kernel/providers/process.py", line 362, in invoke
    return self._process.send(request, InvokeResponse)
  File "/Users/kornicameister/.pyenv/versions/app/lib/python3.10/site-packages/jsii/_kernel/providers/process.py", line 329, in send
    raise JSIIError(resp.error) from JavaScriptError(resp.stack)
jsii.errors.JSIIError: 'AGW' depends on 'KMS' (dependency added using stack.addDependency()). Adding this dependency (KMS -> AGW/API/Authorizer/Handler/ServiceRole/Resource.Arn) would create a cyclic reference.

How I could make it work and why I haven't

There's a workaround for that problem. I actually need to have:

But it is a bit awkward just by looking at it and I am pretty sure I would have created unmaintainable code just because I would create two dependency points:

Expected Behavior

Not quite sure if there's any. I mean I would love the code to work but I cannot get stop thinking that either:

However, I wonder if any calls to addToResourcePolicies executed over imported keys Key.fromLookup, Key.fromKeyArn or Alias.fromAliasName should fail. I mean it clearly haven't worked for me and it makes to me that it shouldn't. After all key policy is created the moment key is defined and each of mentioned methods do assume key already exists (in different stack, in different place, manually created or whatever else). If there's a point to this bug: it might be it.

Current Behavior

Either:

Reproduction Steps

#!/usr/bin/env python3

import aws_cdk as cdk
from aws_cdk import (
    aws_kms as kms,
    aws_ssm as ssm,
    aws_secretsmanager as secrets,
    aws_lambda as fn,
    aws_apigateway as agw,
)

app = cdk.App()
arn_ssm_alias = '/alias/for/arn'

master_key_stack = cdk.Stack(app, 'MasterKeyStack')
master_encryption_key = kms.Key(master_key_stack, 'MasterKey')
ssm.StringParameter(
    master_key_stack,
    'ARNAlias',
    type=ssm.ParameterType.STRING,
    data_type=ssm.ParameterDataType.TEXT,
    tier=ssm.ParameterTier.STANDARD,
    parameter_name=arn_ssm_alias,
    string_value=master_encryption_key.key_arn,
)

def approach_1():
    stack = cdk.Stack(app, 'StackThatUsesKMS')
    key = kms.Key.from_key_arn(
        stack,
        'ImportedMasterKey',
        ssm.StringParameter.from_string_parameter_attributes(
            stack,
            'ImportedMasterKeyArn',
            parameter_name=arn_ssm_alias,
        ).string_value,
    )

    return stack, key.add_alias('whatever')

def approach_2():
    stack = cdk.Stack(app, 'StackThatUsesKMS')
    return stack, master_encryption_key

def approach_3():
    stack = cdk.Stack(app, 'StackThatUsesKMS')
    return stack, master_encryption_key.add_alias('whatever')

def api(stack: cdk.Stack, encryption_key: kms.IKey):
    secret = secrets.Secret(
        stack,
        'SecretKey',
        encryption_key=encryption_key,
        generate_secret_string=secrets.SecretStringGenerator(
            exclude_punctuation=True,
            password_length=64,
        ),
    )
    auth_fn = fn.Function(
        stack,
        'Authorizer',
        handler='index.handler',
        code=fn.Code.from_inline('def handler(event, ctx): ...'),
        runtime=fn.Runtime.PYTHON_3_9,
    )
    secret.grant_read(auth_fn)
    api = agw.RestApi(
        stack,
        'API',
        deploy=True,
        endpoint_types=[agw.EndpointType.REGIONAL],
        default_method_options=agw.MethodOptions(
            authorizer=agw.RequestAuthorizer(
                stack,
                'RequestAuthorizer',
                handler=auth_fn,
                identity_sources=[
                    agw.IdentitySource.header('x-key'),
                ],
            ),
        ),
    )
    api.root.add_resource('test').add_method('GET')

# api(*approach_1())
# api(*approach_2())
# api(*approach_3())

app.synth()

Possible Solution

N/A

Additional Information/Context

No response

CDK CLI Version

2.41.0

Framework Version

?

Node.js Version

14.17.5

OS

MAC

Language

Python

Language Version

3.10.5

Other information

peterwoodworth commented 1 year ago

You're running into a circular dependency here for a similar reason as this issue and this issue. Stack B may run code which will cause new policies to be added in Stack A which in turn attempt to reference a resource in Stack B.

These can be tricky to workaround depending on the specific case. If there are no simple escape hatches, then you may be able to introduce a third stack, and/or implement a custom resource which creates and adds the necessary policy once you have the information you need to create the policy.