# Multi-region multi-account  deployments with AWS CDK Pipelines

This post describes how you can create an AWS CDK project that defines resources in multiple regions and then automatically deploy it in multiple environments (AWS accounts) with CDK pipelines.

## Full code

In this post, I am including the most important snippets of code. You can find the complete example project here [github.com/codiply/multi-account-multi-region-cdk-pipeline-example](https://github.com/codiply/multi-account-multi-region-cdk-pipeline-example). This is where you can find, for example, all the Python dependencies (see the requirements file), or all the imports.

In the code in this post, you will encounter parsed configuration objects. These objects are built from configuration files in YAML format. You can find the actual `.yaml` files and the configuration code in the GitHub repo in the `config/` directory.

## Accounts

A very common setup is to have 3 environments: Development (DEV), Preproduction (PRE) and Production (PRO). In addition, a 4th account (tools account) will be used to house the CI/CD pipeline.

In the examples below, I will use the following as AWS account numbers

* `DEV`: `111111111111`
    
* `PRE`: `222222222222`
    
* `PRO`: `333333333333`
    
* `TOOL`: `999999999999`
    

## Manual steps

### GitHub access token

I am hosting my code on GitHub. For Code Pipelines to be able to access the code, I generate a [github access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with scopes `repo` and `admin:repo_hook`. This token needs to be stored as a secret in Secrets Manager in the tools account and in the region where the CI/CD pipeline will be deployed.

### Bootstrap AWS regions for CDK

Bootstrapping of AWS accounts is described [here](https://docs.aws.amazon.com/cdk/v2/guide/cdk_pipeline.html#cdk_pipeline_bootstrap). For how to install the AWS CDK toolkit see [here](https://docs.aws.amazon.com/cdk/v2/guide/cli.html).

In the account/region where the pipeline is deployed (the tools account), run

```bash
cdk bootstrap aws://<account number>/<region> \
  --profile <aws profile name> \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
```

In all account/region pairs where resources are deployed by the pipeline, run

```bash
cdk bootstrap aws://<account number>/<region> \ 
    --profile <aws profile name> \
    --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ 
    --trust <tools/pipeline account number>
```

## Composition

Before diving into CDK code, here is a diagram summarising the composition of different classes. I hope this will aid you to understand how the pieces are put together.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698250162325/e7fbb99a-ee6e-4b5b-a317-d1f89862de7c.png align="center")

## CDK Code

### Constructs

The smallest building block is a Construct. This is a reusable component containing one or more resources, for example, a VPC. ([full code](https://github.com/codiply/multi-account-multi-region-cdk-pipeline-example/blob/main/infrastructure/components/vpc.py))

```python
class Vpc(Construct):
    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        props: VpcProps,
        **kwargs: typing.Any,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        vpc_name = props.naming.prefix

        if props.name_suffix is not None:
            vpc_name += f"-{props.name_suffix}"

        vpc = ec2.Vpc(
            self,
            "vpc",
            ip_addresses=ec2.IpAddresses.cidr(props.cidr or "10.0.0.0/16"),
            max_azs=props.max_availability_zones,
            nat_gateways=props.nat_gateways,
        )

        cdk.Tags.of(vpc).add("Name", vpc_name)
```

### Stacks

One level higher, we create Stacks, for example, a networking stack. ([full code](https://github.com/codiply/multi-account-multi-region-cdk-pipeline-example/blob/main/infrastructure/stacks/networking.py))

```python
class NetworkingStack(cdk.Stack):
    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        props: NetworkingStackProps,
        **kwargs: typing.Any,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        Vpc(
            self,
            "vpc",
            VpcProps(
                naming=props.naming,
                cidr=props.networking_config.vpc_cidr,
                max_availability_zones=props.networking_config.max_availability_zones,
                nat_gateways=props.networking_config.nat_gateways,
            ),
        )
```

### Stages

One level higher, we have stages. **Stages can span regions**. Notice that I create several copies of the networking stack, one in each region. I set the region part of the environment `env=cdk.Environment(region=region)` within the stage. ([full code](https://github.com/codiply/multi-account-multi-region-cdk-pipeline-example/blob/main/infrastructure/stages/architecture.py))

```python
class ArchStage(cdk.Stage):
    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        props: ArchStageProps,
        **kwargs: typing.Any,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        config = load_arch_config(deployment_id=props.deployment_id, environment_id=props.environment_id)

        for region in config.stacks.networking.regions:
            NetworkingStack(
                self,
                f"networking-{region}",
                NetworkingStackProps(naming=config.naming, networking_config=config.networking),
                env=cdk.Environment(region=region),
            )
```

### Pipeline Stack

Finally, I create a pipeline stack. ([full code](https://github.com/codiply/multi-account-multi-region-cdk-pipeline-example/blob/main/infrastructure/stacks/pipeline.py)) This stack is going to be deployed to the Tools account.

First, I create the pipeline.

```python
pipeline = pipelines.CodePipeline(
    self,
    "pipeline",
    pipeline_name=f"{project_name}",
    synth=pipelines.ShellStep(
        "Synth",
        input=pipelines.CodePipelineSource.git_hub(
            repo_string=config.cicd_pipeline.github_repo,
            branch=config.cicd_pipeline.git_branch,
            authentication=cdk.SecretValue.secrets_manager(config.cicd_pipeline.github_token_secret_key),
        ),
        commands=[
            "npm install -g aws-cdk",
            "python -m pip install -r requirements/requirements.txt",
            "cdk synth",
        ],
    ),
    cross_account_keys=True,
)
```

This is where the GitHub repo and the `main` branch are defined, together with the key in Secrets Manager where the GitHub token is stored.

Then I create one stage for each environment. If you have several stages per environment, you can create a Wave and add the stages to the wave. All stages in a wave are deployed in parallel.

```python
for environment_id, account_config in props.accounts_config.accounts.items():
    if account_config.is_enabled and not account_config.is_cicd_account:
        wave = pipeline.add_wave(f"wave-{environment_id}")

        account_config = props.accounts_config.accounts[environment_id]
        account_id = account_config.account_id

        if account_config.needs_manual_approval:
            wave.add_pre(
                pipelines.ManualApprovalStep(
                    f"approve-{environment_id}", comment=f"Approve deployment to {environment_id}"
                )
            )

        wave.add_stage(
            ArchStage(
                self,
                f"{project_name}-{environment_id}",
                props=ArchStageProps(deployment_id=props.deployment_id, environment_id=environment_id),
                env=cdk.Environment(account=account_id),
            )
        )
```

Notice that for each Stage that I am instantiating, I am setting only the account in the environment `env=cdk.Environment(account=account_id)`. The regions are set within the stage code.

For environments PRE and PRO, I have included a manual approval step.

## Deploy the pipeline

In my example, I have configured the pipeline to be deployed in `eu-west-1` and I create a networking stack in 2 regions `eu-west-1` and `eu-west-2`.

When I list the stacks, I see the following.

```bash
> cdk ls
example-cdk-pipeline-pipeline
cross-region-stack-999999999999:eu-west-2
example-cdk-pipeline-pipeline/example-cdk-pipeline-dev/networking-eu-west-1
example-cdk-pipeline-pipeline/example-cdk-pipeline-dev/networking-eu-west-2
example-cdk-pipeline-pipeline/example-cdk-pipeline-pre/networking-eu-west-1
example-cdk-pipeline-pipeline/example-cdk-pipeline-pre/networking-eu-west-2
example-cdk-pipeline-pipeline/example-cdk-pipeline-prod/networking-eu-west-1
example-cdk-pipeline-pipeline/example-cdk-pipeline-prod/networking-eu-west-2
```

First, you have to make sure that you have pushed your code to the main branch of your GitHub repo. Then, deploy the pipeline stack.

```bash
cdk deploy example-cdk-pipeline-pipeline
```

This will create the Code Pipeline project, and it will trigger it.

From that point forward, you do not need to deploy anything manually, not even when you make changes to the pipeline stack. The Code Pipeline project is self-mutating.

While developing, you can deploy individual stacks from your development environment by passing the full name of the stack. For example:

```bash
cdk deploy example-cdk-pipeline-pipeline/example-cdk-pipeline-dev/networking-eu-west-1
```

When using `cdk deploy`, you will need to have the right credentials in your environment for the account you are deploying to. Alternatively, if you are using profiles then pass `--profile <name of the profile>`.

## Running the pipeline

The pipeline runs automatically when new code is merged into the `main` branch. You can also trigger it manually by clicking `Release Change` in the page of the pipeline Management Console.

After deploying to DEV, the pipeline will wait for manual approval before deploying to PRE. The same will happen before deploying to PRO environment.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698250656180/b9d318d4-c543-4984-8005-ab431eeb901a.png align="center")

## Cleanup

Delete manually all Cloudformation stacks. Do not forget to do that for all AWS accounts and regions. See the output of `cdk ls` to see all stacks that need to be deleted.

Alternatively, you can do the following for each stack from the command line

```bash
cdk destroy --profile <aws profile> <name of the stack>
```

## Wrap up

In this post, I described how you can create a CDK project that creates resources in multiple regions, and then I deployed the project in several environments (AWS accounts) with CDK Pipelines.

Do not forget to check the full code in the GitHub repo: [github.com/codiply/multi-account-multi-region-cdk-pipeline-example](https://github.com/codiply/multi-account-multi-region-cdk-pipeline-example)
