🌎

AWSネットワーク構築を CloudFormation → CDKで“再現”する手順【初心者向け・IaC入門】

に公開

はじめに

私自身約1か月前からAWSを触り始め、ネットワーク構築をGUIから始めて、IaCでの構築の基礎はできるようになりました。
今回はAWS CDK(Cloud Development Kit)を利用したので、構築する過程で得た知識をアウトプットします。

以前にCloudFormationで同様の内容の構築をしたため比較も交えながら、AWS CDKを使用するメリット・使用前に押さえておきたかったポイントもご紹介していきます。

今回AWS CDKで作成した構成図

前回CloudFormationで作成した環境を、今回CDKで再現してみました。

VPC内にパブリックサブネットとプライベートサブネットを配置し、パブリックサブネットには外部公開用のEC2、プライベートサブネットにはDBを設置します。外部からのアクセスはELBを経由してEC2へ振り分けられる構成です。
画像

CloudFormationについて確認したい方やネットワークの基礎について、前回作成した記事に記載してますので、必要に応じて下記はご参照ください。

CloudFormationとAWS CDKの違いは?

AWS CDKの詳しい説明の前に、まずはCloudFormationと比較しながら概要に触れていきます。

【初心者向け】AWS CDK 入門!完全ガイドによると、両者の特徴は下記が挙げられています。

特徴 CloudFormation AWS CDK
記述形式 JSONやYAMLのみ TypeScript、Pythonなどのプログラミング言語が利用可能
定義方法 すべて明示的に記述する必要がある すべて明記する必要はなく、テンプレート生成時に自動設定される
コード量 構成が複雑になると増大する 自動生成や抽象化により抑えられる
開発支援 基本的になし IDEでのコード補完などが利用可能

上記のコード量に関する記述は私も使用して実感できた点なので、具体例としてVPCスタックを作成するファイルの内容を確認します。

前回CloudFormationで作成したymlファイルと、今回AWS CDKで作成したコード量を比較しました。
CloudFormationでは128行で構築した内容が、AWS CDKで書くと47行で済み、記載量を半分以上削減できました。

CloudFromationで作成したymlファイルの内容です。
AWSTemplateFormatVersion: 2010-09-09
Description: Hands-on template for VPC

Resources:
  CFnVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      InstanceTenancy: default
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: handson-cfn

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId: !Ref CFnVPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicSubnet1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId: !Ref CFnVPC
      AvailabilityZone: !Select [ 1, !GetAZs ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicSubnet2

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.2.0/24
      VpcId: !Ref CFnVPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      Tags:
        - Key: Name
          Value: PrivateSubnet1

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.3.0/24
      VpcId: !Ref CFnVPC
      AvailabilityZone: !Select [ 1, !GetAZs ]
      Tags:
        - Key: Name
          Value: PrivateSubnet2

  CFnVPCIGW:
    Type: AWS::EC2::InternetGateway
    Properties: 
      Tags:
        - Key: Name
          Value: handson-cfn

  CFnVPCIGWAttach:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      InternetGatewayId: !Ref CFnVPCIGW
      VpcId: !Ref CFnVPC

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CFnVPC
      Tags:
        - Key: Name
          Value: Public Route

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: CFnVPCIGW
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref CFnVPCIGW

  PublicSubnet1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

Outputs:
  VPCID:
    Description: VPC ID
    Value: !Ref CFnVPC
    Export:
      Name: !Sub ${AWS::StackName}-VPCID

  PublicSubnet1:
    Description: PublicSubnet1
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet1

  PublicSubnet2:
    Description: PublicSubnet2
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet2

  PrivateSubnet1:
    Description: PrivateSubnet1
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet1

  PrivateSubnet2:
    Description: PrivateSubnet2
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet2


コード量が削減できる仕組みや理由を把握していただけるよう、このあとAWS CDKの特徴をお伝えします。

AWS CDKとは

AWS CDKは、TypeScript、Pythonなどのプログラミング言語で、AWSリソースを定義できるオープンソースフレームワークです。

画像参照元:https://aws.amazon.com/jp/builders-flash/202309/awsgeek-aws-cdk/
こちらの参照元のサイトが一番概要を掴みやすかったです。

AWS CDKの構成要素について

  • AWS CDK の構成要素は主に「アプリケーション」「スタック」「コンストラクト」の 3つのレイヤーに分けられます。 (参照元:AWS builders.flash)

AWS CDKではアプリケーションがスタックを内包し、スタックの中でコンストラクトを組み合わせてインフラを構築していくという階層構造を持っています。

AWS CDKの核である「コンストラクト」は、1つあるいは複数のAWSリソースをまとめた部品です。このコンストラクトのまとまりが「スタック」で、デプロイ可能な最小単位です。そして、複数スタックの依存関係を定義する要素が「アプリケーション」です。

コンストラクトにはレベルがあり、詳しい説明は下記のように書かれていました。
画像参照元:https://blog.usize-tech.com/aws-cdk-construct/

私は最初コンストラクトの優位性が理解しきれておらず、AWS CDKの恩恵を享受しながらコードを書けていませんでした。コンストラクトの種類を理解したうえで、選定することでコード量の削減もできるようになったので、冒頭にお伝えしたポイントはコンストラクトを使いこなすことではないかと考えています。

AWSのドキュメントでは、S3バケットを例にしてコンストラクトの比較が行われていました。

手順概要

今回行った手順とともに、AWS CDKで使用する主なコマンドについても説明していきます。

AWS CDKの環境構築が完了していることを前提に、作業の流れを下記に記載します。

1. CDKプロジェクトの初期化

任意のフォルダ内で、必要なファイルをセットアップします。

  • mkdir tmp

  • cd tmp

  • cdk init app --language [language]

    • languageの部分は利用するプログラミング言語を指定

今回はcdk init app --language typescriptを実行し、ディレクトリとファイルが下記のように作成されました。

├── README.md
├── bin
│   └── tmp.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── tmp.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json

※ 対象アカウント・リージョンにつき、初回1回のみcdk bootstrapコマンドを実行してCDK環境の初期化が必要。

2. アプリケーションの使用を宣言する

まずはじめにbinディレクトリのtmp.tsで「アプリケーション」の使用を宣言します。

bin/tmp.ts
import * as cdk from 'aws-cdk-lib';

const app = new cdk.App();

3. VPCスタックを作成する

  • 次にVPCスタックの使用を宣言します。

画像

bin/tmp.ts
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from "../lib/vpc-stack"; // 追加

const app = new cdk.App();

// 追加:VPC
const vpcStack = new VpcStack(app, "SamplePjVpcStack");

vpc-stack.tsにスタックの詳細を記載していきます。

lib/vpc-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";

export class VpcStack extends cdk.Stack {
    public readonly vpc: ec2.Vpc; 

    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props); 

        // VPC
        this.vpc = new ec2.Vpc(this, "vpc", {
            ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
            maxAzs: 2,
            natGateways: 0, 
            subnetConfiguration: [
                {
                    name: `${id}-PublicSubnet`,
                    subnetType: ec2.SubnetType.PUBLIC,
                    cidrMask: 24,
                },
                {
                    name: `${id}-PrivateSubnet`,
                    subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
                    cidrMask: 24,
                },
            ], 
        });
    }
}
  • cdk diff {スタック名}で差分を確認し、必要に応じて修正していきます。

画像

  • スタックの内容が完成後、cdk deploy {スタック名}コマンドを実行します。

CloudFormationテンプレートが生成されたあと、デプロイ時にCloudFormationサービスへ渡されることで、AWSリソースが構築されます。
画像参照元:https://speakerdeck.com/konokenj/cdk-best-practice-2024?slide=8

4. RDSスタックを作成する

画像

bin/tmp.ts
import * as cdk from 'aws-cdk-lib';

import { VpcStack } from "../lib/vpc-stack";
import { RdsStack } from "../lib/rds-stack";// 追記

const app = new cdk.App();

// VPC
const vpcStack = new VpcStack(app, "SamplePjVpcStack");

// 追記:RDS
const rdsStack = new RdsStack(app, "SamplePjRdsStack", {
    vpc: vpcStack.vpc,
});

rds-stack.tsにスタックの詳細を記載します。

lib/rds-stack.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";

interface RdsStackProps extends cdk.StackProps {
    vpc: ec2.Vpc;
}

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

        const sg = new ec2.SecurityGroup(this, "RdsSg", {
            vpc: props.vpc,
            description: 'Allow MySQL access from within VPC',
            allowAllOutbound: true,
        });
        sg.addIngressRule(ec2.Peer.ipv4("10.0.0.0/16"), ec2.Port.tcp(3306), "MySQL access");

        const rds = new rds.DatabaseInstance(this, "AppRds", {
            vpc: props.vpc,
            engine: rds.DatabaseInstanceEngine.mysql({
                version: rds.MysqlEngineVersion.VER_8_0,
            }),
            instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO),
            credentials: rds.Credentials.fromPassword(
                "dbmaster", 
                cdk.SecretValue.unsafePlainText("H&ppyHands0n") 
            ),
            databaseName: "wordpress",
            allocatedStorage: 20,
            securityGroups: [sg],
            vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
            multiAz: false,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
        });
    }
}

記載後cdk diffコマンドを実行し、内容を確認して問題なければcdk deployコマンドを実行します。

5. EC2スタックを作成する

画像

tmp.tsにEC2スタックの詳細を記載します。

bin/tmp.ts
import * as cdk from 'aws-cdk-lib';

import { VpcStack } from "../lib/vpc-stack";
import { RdsStack } from "../lib/rds-stack";
import { Ec2Stack } from "../lib/ec2-stack";// 追記

const app = new cdk.App();

// VPC
const vpcStack = new VpcStack(app, "SamplePjVpcStack");

// RDS
const rdsStack = new RdsStack(app, "SamplePjRdsStack", {
    vpc: vpcStack.vpc,
});

// 追記:EC2
const ec2Stack = new Ec2Stack(app, "SamplePjEc2Stack", {
    vpc: vpcStack.vpc,
});

ec2-stack.tsにスタックの詳細を記載します。

lib/ec2-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";

interface Ec2StackProps extends cdk.StackProps {
    vpc: ec2.Vpc;
}

export class Ec2Stack extends cdk.Stack {
    public readonly instance: ec2.Instance;

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

        const role = new iam.Role(this, "EC2Role", {
            assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
            managedPolicies: [
                iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
            ],
        });

        const sg = new ec2.SecurityGroup(this, "Ec2Sg", {
            vpc: props.vpc,
            allowAllOutbound: true,
        });

        sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), "HTTP access");

        const publicSubnet = props.vpc.publicSubnets[0];

        this.instance = new ec2.Instance(this, "Ec2", {
            vpc: props.vpc,
            vpcSubnets: { subnets: [publicSubnet] }, 
            instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
            machineImage: ec2.MachineImage.genericLinux({
                'ap-northeast-1': 'ami-09ed31f8f34719e20' // Amazon Linux 2のインスタンスイメージ
            }),
            securityGroup: sg,
            role,
        });

        this.instance.userData.addCommands(
            '#! /bin/bash',
            'yum update -y',
            'amazon-linux-extras install php7.2 -y',
            'yum -y install mysql httpd php-mbstring php-xml',
            'wget http://ja.wordpress.org/latest-ja.tar.gz -P /tmp/',
            'tar zxvf /tmp/latest-ja.tar.gz -C /tmp',
            'cp -r /tmp/wordpress/* /var/www/html/',
            'touch /var/www/html/.check_alive',
            'chown apache:apache -R /var/www/html',
            'systemctl enable httpd.service',
            'systemctl start httpd.service'
        );
    }
}

記載後cdk diffコマンドを実行し、内容を確認して問題なければcdk deployコマンドを実行します。

6. ELBスタックを作成する

画像

tmp.tsにELBスタックを記載します。

bin/tmp.ts
import * as cdk from 'aws-cdk-lib';

import { VpcStack } from "../lib/vpc-stack";
import { RdsStack } from "../lib/rds-stack";
import { Ec2Stack } from "../lib/ec2-stack";
import { ElbStack } from "../lib/elb-stack";// 追記:ELB

const app = new cdk.App();

// VPC
const vpcStack = new VpcStack(app, "SamplePjVpcStack");

// RDS
const rdsStack = new RdsStack(app, "SamplePjRdsStack", {
    vpc: vpcStack.vpc,
});

// EC2
const ec2Stack = new Ec2Stack(app, "SamplePjEc2Stack", {
    vpc: vpcStack.vpc,
});

// 追記:ELB 
const elbStack = new ElbStack(app, "SamplePjElbStack", {
    vpc: vpcStack.vpc,
    ec2Instance: ec2Stack.instance,
});

elb-stack.tsにスタックの詳細を記載します。

lib/elb-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as elbv2Targtes from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'

interface ElbStackProps extends cdk.StackProps {
    vpc: ec2.Vpc;
    ec2Instance: ec2.Instance;
}

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

        const lbSecurityGroup = new ec2.SecurityGroup(this, "elbSg", {
            vpc: props.vpc,
            description: 'Allow HTTP access for elb',
            allowAllOutbound: true,
        });

        lbSecurityGroup.addIngressRule(
            ec2.Peer.anyIpv4(),
            ec2.Port.tcp(80),
            'Allow elb HTTP from anywhere for elb'
        );

        const alb = new elbv2.ApplicationLoadBalancer(this, "FrontLB", {
            vpc: props.vpc,
            internetFacing: true,
            securityGroup: lbSecurityGroup,
            vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, 
        });

        const targetGroup = new elbv2.ApplicationTargetGroup(this, "FrontLBTargetGroup", {
            vpc: props.vpc,
            port: 80,
            protocol: elbv2.ApplicationProtocol.HTTP,
            healthCheck: {
                path: '/.check_alive',
                interval: cdk.Duration.seconds(30),
            },
            targetGroupName: `${id}-tg`,
        });

        targetGroup.addTarget(new elbv2Targtes.InstanceTarget(props.ec2Instance, 80));

        alb.addListener('FrontLBListener', {
            port: 80,
            defaultTargetGroups: [targetGroup],
        });

        const FrontLBEndpoint =new cdk.CfnOutput(this, "FrontLBEndpoint", {
            value: alb.loadBalancerDnsName,
            exportName: `${id}-Endpoint`, 
        });
    }
}

記載後cdk diffコマンドを実行し、内容を確認して問題なければcdk deployコマンドを実行します。

7. EC2にアクセスできることを確認する

  • CloudFormationのELBスタックのDNS名(出力欄の値)を、新しいタブもしくはウィンドウで入力して、EC2上のWordPressの初期設定画面が表示されることを確認します。
    (WordPressの設定はこちらをご確認ください)

さいごに

あくまで個人の意見ですが、今回AWS CDKで環境構築をしてみて、CloudFormationより難しいけど楽しいし便利だなあと感じています。

CloudFormationはymlファイルで必要な項目の設定内容をすべて記載し、アップロードを行う流れでした。一方でAWS CDKはどのコンストラクトを使用するか選び、テンプレートが自動生成される恩恵を使いながらCLI上で確認を挟みながら組み立てていける点に面白さがありました。
(AWSにほんの少し慣れてきて、小さな成功体験が増えたことで目を向けられることの幅が少しずつ広がっていることも、楽しさに比例している可能性は否めません)

とはいえ、AWSが出しているベストプラクティスにはまだまだ程遠いですし、ポイントを押さえられていない状態で手を動かしてしまったり、反省・課題は山積みなのでこれからもっと精進していきます。今回の記事が私と同じようなAWS初心者の方にとって有益な情報となっていれば幸いです。
最後までご覧いただき、ありがとうございました。

参考

セカンドセレクション

Discussion