Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CloudFormation Cross-Region Reference

When you are running multiple CloudFormation stacks within the same region, you are able to share references across stacks using CloudFormation Outputs

However, outputs cannot be used for cross region references as that documentation highlights.

You can't create cross-stack references across regions. You can use the intrinsic function Fn::ImportValue to import only values that have been exported within the same region.

How do you reference values across regions in CloudFormation?

For an example to follow, I have a Route 53 hosted zone deployed in us-east-1. However, I have a backend in us-west-2 that I want to create a DNS-validated ACM certificate which requires a reference to the hosted zone in order to be able to create the appropriate CNAME for prove ownership.

How would I go about referencing that hosted zone id created in us-east-1 from within us-west-2?

like image 272
Reed Hermes Avatar asked Jan 16 '20 17:01

Reed Hermes


2 Answers

The easiest way I have found of doing this is writing the reference you want to share (i.e. your hosted zone id in this case) to the Systems Manager Parameter Store and then referencing that value in your "child" stack in the separate region using a custom resource.

Fortunately, this is incredibly easy if your templates are created using Cloud Development Kit (CDK).

For the custom resource to read from SSM, you can use something like this:

// ssm-parameter-reader.ts

import { Construct } from '@aws-cdk/core';
import { AwsCustomResource, AwsSdkCall } from '@aws-cdk/custom-resources';

interface SSMParameterReaderProps {
  parameterName: string;
  region: string;
}

export class SSMParameterReader extends AwsCustomResource {
  constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
    const { parameterName, region } = props;

    const ssmAwsSdkCall: AwsSdkCall = {
      service: 'SSM',
      action: 'getParameter',
      parameters: {
        Name: parameterName
      },
      region,
      physicalResourceId: Date.now().toString() // Update physical id to always fetch the latest version
    };

    super(scope, name, { onUpdate: ssmAwsSdkCall });
  }

  public getParameterValue(): string {
    return this.getData('Parameter.Value').toString();
  }
}

To write the hosted zone id to parameter store, you can simply do this:

// route53.ts (deployed in us-east-1)

import { PublicHostedZone } from '@aws-cdk/aws-route53';
import { StringParameter } from '@aws-cdk/aws-ssm';

export const ROUTE_53_HOSTED_ZONE_ID_SSM_PARAM = 'ROUTE_53_HOSTED_ZONE_ID_SSM_PARAM';

/**
 * Other Logic
 */

const hostedZone = new PublicHostedZone(this, 'WebsiteHostedZone', { zoneName: 'example.com' });

new StringParameter(this, 'Route53HostedZoneIdSSMParam', {
  parameterName: ROUTE_53_HOSTED_ZONE_ID_SSM_PARAM,
  description: 'The Route 53 hosted zone id for this account',
  stringValue: hostedZone.hostedZoneId
});

Lastly, you can read that value from the parameter store in that region using the custom resource we just created and use that to create a certificate in us-west-2.

// acm.ts (deployed in us-west-2)

import { DnsValidatedCertificate } from '@aws-cdk/aws-certificatemanager';
import { PublicHostedZone } from '@aws-cdk/aws-route53';

import { ROUTE_53_HOSTED_ZONE_ID_SSM_PARAM } from './route53';
import { SSMParameterReader } from './ssm-parameter-reader';

/**
 * Other Logic
 */

const hostedZoneIdReader = new SSMParameterReader(this, 'Route53HostedZoneIdReader', {
  parameterName: ROUTE_53_HOSTED_ZONE_ID_SSM_PARAM,
  region: 'us-east-1'
});
const hostedZoneId: string = hostedZoneIdReader.getParameterValue();
const hostedZone = PublicHostedZone.fromPublicHostedZoneId(this, 'Route53HostedZone', hostedZoneId);

const certificate = new DnsValidatedCertificate(this, 'ApiGatewayCertificate', { 'pdx.example.com', hostedZone });
like image 72
Reed Hermes Avatar answered Sep 19 '22 19:09

Reed Hermes


The cdk library has been updated, the code avove needs to be changed to the following:

import { Construct } from '@aws-cdk/core';
import { AwsCustomResource, AwsSdkCall } from '@aws-cdk/custom-resources';
import iam = require("@aws-cdk/aws-iam");

interface SSMParameterReaderProps {
  parameterName: string;
  region: string;
}

export class SSMParameterReader extends AwsCustomResource {
  constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
    const { parameterName, region } = props;

    const ssmAwsSdkCall: AwsSdkCall = {
      service: 'SSM',
      action: 'getParameter',
      parameters: {
        Name: parameterName
      },
      region,
      physicalResourceId: {id:Date.now().toString()} // Update physical id to always fetch the latest version
    };

    super(scope, name, { onUpdate: ssmAwsSdkCall,policy:{
        statements:[new iam.PolicyStatement({
        resources : ['*'],
        actions   : ['ssm:GetParameter'],
        effect:iam.Effect.ALLOW,
      }
      )]
    }});
  }

  public getParameterValue(): string {
    return this.getResponseField('Parameter.Value').toString();
  }
}
like image 20
user3782299 Avatar answered Sep 21 '22 19:09

user3782299