CDK for TerraformでWebシステムを構築してみた

記事タイトルとURLをコピーする

こんにちは、マネージドサービス部テクニカルサポート課の坂口です。
最近、AWSのCDK for Terraform(以下、CDKTF)を使用してWebシステムを構築することに挑戦しました。

はじめに

CDKTFは、AWSのインフラストラクチャを管理するためのツールであり、AWS CDKとTerraformの機能を組み合わせたものです。

developer.hashicorp.com

この記事では、CDKTFを使用してWebシステムを構築する方法について説明します。

環境

  • macOS Ventura
  • CDKTF: 0.15.5
  • Node.js: 18.12.1
  • Typescript: 4.9.5

構成図

CDKTFのインストール

公式ドキュメントに沿って必要なツールをインストールします。

developer.hashicorp.com

 npm install --global cdktf-cli@latest

プロジェクトの作成

こちらも公式ドキュメントに沿ってプロジェクトの作成を行っていきます。

developer.hashicorp.com

mkdir cdktf
cd cdktf
cdktf init --template="typescript" --providers="aws@~>4.0"

コマンド実行後の入力項目は下記になります。

Project Name: プロジェクト名

Project Description: プロジェクト概要

Would you like to use Terraform Cloud?: tfstateファイルの格納場所にTerraform Cloudを利用有無
※今回はS3バケットに格納するため、「n」を入力

Do you want to start from an existing Terraform project?: 既存Terraformプロジェクトの使用有無
※今回は新規作成のため、「n」を入力

Do you want to send crash reports to the CDKTF team?: クラッシュレポートの送信有無

Package installed.が表示されれば完了です。

リソースの構築

プロジェクト作成時に生成されるmain.tsに各リソースを追加していきます。
まずは、コード全体を記載しその後に各リソースの説明を行います。

事前作業

今回、tfstateファイルはS3で管理するため、事前にS3バケットの作成が必要です。
tfstateファイルとは、Terraformが管理するリソースの状態を表すファイルになります。

S3バケットの作成
  1. AWSマネジメントコンソールからS3のダッシュボードを開きます。
  2. 「バケットを作成」を押下します。
  3. 「バケット名」に任意の名前を入力します。
  4. 「バケットを作成」を押下します。

各リソースの作成

完成物

行数多いので折りたたんでいます。

コード全体を見る

import { Construct } from "constructs";
import { App, TerraformStack, S3Backend } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Vpc } from "@cdktf/provider-aws/lib/vpc";
import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway";
import { Subnet } from "@cdktf/provider-aws/lib/subnet";
import { Eip } from "@cdktf/provider-aws/lib/eip";
import { NatGateway } from "@cdktf/provider-aws/lib/nat-gateway";
import { RouteTable } from "@cdktf/provider-aws/lib/route-table";
import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";
import { IamRole } from "@cdktf/provider-aws/lib/iam-role";
import { DataAwsAmi } from "@cdktf/provider-aws/lib/data-aws-ami";
import { Instance } from "@cdktf/provider-aws/lib/instance";
import { Alb } from "@cdktf/provider-aws/lib/alb";
import { AlbTargetGroup } from "@cdktf/provider-aws/lib/alb-target-group";
import { AlbTargetGroupAttachment } from "@cdktf/provider-aws/lib/alb-target-group-attachment";
import { AlbListener } from "@cdktf/provider-aws/lib/alb-listener";
import { DbSubnetGroup } from "@cdktf/provider-aws/lib/db-subnet-group";
import { DbInstance } from "@cdktf/provider-aws/lib/db-instance";
import { IamInstanceProfile } from "@cdktf/provider-aws/lib/iam-instance-profile";
import { VpcEndpoint } from "@cdktf/provider-aws/lib/vpc-endpoint";

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new AwsProvider(this, "AWS", {
      region: "ap-northeast-1",
    });

    const vpc = new Vpc(this, "vpc", {
      cidrBlock: "10.1.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      tags: {
        ["Name"]: "cdktf-vpc",
      },
    });

    const internetGateway = new InternetGateway(this, "internetGateway", {
      vpcId: vpc.id,
      tags: {
        ["Name"]: "cdktf-igw",
      },
    });

    const publicSubnet1a = new Subnet(this, "publicSubnet1a", {
      vpcId: vpc.id,
      cidrBlock: "10.1.1.0/24",
      availabilityZone: "ap-northeast-1a",
      mapPublicIpOnLaunch: true,
      tags: {
        ["Name"]: "cdktf-public-sub-1a",
      },
    });

    const publicSubnet1c = new Subnet(this, "publicSubnet1c", {
      vpcId: vpc.id,
      cidrBlock: "10.1.2.0/24",
      availabilityZone: "ap-northeast-1c",
      mapPublicIpOnLaunch: true,
      tags: {
        ["Name"]: "cdktf-public-sub-1c",
      },
    });

    const privateSubnet1a = new Subnet(this, "privateSubnet1a", {
      vpcId: vpc.id,
      cidrBlock: "10.1.3.0/24",
      availabilityZone: "ap-northeast-1a",
      tags: {
        ["Name"]: "cdktf-private-sub-1a",
      },
    });

    const privateSubnet1c = new Subnet(this, "privateSubnet1c", {
      vpcId: vpc.id,
      cidrBlock: "10.1.4.0/24",
      availabilityZone: "ap-northeast-1c",
      tags: {
        ["Name"]: "cdktf-private-sub-1c",
      },
    });

    const dbSubnet1a = new Subnet(this, "dbSubnet1a", {
      vpcId: vpc.id,
      cidrBlock: "10.1.5.0/24",
      availabilityZone: "ap-northeast-1a",
      tags: {
        ["Name"]: "cdktf-db-sub-1a",
      },
    });

    const dbSubnet1c = new Subnet(this, "dbSubnet1c", {
      vpcId: vpc.id,
      cidrBlock: "10.1.6.0/24",
      availabilityZone: "ap-northeast-1c",
      tags: {
        ["Name"]: "cdktf-db-sub-1c",
      },
    });

    const eipNatGateway1a = new Eip(this, "eipNatGateway1a", {
      tags: {
        ["Name"]: "cdktf-eip-ngw-1a",
      },
    });

    const eipNatGateway1c = new Eip(this, "eipNatGateway1c", {
      tags: {
        ["Name"]: "cdktf-eip-ngw-1c",
      },
    });

    const natGateway1a = new NatGateway(this, "natGateway1a", {
      subnetId: publicSubnet1a.id,
      allocationId: eipNatGateway1a.allocationId,
      tags: {
        ["Name"]: "cdktf-ngw-1a",
      },
    });

    const natGateway1c = new NatGateway(this, "natGateway1c", {
      subnetId: publicSubnet1c.id,
      allocationId: eipNatGateway1c.allocationId,
      tags: {
        ["Name"]: "cdktf-ngw-1c",
      },
    });

    const routeTablePublicSubnet1a = new RouteTable(this, "routeTablePublicSubnet1a", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: internetGateway.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-public-sub-1a",
      },
    });

    const routeTablePublicSubnet1c = new RouteTable(this, "routeTablePublicSubnet1c", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: internetGateway.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-public-sub-1c",
      },
    });

    const routeTablePrivateSubnet1a = new RouteTable(this, "routeTablePrivateSubnet1a", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        natGatewayId: natGateway1a.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-private-sub-1a",
      },
    });

    const routeTablePrivateSubnet1c = new RouteTable(this, "routeTablePrivateSubnet1c", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        natGatewayId: natGateway1c.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-private-sub-1c",
      },
    });

    const routeTableDBSubnet1a = new RouteTable(this, "routeTableDBSubnet1a", {
      vpcId: vpc.id,
      tags: {
        ["Name"]: "cdktf-rt-db-sub-1a",
      },
    });

    const routeTableDBSubnet1c = new RouteTable(this, "routeTableDBSubnet1c", {
      vpcId: vpc.id,
      tags: {
        ["Name"]: "cdktf-rt-db-sub-1c",
      },
    });

    new RouteTableAssociation(this, "routeTableAssociationPublicSubnet1a", {
      routeTableId: routeTablePublicSubnet1a.id,
      subnetId: publicSubnet1a.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationPublicSubnet1c", {
      routeTableId: routeTablePublicSubnet1c.id,
      subnetId: publicSubnet1c.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationPrivateSubnet1a", {
      routeTableId: routeTablePrivateSubnet1a.id,
      subnetId: privateSubnet1a.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationPrivateSubnet1c", {
      routeTableId: routeTablePrivateSubnet1c.id,
      subnetId: privateSubnet1c.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationDBSubnet1a", {
      routeTableId: routeTableDBSubnet1a.id,
      subnetId: dbSubnet1a.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationDBSubnet1c", {
      routeTableId: routeTableDBSubnet1c.id,
      subnetId: dbSubnet1c.id,
    });

    const securityGroupAlb = new SecurityGroup(this, "securityGroupAlb", {
      name: "cdktf-sg-alb",
      description: "cdktf-sg-alb",
      vpcId: vpc.id,
      ingress: [{
        description: "HTTP from Internet",
        fromPort: 80,
        toPort: 80,
        protocol: "tcp",
        cidrBlocks: ["119.47.193.2/32"],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-alb",
      },
    });

    const securityGroupEc2 = new SecurityGroup(this, "securityGroupEc2", {
      name: "cdktf-sg-ec2",
      description: "cdktf-sg-ec2",
      vpcId: vpc.id,
      ingress: [{
        description: "HTTP from ALB",
        fromPort: 80,
        toPort: 80,
        protocol: "tcp",
        securityGroups: [securityGroupAlb.id],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-ec2",
      },
    });

    const securityGroupRds = new SecurityGroup(this, "securityGroupRds", {
      name: "cdktf-sg-rds",
      description: "cdktf-sg-rds",
      vpcId: vpc.id,
      ingress: [{
        description: "MySQL from EC2",
        fromPort: 3306,
        toPort: 3306,
        protocol: "tcp",
        securityGroups: [securityGroupEc2.id],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-rds",
      },
    });

    const securityGroupVpcEndpoint = new SecurityGroup(this, "securityGroupVpcEndpoint", {
      name: "cdktf-sg-vpc-endpoint",
      description: "cdktf-sg-vpc-endpoint",
      vpcId: vpc.id,
      ingress: [{
        description: "HTTPS from Private Subnet",
        fromPort: 443,
        toPort: 443,
        protocol: "tcp",
        cidrBlocks: [privateSubnet1a.cidrBlock,privateSubnet1c.cidrBlock],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-vpc-endpoint",
      },
    });

    const ec2Role = new IamRole(this, "ec2Role", {
      name: "cdktf-role-ec2",
      assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [
          {
            Action: "sts:AssumeRole",
            Effect: "Allow",
            Principal: {
              Service: "ec2.amazonaws.com",
            },
          },
        ],
      }),
      managedPolicyArns: ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"],
    });

    const ec2InstancePolicy = new IamInstanceProfile(this, "ec2InstanceProfile", {
      name: "cdktf-instance-profile-ec2",
      role: ec2Role.name,
    });

    const mostRecentAmi = new DataAwsAmi(this, "mostRecentAmi", {
      mostRecent: true,
      owners: ["amazon"],
      filter: [
        { name: "name", values: ["amzn2-ami-hvm-*-x86_64-gp2"] },
      ],
    });

    const ec2Instance = new Instance(this, "ec2Instance", {
      ami: mostRecentAmi.id,
      instanceType: "t3.nano",
      subnetId: privateSubnet1a.id,
      vpcSecurityGroupIds: [securityGroupEc2.id],
      iamInstanceProfile: ec2InstancePolicy.name,
      userDataBase64: "IyEvYmluL2Jhc2gNCg0KIyMgbmdpbnggaW5zdGFsbA0Kc3VkbyBhbWF6b24tbGludXgtZXh0cmFzIGluc3RhbGwgbmdpbngxDQpzdWRvIHN5c3RlbWN0bCBzdGFydCBuZ2lueA0Kc3VkbyBzeXN0ZW1jdGwgZW5hYmxlIG5naW54DQoNCiMjbXlzcWwtY2xpZW50IGluc3RhbGwNCnN1ZG8geXVtIHJlbW92ZSBtYXJpYWRiLWxpYnMNCnN1ZG8geXVtIGxvY2FsaW5zdGFsbCAteSBodHRwczovL2Rldi5teXNxbC5jb20vZ2V0L215c3FsODAtY29tbXVuaXR5LXJlbGVhc2UtZWw3LTMubm9hcmNoLnJwbQ0Kc3VkbyB5dW0tY29uZmlnLW1hbmFnZXIgLS1kaXNhYmxlIG15c3FsNTctY29tbXVuaXR5DQpzdWRvIHl1bS1jb25maWctbWFuYWdlciAtLWVuYWJsZSBteXNxbDgwLWNvbW11bml0eQ0Kc3VkbyBycG0gLS1pbXBvcnQgaHR0cHM6Ly9yZXBvLm15c3FsLmNvbS9SUE0tR1BHLUtFWS1teXNxbC0yMDIyDQpzdWRvIHl1bSBpbnN0YWxsIC15IG15c3FsLWNvbW11bml0eS1jbGllbnQ=",
      tags: {
        ["Name"]: "cdktf-ec2",
      },
    });

    const alb = new Alb(this, "alb", {
      name: "cdktf-alb-ec2",
      securityGroups: [securityGroupAlb.id],
      subnets: [publicSubnet1a.id,publicSubnet1c.id],
    });

    const albTargetGroup = new AlbTargetGroup(this, "albTargetGroup", {
      name: "cdktf-alb-ec2-tg",
      port: 80,
      protocol: "HTTP",
      vpcId: vpc.id,
    });

    new AlbTargetGroupAttachment(this, "albTargetGroupAttach", {
      targetGroupArn: albTargetGroup.arn,
      targetId: ec2Instance.id,
      port: 80,
    });

    new AlbListener(this, "albListener", {
      loadBalancerArn: alb.arn,
      port: 80,
      protocol: "HTTP",
      defaultAction: [
        {
          targetGroupArn: albTargetGroup.arn,
          type: "forward",
        },
      ],
    });

    const rdsSubnetGroup = new DbSubnetGroup(this, "rdsSubnetGroup", {
      name: "cdktf-rds-subnet-group",
      subnetIds: [dbSubnet1a.id,dbSubnet1c.id]
    });

    new DbInstance(this, "rdsInstance", {
      identifier: "cdktf-rds-instance",
      allocatedStorage: 40,
      instanceClass: "db.t3.micro",
      engine: "mysql",
      engineVersion: "5.7",
      dbName: "test",
      username: "root",
      password: "password",
      parameterGroupName: "default.mysql5.7",
      dbSubnetGroupName: rdsSubnetGroup.name,
      vpcSecurityGroupIds: [securityGroupRds.id],
      skipFinalSnapshot: true,
      tags: {
        ["Name"]: "cdktf-rds-instance",
      },
    });

    new VpcEndpoint(this, "ssm", {
      vpcId: vpc.id,
      serviceName: "com.amazonaws.ap-northeast-1.ssm",
      vpcEndpointType: "Interface",
      privateDnsEnabled: true,
      subnetIds: [privateSubnet1a.id,privateSubnet1c.id],
      securityGroupIds: [securityGroupVpcEndpoint.id],
      tags: {
        ["Name"]: "cdktf-vpc-endpoint-ssm",
      },
    });

    new VpcEndpoint(this, "ssmmessages", {
      vpcId: vpc.id,
      serviceName: "com.amazonaws.ap-northeast-1.ssmmessages",
      vpcEndpointType: "Interface",
      privateDnsEnabled: true,
      subnetIds: [privateSubnet1a.id,privateSubnet1c.id],
      securityGroupIds: [securityGroupVpcEndpoint.id],
      tags: {
        ["Name"]: "cdktf-vpc-endpoint-ssmmessages",
      },
    });
  }
}

const app = new App();
const stack = new MyStack(app, "cdktf");

new S3Backend(stack, {
  bucket: <作成したS3バケット>,
  key: "cdktf.tfstate",
  region: "ap-northeast-1",
});

app.synth();

VPC

10.1.0.0/16のVPCを作成します。

    const vpc = new Vpc(this, "vpc", {
      cidrBlock: "10.1.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      tags: {
        ["Name"]: "cdktf-vpc",
      },
    });
InternetGateway

InternetGatewayを作成します。

    const internetGateway = new InternetGateway(this, "internetGateway", {
      vpcId: vpc.id,
      tags: {
        ["Name"]: "cdktf-igw",
      },
    });
Subnet

構成図に記載の通り、2Az,3Subnetを作成します。
CIDRは/24で分割し、第3オクテットを1から順番に割り当てています。

    const publicSubnet1a = new Subnet(this, "publicSubnet1a", {
      vpcId: vpc.id,
      cidrBlock: "10.1.1.0/24",
      availabilityZone: "ap-northeast-1a",
      mapPublicIpOnLaunch: true,
      tags: {
        ["Name"]: "cdktf-public-sub-1a",
      },
    });

    const publicSubnet1c = new Subnet(this, "publicSubnet1c", {
      vpcId: vpc.id,
      cidrBlock: "10.1.2.0/24",
      availabilityZone: "ap-northeast-1c",
      mapPublicIpOnLaunch: true,
      tags: {
        ["Name"]: "cdktf-public-sub-1c",
      },
    });

    const privateSubnet1a = new Subnet(this, "privateSubnet1a", {
      vpcId: vpc.id,
      cidrBlock: "10.1.3.0/24",
      availabilityZone: "ap-northeast-1a",
      tags: {
        ["Name"]: "cdktf-private-sub-1a",
      },
    });

    const privateSubnet1c = new Subnet(this, "privateSubnet1c", {
      vpcId: vpc.id,
      cidrBlock: "10.1.4.0/24",
      availabilityZone: "ap-northeast-1c",
      tags: {
        ["Name"]: "cdktf-private-sub-1c",
      },
    });

    const dbSubnet1a = new Subnet(this, "dbSubnet1a", {
      vpcId: vpc.id,
      cidrBlock: "10.1.5.0/24",
      availabilityZone: "ap-northeast-1a",
      tags: {
        ["Name"]: "cdktf-db-sub-1a",
      },
    });

    const dbSubnet1c = new Subnet(this, "dbSubnet1c", {
      vpcId: vpc.id,
      cidrBlock: "10.1.6.0/24",
      availabilityZone: "ap-northeast-1c",
      tags: {
        ["Name"]: "cdktf-db-sub-1c",
      },
    });
NATGateway

先にNATGatewayに付与するEIPを作成します。
その後作成したEIPを参照してNATGatewayを作成します。

    const eipNatGateway1a = new Eip(this, "eipNatGateway1a", {
      tags: {
        ["Name"]: "cdktf-eip-ngw-1a",
      },
    });

    const eipNatGateway1c = new Eip(this, "eipNatGateway1c", {
      tags: {
        ["Name"]: "cdktf-eip-ngw-1c",
      },
    });

    const natGateway1a = new NatGateway(this, "natGateway1a", {
      subnetId: publicSubnet1a.id,
      allocationId: eipNatGateway1a.allocationId,
      tags: {
        ["Name"]: "cdktf-ngw-1a",
      },
    });

    const natGateway1c = new NatGateway(this, "natGateway1c", {
      subnetId: publicSubnet1c.id,
      allocationId: eipNatGateway1c.allocationId,
      tags: {
        ["Name"]: "cdktf-ngw-1c",
      },
    });
RouteTable

各RouteTableの用途は下記になります。

  • routeTablePublicSubnet: インターネットへのルーティング
  • routeTablePrivateSubnet: NATGatewayを経由するインターネットへのルーティング
  • routeTableDBSubnet: VPC内へのルーティング(デフォルト)
    const routeTablePublicSubnet1a = new RouteTable(this, "routeTablePublicSubnet1a", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: internetGateway.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-public-sub-1a",
      },
    });

    const routeTablePublicSubnet1c = new RouteTable(this, "routeTablePublicSubnet1c", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: internetGateway.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-public-sub-1c",
      },
    });

    const routeTablePrivateSubnet1a = new RouteTable(this, "routeTablePrivateSubnet1a", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        natGatewayId: natGateway1a.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-private-sub-1a",
      },
    });

    const routeTablePrivateSubnet1c = new RouteTable(this, "routeTablePrivateSubnet1c", {
      vpcId: vpc.id,
      route: [{
        cidrBlock: "0.0.0.0/0",
        natGatewayId: natGateway1c.id,
      }],
      tags: {
        ["Name"]: "cdktf-rt-private-sub-1c",
      },
    });

    const routeTableDBSubnet1a = new RouteTable(this, "routeTableDBSubnet1a", {
      vpcId: vpc.id,
      tags: {
        ["Name"]: "cdktf-rt-db-sub-1a",
      },
    });

    const routeTableDBSubnet1c = new RouteTable(this, "routeTableDBSubnet1c", {
      vpcId: vpc.id,
      tags: {
        ["Name"]: "cdktf-rt-db-sub-1c",
      },
    });

    new RouteTableAssociation(this, "routeTableAssociationPublicSubnet1a", {
      routeTableId: routeTablePublicSubnet1a.id,
      subnetId: publicSubnet1a.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationPublicSubnet1c", {
      routeTableId: routeTablePublicSubnet1c.id,
      subnetId: publicSubnet1c.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationPrivateSubnet1a", {
      routeTableId: routeTablePrivateSubnet1a.id,
      subnetId: privateSubnet1a.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationPrivateSubnet1c", {
      routeTableId: routeTablePrivateSubnet1c.id,
      subnetId: privateSubnet1c.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationDBSubnet1a", {
      routeTableId: routeTableDBSubnet1a.id,
      subnetId: dbSubnet1a.id,
    });

    new RouteTableAssociation(this, "routeTableAssociationDBSubnet1c", {
      routeTableId: routeTableDBSubnet1c.id,
      subnetId: dbSubnet1c.id,
    });
SecurityGroup

<ご自身のグローバルIPアドレス>にHTTPアクセス元のIPアドレスを記載してください。
※インバウンドで必要な通信を許可、アウトバウンドでは全通信を許可にしています。

    const securityGroupAlb = new SecurityGroup(this, "securityGroupAlb", {
      name: "cdktf-sg-alb",
      description: "cdktf-sg-alb",
      vpcId: vpc.id,
      ingress: [{
        description: "HTTP from Internet",
        fromPort: 80,
        toPort: 80,
        protocol: "tcp",
        cidrBlocks: [<ご自身のグローバルIPアドレス>],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-alb",
      },
    });

    const securityGroupEc2 = new SecurityGroup(this, "securityGroupEc2", {
      name: "cdktf-sg-ec2",
      description: "cdktf-sg-ec2",
      vpcId: vpc.id,
      ingress: [{
        description: "HTTP from ALB",
        fromPort: 80,
        toPort: 80,
        protocol: "tcp",
        securityGroups: [securityGroupAlb.id],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-ec2",
      },
    });

    const securityGroupRds = new SecurityGroup(this, "securityGroupRds", {
      name: "cdktf-sg-rds",
      description: "cdktf-sg-rds",
      vpcId: vpc.id,
      ingress: [{
        description: "MySQL from EC2",
        fromPort: 3306,
        toPort: 3306,
        protocol: "tcp",
        securityGroups: [securityGroupEc2.id],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-rds",
      },
    });

    const securityGroupVpcEndpoint = new SecurityGroup(this, "securityGroupVpcEndpoint", {
      name: "cdktf-sg-vpc-endpoint",
      description: "cdktf-sg-vpc-endpoint",
      vpcId: vpc.id,
      ingress: [{
        description: "HTTPS from Private Subnet",
        fromPort: 443,
        toPort: 443,
        protocol: "tcp",
        cidrBlocks: [privateSubnet1a.cidrBlock,privateSubnet1c.cidrBlock],
      }],
      egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
      }],
      tags: {
        ["Name"]: "cdktf-sg-vpc-endpoint",
      },
    });
EC2

SSM経由でEC2インスタンスに接続する構成のため、SSMを許可したロールをインスタンスプロファイルとして使用します。
ユーザデータにて、nginxとmysql-clientのインストールを実施しています。
最新のAMIを使用するため、Data Sourcesを使い最新のAMIを取得しています。

参考資料

developer.hashicorp.com

    const ec2Role = new IamRole(this, "ec2Role", {
      name: "cdktf-role-ec2",
      assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [
          {
            Action: "sts:AssumeRole",
            Effect: "Allow",
            Principal: {
              Service: "ec2.amazonaws.com",
            },
          },
        ],
      }),
      managedPolicyArns: ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"],
    });

    const ec2InstancePolicy = new IamInstanceProfile(this, "ec2InstanceProfile", {
      name: "cdktf-instance-profile-ec2",
      role: ec2Role.name,
    });

    const mostRecentAmi = new DataAwsAmi(this, "mostRecentAmi", {
      mostRecent: true,
      owners: ["amazon"],
      filter: [
        { name: "name", values: ["amzn2-ami-hvm-*-x86_64-gp2"] },
      ],
    });

    const ec2Instance = new Instance(this, "ec2Instance", {
      ami: mostRecentAmi.id,
      instanceType: "t3.nano",
      subnetId: privateSubnet1a.id,
      vpcSecurityGroupIds: [securityGroupEc2.id],
      iamInstanceProfile: ec2InstancePolicy.name,
      userDataBase64: "IyEvYmluL2Jhc2gNCg0KIyMgbmdpbnggaW5zdGFsbA0Kc3VkbyBhbWF6b24tbGludXgtZXh0cmFzIGluc3RhbGwgbmdpbngxDQpzdWRvIHN5c3RlbWN0bCBzdGFydCBuZ2lueA0Kc3VkbyBzeXN0ZW1jdGwgZW5hYmxlIG5naW54DQoNCiMjbXlzcWwtY2xpZW50IGluc3RhbGwNCnN1ZG8geXVtIHJlbW92ZSBtYXJpYWRiLWxpYnMNCnN1ZG8geXVtIGxvY2FsaW5zdGFsbCAteSBodHRwczovL2Rldi5teXNxbC5jb20vZ2V0L215c3FsODAtY29tbXVuaXR5LXJlbGVhc2UtZWw3LTMubm9hcmNoLnJwbQ0Kc3VkbyB5dW0tY29uZmlnLW1hbmFnZXIgLS1kaXNhYmxlIG15c3FsNTctY29tbXVuaXR5DQpzdWRvIHl1bS1jb25maWctbWFuYWdlciAtLWVuYWJsZSBteXNxbDgwLWNvbW11bml0eQ0Kc3VkbyBycG0gLS1pbXBvcnQgaHR0cHM6Ly9yZXBvLm15c3FsLmNvbS9SUE0tR1BHLUtFWS1teXNxbC0yMDIyDQpzdWRvIHl1bSBpbnN0YWxsIC15IG15c3FsLWNvbW11bml0eS1jbGllbnQ=",
      tags: {
        ["Name"]: "cdktf-ec2",
      },
    });
ALB

ALBでSSL終端する場合、別途、ACMにて証明書発行およびHTTPS用のALB Listenerルールを追加する必要があります。

    const alb = new Alb(this, "alb", {
      name: "cdktf-alb-ec2",
      securityGroups: [securityGroupAlb.id],
      subnets: [publicSubnet1a.id,publicSubnet1c.id],
    });

    const albTargetGroup = new AlbTargetGroup(this, "albTargetGroup", {
      name: "cdktf-alb-ec2-tg",
      port: 80,
      protocol: "HTTP",
      vpcId: vpc.id,
    });

    new AlbTargetGroupAttachment(this, "albTargetGroupAttach", {
      targetGroupArn: albTargetGroup.arn,
      targetId: ec2Instance.id,
      port: 80,
    });

    new AlbListener(this, "albListener", {
      loadBalancerArn: alb.arn,
      port: 80,
      protocol: "HTTP",
      defaultAction: [
        {
          targetGroupArn: albTargetGroup.arn,
          type: "forward",
        },
      ],
    });
RDS

skipFinalSnapshot: trueにしないとリソース削除時にエラーが出ます。 skipFinalSnapshotとは、リソース削除時のスナップショット取得をスキップするパラメータになります。

参考資料 registry.terraform.io

    const rdsSubnetGroup = new DbSubnetGroup(this, "rdsSubnetGroup", {
      name: "cdktf-rds-subnet-group",
      subnetIds: [dbSubnet1a.id,dbSubnet1c.id]
    });

    new DbInstance(this, "rdsInstance", {
      identifier: "cdktf-rds-instance",
      allocatedStorage: 40,
      instanceClass: "db.t3.micro",
      engine: "mysql",
      engineVersion: "5.7",
      dbName: "test",
      username: "root",
      password: "password",
      parameterGroupName: "default.mysql5.7",
      dbSubnetGroupName: rdsSubnetGroup.name,
      vpcSecurityGroupIds: [securityGroupRds.id],
      skipFinalSnapshot: true,
      tags: {
        ["Name"]: "cdktf-rds-instance",
      },
    });
VPC Endpoint

今回、Session Managerのみを使うためssmssmmessagesのエンドポイントを作成しています。
参考資料

aws.amazon.com

    new VpcEndpoint(this, "ssm", {
      vpcId: vpc.id,
      serviceName: "com.amazonaws.ap-northeast-1.ssm",
      vpcEndpointType: "Interface",
      privateDnsEnabled: true,
      subnetIds: [privateSubnet1a.id,privateSubnet1c.id],
      securityGroupIds: [securityGroupVpcEndpoint.id],
      tags: {
        ["Name"]: "cdktf-vpc-endpoint-ssm",
      },
    });

    new VpcEndpoint(this, "ssmmessages", {
      vpcId: vpc.id,
      serviceName: "com.amazonaws.ap-northeast-1.ssmmessages",
      vpcEndpointType: "Interface",
      privateDnsEnabled: true,
      subnetIds: [privateSubnet1a.id,privateSubnet1c.id],
      securityGroupIds: [securityGroupVpcEndpoint.id],
      tags: {
        ["Name"]: "cdktf-vpc-endpoint-ssmmessages",
      },
    });
tfstateファイルの保存先指定

bucketに事前作業で作成したS3バケット名を指定します。

new S3Backend(stack, {
  bucket: <作成したS3バケット>,
  key: "cdktf.tfstate",
  region: "ap-northeast-1",
});

デプロイ

cdktf deployコマンドでデプロイすることが可能です。
cdktf deploy実行後、想定されるリソースが表示されるため、問題なければApproveを選択しEnterを押下するとデプロイ処理が走ります。

まとめ

CDKTFを使用することで、Terraformのコードをプログラミング言語で記述することができます。
これにより、コードの再利用性や保守性が向上するだけでなく、プログラミング言語での開発に慣れている人にとても扱いやすくなります。
今回は、CDKTFを使用してAWSのWebシステムを構築する方法について紹介しました。
是非、実際に試してみてください。

坂口 大樹 (記事一覧)

マネージドサービス部テクニカルサポート課

2023年3月にサーバーワークス入社。

スパイスカレーが好きです。