実践!AWS CDK #13 ルートテーブル

題字・息子たち
2021.06.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回はルートテーブルを作成します。
プログラムは長めです。

前回の記事はこちら。

AWS 構成図

1

前回と変わらずです。ネットワークのルーティングなので構成図には記載していません。

設計

作成するルートテーブルは 4 つ。プロパティは以下の通りです。

リソース名 関連付けるサブネット
devio-stg-rtb-public devio-stg-subnet-public-1a
devio-stg-subnet-public-1c
devio-stg-rtb-app-1a devio-stg-subnet-app-1a
devio-stg-rtb-app-1c devio-stg-subnet-app-1c
devio-stg-rtb-db devio-stg-subnet-db-1a
devio-stg-subnet-db-1c

以下、詳細。

devio-stg-rtb-public

送信先 ターゲット
10.0.0.0/16 local
0.0.0.0/0 devio-stg-igw

パブリックサブネットに関連付けるルートテーブル。
VPC 内の通信(ローカルルート)以外はインターネットゲートウェイをターゲットとします。

devio-stg-rtb-app-1a

送信先 ターゲット
10.0.0.0/16 local
0.0.0.0/0 devio-stg-ngw-1a

ap-northeast-1a にあるアプリケーション層プライベートサブネットに関連付けるルートテーブル。
ローカルルート以外は同一 AZ の NAT ゲートウェイへ。

devio-stg-rtb-app-1c

送信先 ターゲット
10.0.0.0/16 local
0.0.0.0/0 devio-stg-ngw-1c

ap-northeast-1c にあるアプリケーション層プライベートサブネットに関連付けるルートテーブル。
ローカルルート以外は同一 AZ の NAT ゲートウェイへ。

devio-stg-rtb-db

送信先 ターゲット
10.0.0.0/16 local

データ層のプライベートサブネットに関連付けるルートテーブル。
特に指定なし。とりあえず作成する。

上記のローカルルート設定はルートテーブルを作成すると自動で付与されます。よって、この部分に関してはプログラムを書きません。

実装

ルートテーブルに関する処理を行うクラスはこちら。

lib/resource/routeTable.ts

import * as cdk from '@aws-cdk/core';
import { CfnRouteTable, CfnRoute, CfnSubnetRouteTableAssociation, CfnVPC, CfnSubnet, CfnInternetGateway, CfnNatGateway } from '@aws-cdk/aws-ec2';
import { Resource } from './abstract/resource';

interface RouteInfo {
    readonly id: string;
    readonly destinationCidrBlock: string;
    readonly gatewayId?: () => string;
    readonly natGatewayId?: () => string;
}

interface AssociationInfo {
    readonly id: string;
    readonly subnetId: () => string;
}

interface ResourceInfo {
    readonly id: string;
    readonly resourceName: string;
    readonly routes: RouteInfo[];
    readonly associations: AssociationInfo[];
    readonly assign: (routeTable: CfnRouteTable) => void;
}

export class RouteTable extends Resource {
    public public: CfnRouteTable;
    public app1a: CfnRouteTable;
    public app1c: CfnRouteTable;
    public db: CfnRouteTable;

    private readonly vpc: CfnVPC;
    private readonly subnetPublic1a: CfnSubnet;
    private readonly subnetPublic1c: CfnSubnet;
    private readonly subnetApp1a: CfnSubnet;
    private readonly subnetApp1c: CfnSubnet;
    private readonly subnetDb1a: CfnSubnet;
    private readonly subnetDb1c: CfnSubnet;
    private readonly internetGateway: CfnInternetGateway;
    private readonly natGateway1a: CfnNatGateway;
    private readonly natGateway1c: CfnNatGateway;
    private readonly resources: ResourceInfo[] = [
        {
            id: 'RouteTablePublic',
            resourceName: 'rtb-public',
            routes: [{
                id: 'RoutePublic',
                destinationCidrBlock: '0.0.0.0/0',
                gatewayId: () => this.internetGateway.ref
            }],
            associations: [
                {
                    id: 'AssociationPublic1a',
                    subnetId: () => this.subnetPublic1a.ref
                },
                {
                    id: 'AssociationPublic1c',
                    subnetId: () => this.subnetPublic1c.ref
                }
            ],
            assign: routeTable => this.public = routeTable
        },
        {
            id: 'RouteTableApp1a',
            resourceName: 'rtb-app-1a',
            routes: [{
                id: 'RouteApp1a',
                destinationCidrBlock: '0.0.0.0/0',
                natGatewayId: () => this.natGateway1a.ref
            }],
            associations: [{
                id: 'AssociationApp1a',
                subnetId: () => this.subnetApp1a.ref
            }],
            assign: routeTable => this.app1a = routeTable
        },
        {
            id: 'RouteTableApp1c',
            resourceName: 'rtb-app-1c',
            routes: [{
                id: 'RouteApp1c',
                destinationCidrBlock: '0.0.0.0/0',
                natGatewayId: () => this.natGateway1c.ref
            }],
            associations: [{
                id: 'AssociationApp1c',
                subnetId: () => this.subnetApp1c.ref
            }],
            assign: routeTable => this.app1c = routeTable
        },
        {
            id: 'RouteTableDb',
            resourceName: 'rtb-db',
            routes: [],
            associations: [
                {
                    id: 'AssociationDb1a',
                    subnetId: () => this.subnetDb1a.ref
                },
                {
                    id: 'AssociationDb1c',
                    subnetId: () => this.subnetDb1c.ref
                }
            ],
            assign: routeTable => this.db = routeTable
        }
    ];

    constructor(
        vpc: CfnVPC,
        subnetPublic1a: CfnSubnet,
        subnetPublic1c: CfnSubnet,
        subnetApp1a: CfnSubnet,
        subnetApp1c: CfnSubnet,
        subnetDb1a: CfnSubnet,
        subnetDb1c: CfnSubnet,
        internetGateway: CfnInternetGateway,
        natGateway1a: CfnNatGateway,
        natGateway1c: CfnNatGateway
    ) {
        super();
        this.vpc = vpc;
        this.subnetPublic1a = subnetPublic1a;
        this.subnetPublic1c = subnetPublic1c;
        this.subnetApp1a = subnetApp1a;
        this.subnetApp1c = subnetApp1c;
        this.subnetDb1a = subnetDb1a;
        this.subnetDb1c = subnetDb1c;
        this.internetGateway = internetGateway;
        this.natGateway1a = natGateway1a;
        this.natGateway1c = natGateway1c;
    }

    createResources(scope: cdk.Construct) {
        for (const resourceInfo of this.resources) {
            const routeTable = this.createRouteTable(scope, resourceInfo);
            resourceInfo.assign(routeTable);
        }
    }

    private createRouteTable(scope: cdk.Construct, resourceInfo: ResourceInfo): CfnRouteTable {
        const routeTable = new CfnRouteTable(scope, resourceInfo.id, {
            vpcId: this.vpc.ref,
            tags: [{
                key: 'Name',
                value: this.createResourceName(scope, resourceInfo.resourceName)
            }]
        });

        for (const routeInfo of resourceInfo.routes) {
            this.createRoute(scope, routeInfo, routeTable);
        }

        for (const associationInfo of resourceInfo.associations) {
            this.createAssociation(scope, associationInfo, routeTable);
        }

        return routeTable;
    }

    private createRoute(scope: cdk.Construct, routeInfo: RouteInfo, routeTable: CfnRouteTable) {
        const route = new CfnRoute(scope, routeInfo.id, {
            routeTableId: routeTable.ref,
            destinationCidrBlock: routeInfo.destinationCidrBlock
        });

        if (routeInfo.gatewayId) {
            route.gatewayId = routeInfo.gatewayId();
        } else if (routeInfo.natGatewayId) {
            route.natGatewayId = routeInfo.natGatewayId();
        }
    }

    private createAssociation(scope: cdk.Construct, associationInfo: AssociationInfo, routeTable: CfnRouteTable) {
        new CfnSubnetRouteTableAssociation(scope, associationInfo.id, {
            routeTableId: routeTable.ref,
            subnetId: associationInfo.subnetId()
        });
    }
}

長くなってしまった。が、少し複雑な依存関係なので仕方なし。
例によってリソース情報は ResourceInfo 型の配列にまとめて宣言しています。

今回が初の構成なのですが、この配列の中に更に別のリソース情報が入れ子になっています。それら ルート関連付け の情報をそれぞれ RouteInfo, AssociationInfo というインタフェースで定義しました。

そのコードがこちらです。

interface RouteInfo {
    readonly id: string;
    readonly destinationCidrBlock: string;
    readonly gatewayId?: () => string;
    readonly natGatewayId?: () => string;
}

interface AssociationInfo {
    readonly id: string;
    readonly subnetId: () => string;
}

interface ResourceInfo {
    readonly id: string;
    readonly resourceName: string;
    readonly routes: RouteInfo[];
    readonly associations: AssociationInfo[];
    readonly assign: (routeTable: CfnRouteTable) => void;
}

あとはいつものようにそれぞれリソースを生成するためのメソッドを作成し、ループ処理を行っています。

少し特殊な処理を実行しているのが以下のハイライト部分です。

private createRoute(scope: cdk.Construct, routeInfo: RouteInfo, routeTable: CfnRouteTable) {
    const route = new CfnRoute(scope, routeInfo.id, {
        routeTableId: routeTable.ref,
        destinationCidrBlock: routeInfo.destinationCidrBlock
    });

    if (routeInfo.gatewayId) {
        route.gatewayId = routeInfo.gatewayId();
    } else if (routeInfo.natGatewayId) {
        route.natGatewayId = routeInfo.natGatewayId();
    }
}

CfnRoute でターゲットを指定する場合、インターネットゲートウェイならば gatewayId を、NAT ゲートウェイならば natGatewayId を設定する必要があります。よってここでは先に必須項目を指定して CfnRoute インスタンスを生成してやり、その後リソース情報に応じてそれらの ID をインスタンスのプロパティに設定するという作りになっています。

このため RouteInfo が持つこれらのプロパティは Optional としています。

interface RouteInfo {
    readonly id: string;
    readonly destinationCidrBlock: string;
    readonly gatewayId?: () => string;
    readonly natGatewayId?: () => string;
}

メインのプログラムはこちら。
ハイライト部分を追記しました。

lib/devio-stack.ts

import * as cdk from '@aws-cdk/core';
import { Vpc } from './resource/vpc';
import { Subnet } from './resource/subnet';
import { InternetGateway } from './resource/internetGateway';
import { ElasticIp } from './resource/elasticIp';
import { NatGateway } from './resource/natGateway';
import { RouteTable } from './resource/routeTable';

export class DevioStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new Vpc();
    vpc.createResources(this);

    // Subnet
    const subnet = new Subnet(vpc.vpc);
    subnet.createResources(this);

    // Internet Gateway
    const internetGateway = new InternetGateway(vpc.vpc);
    internetGateway.createResources(this);

    // Elastic IP
    const elasticIp = new ElasticIp();
    elasticIp.createResources(this);

    // NAT Gateway
    const natGateway = new NatGateway(
      subnet.public1a,
      subnet.public1c,
      elasticIp.ngw1a,
      elasticIp.ngw1c
    );
    natGateway.createResources(this);

    // Route Table
    const routeTable = new RouteTable(
      vpc.vpc,
      subnet.public1a,
      subnet.public1c,
      subnet.app1a,
      subnet.app1c,
      subnet.db1a,
      subnet.db1c,
      internetGateway.igw,
      natGateway.ngw1a,
      natGateway.ngw1c
    );
    routeTable.createResources(this);
  }
}

テスト

テストコードはこちら。

test/resource/routeTable.test.ts

import { expect, countResources, haveResource, anything } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as Devio from '../../lib/devio-stack';

test('RouteTable', () => {
    const app = new cdk.App();
    const stack = new Devio.DevioStack(app, 'DevioStack');

    expect(stack).to(countResources('AWS::EC2::RouteTable', 4));
    expect(stack).to(haveResource('AWS::EC2::RouteTable', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-rtb-public' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::RouteTable', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-rtb-app-1a' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::RouteTable', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-rtb-app-1c' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::RouteTable', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-rtb-db' }]
    }));

    expect(stack).to(countResources('AWS::EC2::Route', 3));
    expect(stack).to(haveResource('AWS::EC2::Route', {
        RouteTableId: anything(),
        DestinationCidrBlock: '0.0.0.0/0',
        GatewayId: anything()
    }));
    expect(stack).to(haveResource('AWS::EC2::Route', {
        RouteTableId: anything(),
        DestinationCidrBlock: '0.0.0.0/0',
        NatGatewayId: anything()
    }));

    expect(stack).to(countResources('AWS::EC2::SubnetRouteTableAssociation', 6));
    expect(stack).to(haveResource('AWS::EC2::SubnetRouteTableAssociation', {
        RouteTableId: anything(),
        SubnetId: anything()
    }));
});

以下を確認しています。

  • ルートテーブルリソースが 4 つあること
  • ルートリソースが 3 つあること
  • サブネットとルートテーブルの関連付けリソースが 6 つあること
  • 各リソースのプロパティが正しいこと

また、今回から anything() というプロパティ比較用の関数を利用しています。(便利そうなの見つけました!)
これまでは他のリソースの値を参照する部分に関してはテストを書いていなかったのですが、この関数を使うことにより プロパティが設定されていること をテストすることができます。anything() を指定した場合、値がどんなものでもテストは通ります。

確認

マネジメントコンソール上でリソースを確認してみましょう。

2

上手にできました。

ルートやサブネットの関連付けもバッチリです。

3

4

CloudFormation 版

今回のコードを CFn で書くと以下のようになります。

RouteTablePublic:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-rtb-public
RoutePublic:
  Type: AWS::EC2::Route
  Properties:
    RouteTableId:
      Ref: RouteTablePublic
    DestinationCidrBlock: 0.0.0.0/0
    GatewayId:
      Ref: InternetGateway
AssociationPublic1a:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId:
      Ref: RouteTablePublic
    SubnetId:
      Ref: SubnetPublic1a
AssociationPublic1c:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId:
      Ref: RouteTablePublic
    SubnetId:
      Ref: SubnetPublic1c
RouteTableApp1a:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-rtb-app-1a
RouteApp1a:
  Type: AWS::EC2::Route
  Properties:
    RouteTableId:
      Ref: RouteTableApp1a
    DestinationCidrBlock: 0.0.0.0/0
    NatGatewayId:
      Ref: NatGateway1a
AssociationApp1a:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId:
      Ref: RouteTableApp1a
    SubnetId:
      Ref: SubnetApp1a
RouteTableApp1c:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-rtb-app-1c
RouteApp1c:
  Type: AWS::EC2::Route
  Properties:
    RouteTableId:
      Ref: RouteTableApp1c
    DestinationCidrBlock: 0.0.0.0/0
    NatGatewayId:
      Ref: NatGateway1c
AssociationApp1c:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId:
      Ref: RouteTableApp1c
    SubnetId:
      Ref: SubnetApp1c
RouteTableDb:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-rtb-db
AssociationDb1a:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId:
      Ref: RouteTableDb
    SubnetId:
      Ref: SubnetDb1a
AssociationDb1c:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId:
      Ref: RouteTableDb
    SubnetId:
      Ref: SubnetDb1c

過去最長です。合計 13 のリソースを定義しています。

GitHub

今回のソースコードは コチラ です。

おわりに

今回のプログラミングがこれまでで一番楽しかったです。まだ改善の余地はあるかもしれませんが、複雑なデータ構造をうまくコードに落とし込めたのではないかと思います。

次回作成するリソースは ネットワーク ACL です。

これが終わればネットワークまわりは一段落。

リンク