Infrastructure & Automation

Multiple-account, multiple-Region AWS CloudFormation

AWS CloudFormation nested stacks provide a great way to break down templates into reusable components and logically separate groups of resources. To do this, you can use the AWS::CloudFormation::Stack resource type, which launches the child stack into the same account, AWS Region, and AWS Identity and Access Management (IAM) identity as the parent.

But what if you don’t want the child stack in the same account or Region as the parent stack? Examples of this use case include disaster-recovery stacks that place backups into a different Region, or CI/CD pipelines that are run centrally and manage resources in dev, QA, and prod accounts. In this post, I will cover a custom resource that behaves similarly to the native resource type but allows the customer to specify a target account, Region, and IAM role for the child stack. There are many more use-cases where multi-account or cross-region CloudFormation stacks can be useful.

The example launches a CloudFormation stack in a central account (CentralAccount) that provisions child stacks, each provisioning an Amazon Simple Storage Service (Amazon S3) bucket, into another account (DevAccount) in two different Regions.

cross account architecture diagram

To start using the cross-account custom resource in your own stacks, or to browse the example templates covered in this post, check it out in GitHub.

Getting set up

To complete the steps in the following example walkthrough, you can use the AWS Management Console, AWS Command Line Interface (AWS CLI) or SDKs. I’m going to use the AWS CLI, which I set up with two profiles, one called DevAccount and one called CentralAccount. Ideally, the two profiles should be configured with credentials from two different accounts, but if you do not have access to two different accounts, you can test it all in one account by pointing both profiles at the same account. Instructions on configuring AWS CLI to use profiles are available in the AWS CLI documentation.

I need to create an IAM role in each account. The role in CentralAccount will be granted permission to assume the DevAccount role. The DevAccount role will have a trust policy that trusts the role in CentralAccount, and it will have permissions to manage the CloudFormation stacks and the S3 buckets that the example stack will create. When using this with your own templates, expand the target account (DevAccount) policy to include any resources that your template provisions. For more information on how cross-account IAM works, see the IAM documentation.

To simplify this, I’ve created central-iam.yaml and dev-iam.yaml AWS CloudFormation templates to provision the example roles. These templates each require the other’s role name to be provided, so we have what seems like a circular dependency problem. To deal with this, I will hardcode the role names instead of letting AWS CloudFormation autogenerate them. The downside of this approach is that you cannot launch more than one of these templates in a single account, as the name will collide. If you want to have more than one role, you will need to specify a unique name for the RoleName parameter for each additional stack.

To launch these stacks, I will need the AWS account ID for each account. If you don’t know the account IDs, you can get them from the AWS CLI by using the sts get-caller-identity command.

#CentralAccount ID
aws sts get-caller-identity –profile CentralAccount ––query 'Account'
#DevAccount ID
aws sts get-caller-identity –profile DevAccount ––query 'Account'

To launch the CentralAccount stack and create the role, I use the create-stack command. Be sure to replace <DEV_ACCOUNT_ID> with the AWS account ID for DevAccount.

aws cloudformation create-stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --template-url https://s3.amazonaws.com/aws-quickstart/quickstart-examples/samples/cloudformation-cross-account/examples/central-iam.yaml \
    --stack-name cross-account-cfn-role \
    --output text ––query "StackId" \
    --region us-east-1 \
    --profile CentralAccount \
    --parameters ParameterKey=DevAccountId,ParameterValue=<DEV_ACCOUNT_ID>

We need to wait for the stack to reach CREATE_COMPLETE, because when the DevAccount role is created, the IAM service will validate the Role ARN in the trust policy and transform it to a unique ID for the cross-account trust. More information on this is in the IAM documentation.

You can run the describe-stacks command periodically to check the stack status until CREATE_COMPLETE is shown in the output. Note the CentralAccount ARN value, as you’ll need it later on.

aws cloudformation describe-stacks \
    --stack-name cross-account-cfn-role \
    --query 'Stacks[0].[StackStatus, Outputs[0].OutputValue]' \
    --region us-east-1 \
    --profile CentralAccount

Now, using the DevAccount profile, I create the DevAccount role. Be sure to replace <CENTRAL_ACCOUNT_ID> with the AWS account ID of the CentralAccount stack.

aws cloudformation create-stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --template-url https://s3.amazonaws.com/aws-quickstart/quickstart-examples/samples/cloudformation-cross-account/examples/dev-iam.yaml \
    --stack-name cross-account-cfn-role \
    --output text --query "StackId" \
    --region us-east-1 \
    --profile DevAccount \
    --parameters ParameterKey=CentralAccountId,ParameterValue=<CENTRAL_ACCOUNT_ID>

We’ll use the same describe-stacks command against the DevAccount stack to get the ARN that we will need later on. The ARN will be available only when the stack reaches the CREATE_COMPLETE state.

aws cloudformation describe-stacks \
    --stack-name cross-account-cfn-role \
    --query 'Stacks[0].[StackStatus, Outputs[0].OutputValue]' \
    --region us-east-1 \
    --profile DevAccount

That completes the prerequisites. In a scenario where you want a central account to create stacks in a group of other accounts, you need to create the central role only once. The target roles can be created to delegate trust to the central account as part of the provisioning process for new accounts.

Where the magic happens

With the needed IAM roles in place, we can start to create AWS CloudFormation templates that use the roles to deploy resources across multiple accounts. Let’s have a look at the cross-account.yaml template. It contains an AWS CloudFormation custom resource to launch the provided template into the remote account and Region. Here’s a snippet showing a cross-account custom resource declaration:

...
Resources:
  DevStackTokyo:
    Type: Custom::CrossAccountStack
    Version: 1.0
    Properties:
      ServiceToken: !GetAttCrossAccountLambda.Arn
      RoleArn: !GetAttCrossAccountRole.Arn
      TemplateURL: https://s3.amazonaws.com/aws-quickstart/quickstart-examples/samples/cloudformation-cross-account/examples/bucket.yaml
      ParentStackId: !RefAWS::StackId
      Region: ap-northeast-1
      CfnParameters:
        Tag: Tokyo
...

The TemplateUrl property is pointed at the template that will be launched, and the CfnParameters property provides values for the template’s parameters. In this case, we’ve just got a Tag parameter.

Using this custom resource in your own stacks, you can easily enable cross-account provisioning for your existing template library. You can also have a look at the Quick Start catalog, which provides reference architectures for popular workloads, all of which can be enabled for cross-account provisioning by using this custom resource.

Launching the stack

Let’s go ahead and launch the stack. You will need to replace <CENTRALACCOUNT_ROLE_ARN> and <DEVACCOUNT_ROLE_ARN> with the ARNs that you obtained from the outputs in the Getting set up section of this post.

aws cloudformation create-stack \
    --template-url https://s3.amazonaws.com/aws-quickstart/quickstart-examples/samples/cloudformation-cross-account/examples/cross-account.yaml \
    --stack-name cross-account-buckets \
    --output text ––query "StackId" \
    --region us-east-1 \
    --profile CentralAccount \
    --parameters \
        ParameterKey=DevRoleArn,ParameterValue=<DEVACCOUNT_ROLE_ARN> \
        ParameterKey=CentralRoleArn,ParameterValue=<CENTRALACCOUNT_ROLE_ARN>

Again, we can keep an eye on progress by using the describe-stacks command.

aws cloudformation describe-stacks \
    --stack-name cross-account-buckets \
    --query 'Stacks[0].[StackStatus, Outputs[]]' \
    --region us-east-1 \
    --query 'StackSummaries[?starts_with(StackName, `cross-account-buckets`) == `true`].[StackName, StackStatus][0]' \
    --profile CentralAccount

After the stack reaches CREATE_COMPLETE, the buckets in Stockholm and Tokyo are created. Let’s have a look at the CloudFormation stacks in DevAccount to confirm.

aws cloudformation list-stacks \
    --stack-status-filter CREATE_COMPLETE \
    --region eu-north-1 \
    --query 'StackSummaries[?starts_with(StackName, `cross-account-buckets`) == `true`].[StackName, StackStatus][0]' \
    --profile DevAccount
aws cloudformation list-stacks \
    --stack-status-filter CREATE_COMPLETE \
    --region ap-northeast-1 \
    --query 'StackSummaries[?starts_with(StackName, `cross-account-buckets`) == `true`].[StackName, StackStatus][0]' \
    --profile DevAccount

In the output, you should see the CloudFormation stack names, and that they are in the CREATE_COMPLETE state.

Cleanup

Let’s use the delete-stack command to quickly clean up all the stacks we created in this walkthrough. We’ll need to do the cross-account-buckets stack first, seeing as it needs to use the roles in the other stacks.

aws cloudformation delete-stack \
    --stack-name cross-account-buckets \
    --region us-east-1 \
    --profile CentralAccount

You can use the same describe-stacks command that you used to check on the progress when creating the stack. After delete-stack has completed, we can delete the two roles that we created in the prerequisites.

aws cloudformation delete-stack \
    --stack-name cross-account-cfn-role \
    --region us-east-1 \
    --profile CentralAccount
aws cloudformation delete-stack \
    --stack-name cross-account-cfn-role \
    --region us-east-1 \
    --profile DevAccount

Next steps

The AWS Lambda function source code and the examples in this post are available on GitHub in the cloudformation-cross-account folder in the quickstart-examples repository. Check it out to start building your multi-account infrastructure-as-code templates using AWS CloudFormation. You can use GitHub issues for feature requests, and the comments section below to let us know how you’re using this custom resource in your environment.