DEV Community

Cover image for AWS CDK 100 Drill Exercises #006: VPC Peering - Cross-Account Network Integration and DNS Resolution Automation

AWS CDK 100 Drill Exercises #006: VPC Peering - Cross-Account Network Integration and DNS Resolution Automation

Level 300

Introduction

This is the 6th installment of "AWS CDK 100 Drill Exercises."

For more information about AWS CDK 100 Drill Exercises, please refer to this article.

In this article, we will explore cross-account network integration using VPC Peering, a key feature that enables private communication between multiple VPCs and is a core technology in a multi-account strategy.

Why VPC Peering?

  1. Multi-account strategy: Organizations are recommended to manage development, staging, and production environments in separate accounts
  2. Secure communication: Private communication between VPCs without going through the internet
  3. Cost efficiency: Simpler network configuration at lower cost compared to Transit Gateway
  4. CDK complexity: Learn CDK limitations and workarounds for cross-account resource references
  5. Practical necessity: Essential network configuration pattern in enterprise environments

What You'll Learn

  • VPC Peering connection implementation (same account & cross-account)
  • CDK cross-account stack reference limitations and solutions
  • Value sharing using AWS Systems Manager Parameter Store
  • Cross-account parameter reading via Custom Resources
  • IAM Cross-Account Role design and implementation
  • Automatic VPC Peering connection acceptance and route configuration
  • Cross-account deployment best practices

📁 Code Repository: All code examples for this exercise are available on GitHub.

Architecture Overview

What we will build:

Architecture Overview

Configuration Pattern

Account A (111111111111):
├─ VPC A (10.0.0.0/16)
│  └─ Test Instance (Private Subnet)
├─ VPC B (10.1.0.0/16)
│  └─ Test Instance (Private Subnet)
└─ VPC A ↔ VPC B Peering (same account)

Account B (222222222222):
└─ VPC C (10.2.0.0/16)
   └─ Test Instance (Private Isolated Subnet)

Cross-account Peering:
VPC B (Account A) ↔ VPC C (Account B)
Enter fullscreen mode Exit fullscreen mode

Key Components

  1. VpcAStack (Account A)

    • Create VPC A and VPC B
    • VPC Peering connection within same account
    • Save VPC B information to Parameter Store (readable by Account B)
  2. VpcCStack (Account B)

    • Create VPC C
    • Save VPC C information to Parameter Store (readable by Account A)
    • Create ParameterStoreReadRole (for reading from Account A)
  3. CrossAccountPeeringStack (Account A)

    • Retrieve VPC C information via Custom Resource
    • Create Peering connection between VPC B and VPC C
    • Save Peering connection ID to Parameter Store
    • Create PeeringIdReadRole (for reading from Account B)
  4. VpcCRoutesStack (Account B)

    • Retrieve Peering connection ID via Custom Resource
    • Add routes to VPC B in VPC C's route tables

stack-overview

Prerequisites

  • AWS CLI v2 installed and configured
  • Node.js 20+
  • AWS CDK CLI (npm install -g aws-cdk)
  • Basic TypeScript knowledge
  • Two AWS accounts (for development, production, etc.)
  • AWS CLI profile configuration for each account
  • Understanding of VPC basic concepts (refer to VPC Basics)

Project Directory Structure

vpc-peering/
├── bin/
│   └── vpc-peering.ts                     # Application entry point
├── lib/
│   ├── stacks/
│   │   ├── vpc-a-stack.ts                 # Step1. VPC A/B + same account peering
│   │   ├── vpc-c-stack.ts                 # Step2. VPC C (Account B)
│   │   ├── cross-account-peering-stack.ts # Step3. Cross-account peering
│   │   └── vpc-c-routes-stack.ts          # Step4. VPC C route configuration
│   ├── stages/
│   │   └── vpc-peering-stage.ts           # Deployment orchestration
│   └── types/
│       └── vpc-peering-params.ts          # Parameter type definitions
├── parameters/
│   └── environments.ts                   # Environment-specific parameters
└── test/
    ├── compliance/
    │   └── cdk-nag.test.ts               # CDK Nag compliance test
    ├── snapshot/
    │   └── snapshot.test.ts              # Snapshot test
    └── unit/
        ├── vpc-a.test.ts                 # VPC Peering stack test
        ├── vpc-c.test.ts                 # VPC C stack test
        ├── vpc-c-routes-stack.test.ts    # VPC C route configuration stack test
        └── cross-account-peering.test.ts # Cross-account peering test
Enter fullscreen mode Exit fullscreen mode

Cross-Account Reference Challenges in CDK

This implementation requires cross-account value references. In this case, there are the following issues:

Problem: CloudFormation Runtime Limitations

When referencing cross-account resources in CDK, it cannot execute due to the following limitations:

// ❌ This does not work
const vpcCStack = new VpcCStack(this, 'VpcC', {
  env: { account: accountB }
});

const peeringStack = new CrossAccountPeeringStack(this, 'Peering', {
  env: { account: accountA },
  peerVpc: vpcCStack.vpc  // ❌ Cross-account reference not allowed
});
Enter fullscreen mode Exit fullscreen mode

Error message:

Stack "CrossAccountPeering" cannot consume a cross reference from stack "VpcC".
Cross stack references are only supported for stacks deployed to the same environment
Enter fullscreen mode Exit fullscreen mode

Why doesn't it work?

  1. CloudFormation Export/Import limitations

    • CloudFormation Export/Import only works within the same account and same region
    • Cannot directly reference stack outputs between different accounts
  2. How CDK stack references work

    • CDK internally uses CloudFormation Export/Import
    • This mechanism cannot be used for cross-account
  3. Evaluation timing during deployment

    • vpcCStack.vpc.vpcId is evaluated at deployment time
    • Cannot retrieve values from resources deployed in Account B during Account A deployment

Solutions

To solve cross-account references, the following methods can be considered.
Ultimately, we adopted the second method.

  • 1. Static parameter passing via CDK Context or environment variable files
  • 2. Passing via Parameter Store and AWS Custom Resources

1. Manual Parameter Passing via CDK Context or Environment Variable Files

Implementation:

// Get parameter for VPC C ID created in Account B
const vpcCId = this.node.tryGetContext('vpcCId');

if (!vpcCId) {
  console.warn('vpcCId not provided. Run: cdk deploy --context vpcCId=vpc-xxx');
  return;
}

// Create VPC Peering
const peering = new ec2.CfnVPCPeeringConnection(this, 'Peering', {
  vpcId: localVpc.vpcId,
  peerVpcId: vpcCId,  // ✅ Works
  peerOwnerId: accountB,
});
Enter fullscreen mode Exit fullscreen mode

Deployment procedure:

# 1. Create VPC C in Account B
cdk deploy --profile account-b VpcC

# 2. Manually retrieve VPC C ID
VPC_C_ID=$(aws ec2 describe-vpcs --profile account-b \
  --filters "Name=tag:Name,Values=VpcC" \
  --query 'Vpcs[0].VpcId' --output text)

# 3. Create Peering in Account A (pass VPC ID)
cdk deploy --profile account-a --context vpcCId=$VPC_C_ID CrossAccountPeering
Enter fullscreen mode Exit fullscreen mode

Issues:

  • ⚠️ Manual parameter retrieval required
  • Difficult to automate in CI/CD pipelines
  • Human error risk (copy & paste mistakes, etc.)
  • Lack of scalability (management becomes complex as parameters increase)

2. Parameter Store and AWS Custom Resources

Architecture:

Account B:
├─ VPC C
├─ SSM Parameter (/project/env/vpc-c/id)
└─ ParameterStoreReadRole <- Can be assumed by Account A

Account A:
├─ Custom Resource
│  ├─ assumeRole -> Account B's ParameterStoreReadRole
│  └─ getParameter -> Retrieve VPC C ID
└─ VPC Peering Connection (using retrieved VPC C ID)
Enter fullscreen mode Exit fullscreen mode

stack-step3

stack-step4

Implementation:

  1. Account B: Create Parameter Store + Read Role
// VpcCStack (Account B)
const vpcCIdParam = new ssm.StringParameter(this, 'VpcIdParam', {
  stringValue: this.vpcC.vpc.vpcId,
  parameterName: `/project/env/vpc-c/id`,
});

// IAM role for reading from Account A
const readRole = new iam.Role(this, 'ParameterStoreReadRole', {
  assumedBy: new iam.AccountPrincipal(accountA),
  roleName: 'project-env-ParameterStoreReadRole',
});

vpcCIdParam.grantRead(readRole);
Enter fullscreen mode Exit fullscreen mode
  1. Account A: Read parameter with Custom Resource
// CrossAccountPeeringStack (Account A)
import * as cr from 'aws-cdk-lib/custom-resources';

const readRoleArn = `arn:aws:iam::${accountB}:role/project-env-ParameterStoreReadRole`;

// Read Parameter Store from other account with Custom Resource
const getVpcCId = new cr.AwsCustomResource(this, 'GetVpcCId', {
  onUpdate: {
    service: 'SSM',
    action: 'getParameter',
    parameters: {
      Name: '/project/env/vpc-c/id',
    },
    region: 'ap-northeast-1',
    physicalResourceId: cr.PhysicalResourceId.of('VpcCIdLookup'),
    assumedRoleArn: readRoleArn,  // ✅ Assume Account B's Role
  },
  policy: cr.AwsCustomResourcePolicy.fromStatements([
    new iam.PolicyStatement({
      actions: ['sts:AssumeRole'],
      resources: [readRoleArn],
    }),
  ]),
});

const vpcCId = getVpcCId.getResponseField('Parameter.Value');

// Create VPC Peering
const peering = new ec2.CfnVPCPeeringConnection(this, 'Peering', {
  vpcId: localVpc.vpcId,
  peerVpcId: vpcCId,  // ✅ Use automatically retrieved value
  peerOwnerId: accountB,
});
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Fully automated: No manual parameter retrieval required
  • CI/CD ready: Can be integrated into pipelines
  • Secure: Strict access control with IAM roles
  • Scalable: Supports multiple parameters
  • Error detection: Errors are automatically detected at deployment time

Drawbacks:

  • Lambda function is automatically created (slight cost increase)
  • Implementation is somewhat complex
  • Custom Resource lifecycle management required

VPC Peering Within Same Account

The implementation of VPC Peering within the same account is as follows.

VpcAStack Implementation

// lib/stacks/vpc-peering-stack.ts

export class VpcAStack extends cdk.Stack {
  public readonly vpcA: VpcConstruct;
  public readonly vpcB: VpcConstruct;

  constructor(scope: Construct, id: string, props: VpcAStackProps) {
    super(scope, id, props);

    // Create VPC A
    this.vpcA = new VpcConstruct(this, 'VpcA', {
        // Specify parameters
    });

    // Create VPC B
    this.vpcB = new VpcConstruct(this, 'VpcB', {
        // Specify parameters
    });

    // VPC Peering connection
    const peering = new VpcPeering(this, 'VpcABPeering', {
      vpc: this.vpcA.vpc,
      peerVpc: this.vpcB.vpc,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

VPC Peering Construct

// common/constructs/vpc/vpc-peering.ts
export class VpcPeering extends Construct {
  public readonly vpcPeeringConnection: ec2.CfnVPCPeeringConnection;
  public readonly localSecurityGroup: ec2.ISecurityGroup;
  public readonly peeringSecurityGroup: ec2.ISecurityGroup;

  constructor(scope: Construct, id: string, props: VpcPeeringProps) {
    super(scope, id);

    // CIDR overlap check
    if (props.vpc.vpcCidrBlock === props.peerVpc.vpcCidrBlock) {
      throw new Error(`VPC CIDR blocks overlap`);
    }

    // Create VPC Peering connection
    this.vpcPeeringConnection = new ec2.CfnVPCPeeringConnection(this, 'Connection', {
      vpcId: props.vpc.vpcId,
      peerVpcId: props.peerVpc.vpcId,
    });

    // Add routes (Local VPC -> Peer VPC)
    props.vpc.privateSubnets.forEach((subnet, index) => {
      new ec2.CfnRoute(this, `RouteToPeer${index}`, {
        routeTableId: subnet.routeTable.routeTableId,
        destinationCidrBlock: props.peerVpc.vpcCidrBlock,
        vpcPeeringConnectionId: this.vpcPeeringConnection.ref,
      });
    });

    // Add routes (Peer VPC -> Local VPC)
    props.peerVpc.privateSubnets.forEach((subnet, index) => {
      new ec2.CfnRoute(this, `RouteToLocal${index}`, {
        routeTableId: subnet.routeTable.routeTableId,
        destinationCidrBlock: props.vpc.vpcCidrBlock,
        vpcPeeringConnectionId: this.vpcPeeringConnection.ref,
      });
    });

    // Security groups (allow mutual communication)
    :

  }
}
Enter fullscreen mode Exit fullscreen mode

Cross-Account VPC Peering

Implementation

Step 3. Deploy to Account A: CrossAccountPeeringStack
   ├─ Retrieve VPC C information via Custom Resource
   ├─ Create VPC B <-> VPC C Peering
   ├─ Save Peering ID to Parameter Store
   └─ Create PeeringIdReadRole
Enter fullscreen mode Exit fullscreen mode

stack-step3

Step 4. Deploy to Account B: VpcCRoutesStack
   ├─ Retrieve Peering ID via Custom Resource
   └─ Update VPC C route tables
Enter fullscreen mode Exit fullscreen mode

stack-step4

VpcCStack (Account B)

// lib/stacks/vpc-c-stack.ts

export class VpcCStack extends cdk.Stack {
  public readonly vpcC: VpcConstruct;

  constructor(scope: Construct, id: string, props: VpcCStackProps) {
    super(scope, id, props);

    // Create VPC C
    this.vpcC = new VpcConstruct(this, 'VpcC', {
        // Specify parameters
    });

    if (props.params.accountAId) {
      // Save VPC C information to Parameter Store
      const vpcCIdParam = new ssm.StringParameter(this, 'VpcIdParam', {
        stringValue: this.vpcC.vpc.vpcId,
        description: 'VPC C ID in Account B',
        parameterName: `/${props.project}/${props.environment}/vpc-c/id`,
      });

      const parameterReadRole = new iam.Role(this, 'ParameterStoreReadRole', {
        assumedBy: new iam.AccountPrincipal(props.params.accountAId),
        roleName: `${props.project}-${props.environment}-ParameterStoreReadRole`,
        description: `Role to allow Account ${props.params.accountAId} to read VPC C parameters from Parameter Store`,
      });
      // Grant read access to VPC C parameters
      vpcCIdParam.grantRead(parameterReadRole);

      // Peering acceptance role
      const peeringRole = new iam.Role(this, 'VpcPeeringRole', {
        assumedBy: new iam.AccountPrincipal(props.params.accountAId),
        roleName: props.peeringRoleName,
      });

      peeringRole.addToPolicy(new iam.PolicyStatement({
        actions: ['ec2:AcceptVpcPeeringConnection'],
        resources: ['*'],
      }));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CrossAccountPeeringStack (Account A)

// lib/stacks/cross-account-peering-stack.ts

export class CrossAccountPeeringStack extends cdk.Stack {
  public readonly peeringConnection: ec2.CfnVPCPeeringConnection;

  constructor(scope: Construct, id: string, props: CrossAccountPeeringStackProps) {
    super(scope, id, props);

    // Retrieve VPC C ID via Custom Resource
    const getVpcCId = new cr.AwsCustomResource(this, 'GetVpcCId', {
      onUpdate: {
        service: 'SSM',
        action: 'getParameter',
        parameters: {
          Name: parameterName,
        },
        region: region,
        physicalResourceId: cr.PhysicalResourceId.of('VpcCIdLookup'),
        assumedRoleArn: parameterReadRoleArn,
      },
      policy: cr.AwsCustomResourcePolicy.fromStatements([
        new iam.PolicyStatement({
          actions: ['sts:AssumeRole'],
          resources: [parameterReadRoleArn],
        }),
      ]),
    });
    const vpcCId = getVpcCId.getResponseField('Parameter.Value');

    // Create VPC Peering
    this.peeringConnection = new ec2.CfnVPCPeeringConnection(this, 'VpcBCPeering', {
      vpcId: props.requestorVpc.vpcId,
      peerVpcId: vpcCId,
      peerOwnerId: props.params.accountBId,
      peerRegion: props.params.regionB,
      peerRoleArn: `arn:aws:iam::${props.params.accountBId}:role/${props.peeringRoleName}`,
    });

    // Save Peering ID to Parameter Store
    const peeringIdParam = new ssm.StringParameter(this, 'PeeringConnectionIdParam', {
      stringValue: this.peeringConnectionId,
      description: 'VPC Peering Connection ID for VPC B <-> VPC C',
      parameterName: `/${props.project}/${props.environment}/peering/vpc-b-vpc-c/id`,
    });
    const peeringIdReadRole = new iam.Role(this, 'PeeringIdReadRole', {
      assumedBy: new iam.AccountPrincipal(props.params.accountBId),
      roleName: `${props.project}-${props.environment}-PeeringIdReadRole`,
      description: `Role to allow Account ${props.params.accountBId} to read Peering Connection ID from Parameter Store`,
    });
    peeringIdParam.grantRead(peeringIdReadRole);
  }
}
Enter fullscreen mode Exit fullscreen mode

VpcCRoutesStack (Account B)

// lib/stacks/vpc-c-routes-stack.ts

export class VpcCRoutesStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: VpcCRoutesStackProps) {
    super(scope, id, props);

    // Account A role
    const parameterReadRoleArn = `arn:aws:iam::${props.params.accountAId}:role/${props.project}-${props.environment}-PeeringIdReadRole`;

    // Retrieve Peering ID via Custom Resource
    const getPeeringConnectionId = new cr.AwsCustomResource(this, 'GetPeeringConnectionId', {
      onUpdate: {
        service: 'SSM',
        action: 'getParameter',
        parameters: {
          Name: props.peeringIdParamName, // /${props.project}/${props.environment}/peering/vpc-b-vpc-c/id,
        },
        region: regionA,
        physicalResourceId: cr.PhysicalResourceId.of('PeeringConnectionIdLookup'),
        assumedRoleArn: parameterReadRoleArn,
      },
      policy: cr.AwsCustomResourcePolicy.fromStatements([
        new iam.PolicyStatement({
          actions: ['sts:AssumeRole'],
          resources: [parameterReadRoleArn],
        }),
      ]),
    });
    const peeringConnectionId = getPeeringConnectionId.getResponseField('Parameter.Value');

    // target Subnet
    // private or isolated subnets
    const targetSubnets =
      (props.vpc.privateSubnets && props.vpc.privateSubnets.length > 0)
        ? props.vpc.privateSubnets
        : props.vpc.isolatedSubnets;
    // Add routes to VPC C private subnets pointing to VPC B
    targetSubnets.forEach((subnet, index) => {
      new ec2.CfnRoute(this, `VpcCToVpcBRoute${index + 1}`, {
        routeTableId: subnet.routeTable.routeTableId,
        destinationCidrBlock: props.vpcBCidr,
        vpcPeeringConnectionId: peeringConnectionId,
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

VPC Peering DNS Resolution Options Automation (Custom Resource Implementation Example)

Why DNS Resolution Options are Needed

Just creating a VPC Peering connection doesn't enable resolution of private DNS names in the peer VPC. For example, if you want to access an EC2 instance in VPC B from VPC A using a private DNS name like ip-10-1-0-10.ap-northeast-1.compute.internal, you need to enable DNS resolution options.

Normally, this option is configured manually from the console or set using AWS CLI as follows (documentation is here):

aws ec2 modify-vpc-peering-connection-options \
  --vpc-peering-connection-id pcx-xxxxx \
  --requester-peering-connection-options AllowDnsResolutionFromRemoteVpc=true \
  --accepter-peering-connection-options AllowDnsResolutionFromRemoteVpc=true
Enter fullscreen mode Exit fullscreen mode

In this implementation, we automate this configuration using AWS CDK's AwsCustomResource.

For Same Account

For same account, specify both RequesterPeeringConnectionOptions and AccepterPeeringConnectionOptions.

same-account

    // Enable DNS resolution over VPC Peering
    const onCreate: cr.AwsSdkCall = {
        service: 'EC2',
        action: 'modifyVpcPeeringConnectionOptions',
        parameters: {
            VpcPeeringConnectionId: this.peeringConnection.ref,
                RequesterPeeringConnectionOptions: {
                    AllowDnsResolutionFromRemoteVpc: true
                },
                AccepterPeeringConnectionOptions: {
                    AllowDnsResolutionFromRemoteVpc: true,
                }
        },
        region: props.env?.region,
        physicalResourceId: cr.PhysicalResourceId.of(`EnableVpcPeeringDnsResolution:${this.peeringConnection.ref}`),
    }
    const onUpdate = onCreate;
    const onDelete: cr.AwsSdkCall = {
        service: "EC2",
        action: "modifyVpcPeeringConnectionOptions",
        parameters: {
            VpcPeeringConnectionId: this.peeringConnection.ref,
            RequesterPeeringConnectionOptions: {
                AllowDnsResolutionFromRemoteVpc: false
            },
            AccepterPeeringConnectionOptions: {
                AllowDnsResolutionFromRemoteVpc: false,
            }
        },
    }
    new cr.AwsCustomResource(this, 'EnableVpcPeeringDnsResolution', {
        onUpdate,
        onCreate,
        onDelete,
        policy: cr.AwsCustomResourcePolicy.fromSdkCalls({resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE}),
    });
Enter fullscreen mode Exit fullscreen mode

For Cross-Account

For cross-account VPC Peering, configure separately from Requester and Accepter side stacks.

※Duplicate parts from the same account case are omitted.

  1. Requester side (Account A): CrossAccountPeeringStack

requester-vpc

        parameters: {
            VpcPeeringConnectionId: this.peeringConnection.ref,
            RequesterPeeringConnectionOptions: {
                AllowDnsResolutionFromRemoteVpc: true
            },
        },
Enter fullscreen mode Exit fullscreen mode
  1. Accepter side (Account B): VpcCRoutesStack

accepter-vpc

        parameters: {
            VpcPeeringConnectionId: peeringConnectionId,
            AccepterPeeringConnectionOptions: {
                AllowDnsResolutionFromRemoteVpc: true,
            }
        },
Enter fullscreen mode Exit fullscreen mode

Deployment and Cleanup

1. Environment Parameter Configuration

// parameters/environments.ts

export const params: Record<Environment, EnvParams> = {
  [Environment.DEVELOPMENT]: {
    accountAId: '111111111111',  // Account A
    accountBId: '222222222222',  // Account B
    regionA: 'ap-northeast-1',
    regionB: 'ap-northeast-1',
    vpcAConfig: {
      createConfig: {
        cidr: '10.0.0.0/16',
        maxAzs: 2,
        natGateways: 1,
      },
    },
    vpcBConfig: {
      createConfig: {
        cidr: '10.1.0.0/16',
        maxAzs: 2,
        natGateways: 1,
      },
    },
    vpcCConfig: {
      createConfig: {
        cidr: '10.2.0.0/16',
        maxAzs: 2,
        natGateways: 0,
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

2. Bootstrap

1. Bootstrap Account A (pipeline account)

# Basic bootstrap to host pipeline in Account A
npx cdk bootstrap \
    --profile account-a-admin \
    --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
    aws://111111111111/ap-northeast-1
Enter fullscreen mode Exit fullscreen mode

2. Bootstrap Account B (target account)

# Bootstrap Account B and trust Account A
npx cdk bootstrap \
    --profile account-b-admin \
    --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
    --trust 111111111111 \
    aws://222222222222/ap-northeast-1
Enter fullscreen mode Exit fullscreen mode

Important points:

  • --trust 111111111111: Allows Account A to deploy to Account B
  • This configuration allows Account A's bootstrap role to AssumeRole to Account B's resources

How --trust option works

1. IAM Role Creation

When you bootstrap with the --trust option, the following IAM roles are created:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This trust policy allows Account A's principals to:

  • CDKDeployRole: Deploy CloudFormation stacks
  • CDKFilePublishingRole: Upload file assets to S3
  • CDKImagePublishingRole: Push Docker images to ECR

3. Deployment

CDK_CROSS_ACCOUNT_ID=222222222222 npm run stage:deploy:all -w workspaces/vpc-peering --project=myproject --env=dev
Enter fullscreen mode Exit fullscreen mode

4. Connection Verification

Example connection verification from VPC A. In the example below, we connect to EC2, but you can also use CloudShell.

# Ping from VPC A instance to VPC B instance
aws ssm start-session --profile account-a \
  --target i-0xxxxxxxxxxxxx  # VPC A Instance ID

# Connectivity check to VPC B
ping 10.1.x.x  # VPC B Private IP

# Ping to VPC C (cross-account) -> Cannot connect from VPC A
ping 10.2.x.x  # VPC C Private IP
Enter fullscreen mode Exit fullscreen mode

5. Cleanup

If you connected to VPC using CloudShell for connection verification, delete the CloudShell environment before cleanup. Stack deletion will fail.

# Delete all resources
CDK_CROSS_ACCOUNT_ID=222222222222 npm run stage:destroy:all -w workspaces/vpc-peering --project=myproject --env=dev
Enter fullscreen mode Exit fullscreen mode

Summary

This article explained the implementation of VPC Peering using AWS CDK, focusing on cross-account challenges and solutions.

What We Learned

  1. CDK cross-account limitations

    • CloudFormation Export/Import only works within the same account
    • Stack references do not work cross-account
  2. Implementation patterns

    • Parameter Store: Store and share values
    • IAM Cross-Account Role: Secure access control
    • Custom Resource: Dynamic value retrieval at deployment time

References


Let's continue learning practical AWS CDK patterns through the 100 drill exercises!
If you found this helpful, please ⭐ the repository!

📌 You can see the entire code in My GitHub Repository.

Top comments (0)