aws / aws-cdk-rfcs

RFCs for the AWS CDK
Apache License 2.0
534 stars 83 forks source link

Context provider for cross-account CFN stack outputs #226

Closed Cloudrage closed 3 years ago

Cloudrage commented 4 years ago

Hi there,

It'll be a must have to be able to retrieve Stacks outputs from Cloudformation cross-accounts natively with CDK. Not using Exports, because they have some limitations :

Use Case

Try to imagine :

You can't simply do that with CDK.

This issue is the same with multiple resources created on another Account, like R53, IAM, EndpointServices...

Proposed Solution

Create a native "resolver" and an assume role feature like the "cdk-assume-role-plugin" (ok that last point is another feature request :p) !

I try to merge from Sceptre (Troposphere) to CDK but I have to admit that actually CDK can't cover & offer a full alternative.

For example, to cover that we use a resolver doing a simple describe (example) :

   def resolve(self):
        try:
            response = self.connection_manager.call(
                service="cloudformation",
                command="describe_stacks",
                kwargs={"StackName": self.stack_name}
            )
        except ClientError as e:
            if "does not exist" in e.response["Error"]["Message"]:
                raise Exception(
                    "Stack with name {} does not exist".format(
                        self.stack_name
                    )
                )
            else:
                raise e
        else:
            outputs = response["Stacks"][0]["Outputs"]
        formatted_outputs = dict(
            (output["OutputKey"], output["OutputValue"])
            for output in outputs
        )
        try:
            return formatted_outputs[self.output_key]
        except KeyError:
            raise Exception(
                "The stack '{}' does not have an output named '{}'".format(
                    self.stack_name,
                    self.output_key
                )
            )

So, in the code we just have to put the Arn of the Role to assume on the Account A, the Name of the Stack & the Cfn Output; like that :

{{AppRoleArn}}:::{{AppPath}}/sns/topic-AutoScaling:::Arn

With CDK : The Arn can for example be in cdk.json as variable and retrieved with a "tryGetContext". Name of the Stack ? Easy with "stackName", like the "env" or "description". The Output ? Easy again, created with the Stack with "CfnOutput".

Other

I've created a Custom solution to be able to do that with CDK :

Ok, but now, my code can't work from scratch if I want to build all my PRD environment for example; yes, I have of course errors because my output file is not created yet... So what ? I have to hardcode the Arn of the Key to create my ASG Grant ? Don't even think about it

When a CMK is needed with some resources I've created, I've set the Alias Arn instead of the Key; that way, I can easily name it and set it in my code. But CreateGrant need the Key Arn & it's not possible to bypass that.

Regards, MG


This is a :rocket: Feature Request

Cloudrage commented 4 years ago

Any news about that ?

We are Stuck on our App migration without that feature and have to use Workarounds like a dirty cdk [...] --outputs-file & Cloudformation Exports...

I'm very surprised that an important feature like that is not native with CDK.

Cloudrage commented 3 years ago

Hi @eladb ! Can you gave us an update about that Feature Request ? Or at least a viable workaround ?

eladb commented 3 years ago

The best solution I can offer at this point is to use well-known physical names for resources in different accounts. If you assign the value PhysicalName.GENERATE_IF_NEEDED to a physical name of a resource and reference the resource across environments (account/regions), then a physical name will be automatically generated during synthesis.

eladb commented 3 years ago

Copying @skinny85

Cloudrage commented 3 years ago

Hi @eladb and thanks for your reply.

Not sure to understand how PhysicalName.GENERATE_IF_NEEDED works. For, example, we have a Stack A with an ALB creation in Account A :

const ApplicationLoadBalancerPrivate = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancerPrivate', {
      loadBalancerName: cdk.PhysicalName.GENERATE_IF_NEEDED,
      [...]

Now, I want to create a R53 Record (targeting ApplicationLoadBalancerPrivate.loadBalancerDnsName) in Stack B but in another Account B. How to do that (import/export ALB properties, like DnsName) ? And another thing, it's not possible to set the PhysicalName of our own ?

eladb commented 3 years ago

@Cloudrage you should be able to simply reference loadBalancerDnsName in the consuming stack and if everything works as expected, the ALB will get a physical name, a dependency will be created between the stacks and the DNS name will be hard-coded in the consuming side.

Something like this (sketch):

const ENV1 = { account: '11111', region: 'us-east-1' };
const ENV2 = { account: '2222', region: 'eu-west-2' };

class ProducerStack extends Stack {
  public readonly alb: ApplicationLoadBalancer;

  constructor(scope: Construct, id: string) {
    super(scope, id, { env: ENV1 });

    this.alb = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancerPrivate', {
      loadBalancerName: cdk.PhysicalName.GENERATE_IF_NEEDED,
    });
  }
}

interface ConsumerStackProps {
  readonly alb: ApplicationLoadBalancer;
}

class ConsumerStack extends Stack {
  constructor(scope: Construct, id: string, props: ConsumerStackProps) {
    super(scope, id, { env: ENV2 });

    new route53.ARecord(this, 'AliasRecord', {
      zone,
      target: route53.RecordTarget.fromAlias(new alias.LoadBalancerTarget(props.alb)),
    });
  }
}

const producer = new ProducerStack(app, 'producer');
const consumer = new ConsumerStack(app, 'consumer', { 
  alb: producer.alb 
});

Let me know if this helps/works :-)

Cloudrage commented 3 years ago

Understood @eladb.

But in your sketch : Cannot find name 'ApplicationLoadBalancer'. Public property 'alb' of exported class has or is using private name 'ApplicationLoadBalancer'.

And is it possible to set PhysicalName and not use generated one ?

I think it'll be a good alternative to be able to get Cross-Account Cfn Outputs with CDK (or SSM parameters maybe ?)

eladb commented 3 years ago

It's supposed to be elbv2.ApplicationLoadBalancer.

Yes you could just use any value for physical name. The nice thing about auto_generate is that if this resource is not referenced across environments, it will not use an explicit name. It will also generate a name that is unique for your app.

But otherwise feel free to just assign any name.

Cloudrage commented 3 years ago

How to set up a physical name with class PhysicalName ? I just can see a static GENERATE_IF_NEEDED.

   * The value passed in by users to the physical name prop of the resource.
   *
   * - `undefined` implies that a physical name will be allocated by
   *   CloudFormation during deployment.
   * - a concrete value implies a specific physical name
   * - `PhysicalName.GENERATE_IF_NEEDED` is a marker that indicates that a physical will only be generated
   *   by the CDK if it is needed for cross-environment references. Otherwise, it will be allocated by CloudFormation.

It means that if I set up loadBalancerName: 'toto', I'm in the case "a concrete value implies a specific physical name" ?

I've tested that workaround (with & without _PhysicalName.GENERATE_IFNEEDED) but it seems that I've made something wrong : Error: Stack "B" cannot consume a cross reference from stack "A". Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack

skinny85 commented 3 years ago

@Cloudrage you're right. It turns out there's some logic missing from BaseLoadBalancer that is required for making these references work.

Do you mind creating us a bug for it in the main CDK repo? Thanks!

Cloudrage commented 3 years ago

Made it as you can see @skinny85 , thanks to you. I think it's not an isolated pb, I've faced the same pb between VpcEndpointService & InterfaceVpcEndpoint.

To go back to the intial request, I think the best way is to provide a native solution to retrieve these outputs/references between Cross Account Stacks directly. Can't believe I'm the only one trying to get resources created on other Accounts (CMK/IAM/PrivateLinks/R53/TGW...). Using --outputs-file as a workaround to get Cfn outputs locally to be able to create other resources on other accounts from that file is not thinkable...

skinny85 commented 3 years ago

Thanks @Cloudrage ! We will get those fixed.

nbaillie commented 3 years ago

I also have a few use case.. one right now i am looking at when trying to delegate dns to a hostedZone in another account. So the Prod account hostedZone delegates to Dev account hostedZone. Finding it hard to pass the values needed for the NS records and zoneId. Right now having to use ShellScriptAction with stackOutput then aws cli to configure the records. Would be better if i could use CDK to add the records in TypeScript.

Cloudrage commented 3 years ago

I also have a few use case.. one right now i am looking at when trying to delegate dns to a hostedZone in another account. So the Prod account hostedZone delegates to Dev account hostedZone. Finding it hard to pass the values needed for the NS records and zoneId. Right now having to use ShellScriptAction with stackOutput then aws cli to configure the records. Would be better if i could use CDK to add the records in TypeScript.

Same for me, not found an easy and beautifful way to do that; again, the only workaround "viable" I've found is to use output-file.

eladb commented 3 years ago

Duplicate #161