📕

「Amazon Web Services基礎からのネットワーク&サーバー構築」の環境をCDK(Typescript)で作成してみた

2024/05/12に公開

はじめに

本記事ではXで話題になっていたAmazon Web Services基礎からのネットワーク&サーバー構築改訂4版の復習とCDK入門を兼ねて、コンソール画面から構築するネットワークやサーバーをCDKを使って構築してみました。

前提条件

AWSのアカウントの作成、AWS configの設定、AWS CLInpmのインストールが完了していること。

AWS CDK Toolkitのインストール

AWS CDK Toolkit は npm 経由でインストールします。

npm install -g aws-cdk
npm install -g typescript ts-node

CDK プロジェクト作成

プロジェクトディレクトリの作成

空のディレクトリを作成し、カレントディレクトリを変更します。
本記事ではディレクトリ名をaws-basic-network-and-server-build-cdk とします。

mkdir ~/aws-basic-network-and-server-build-cdk && cd ~/aws-basic-network-and-server-build-cdk

cdk init

新しい TypeScript CDK プロジェクトを作成するために cdk init を使います。

cdk init sample-app --language typescript

プロジェクトの構造

プロジェクトのディレクトリ・ファイル構成は下記のようになります。
これ以降ではlib/aws-basic-network-and-server-build-cdk.tsを修正していきます。

cdk-workshop/
├── bin/
│   └── aws-basic-network-and-server-build-cdk.ts
├── lib/
│   └── aws-basic-network-and-server-build-cdk-stack.ts
├── node_modules/
├── test/
│   └── aws-basic-network-and-server-build-cdk.test.ts
├── .git/
├── cdk.json
├── jest.config.js
├── package.json
├── README.md
├── tsconfig.json
├── .gitignore
└── .npmignore

ネットワークの構成図

CDKで作成するネットワークの構成は下記のようになります。サブネットのCIDRブロックが異なっている点に注意してください。このようにサブネットがなっている点については、VPC・サブネットの作成で述べます。

VPC・サブネットの作成

まず初めにCHAPTER2、6、7で扱われるVPC、サブネット、NATゲートウェイを作成します。
コンソール画面から作成する場合はインターネットゲートウェイやNATゲートウェイについてルーティングテーブルを設定する必要がありますが、CDKでは下記のように少ないコード量でパブリックサブネットとプライベートサブネットが作成できることが分かります。
サブネットのCIDRブロックについてですが、SubnetConfigurationPropertiesにはCIDRブロックがなく、cidrMaskで設定するため、各サブネットに特定のCIDRブロックを設定することはできないです。CDKではサブネットのCIDRは小さい値から付与されるため、下図のようにパブリックサブネットが10.0.0.0/24、プライベートサブネットが10.1.0.0/24となります。具体的に指定したい場合はこの記事を参考にしてみてください。本記事では割愛します。

import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs';
// ec2 に関するパッケージを import
import * as ec2 from 'aws-cdk-lib/aws-ec2';


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

    // VPC の作成
    const vpc = new ec2.Vpc(this, 'VPC', {
      vpcName: 'VPC',
      maxAzs: 1, // アベイラビリティーゾーンは1つ
      createInternetGateway: true,
      natGateways: 1,
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),

      // サブネットの設定
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'パブリックサブネット',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'プライベートサブネット',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });
  }
}


作成されたサブネットのCIDRブロック

WEBサーバーの作成

続いてWEBサーバーを作成します。
WEBサーバーを作成するコードは下記のようになります。
キーペアについてはコンソール画面から作成したものを使用しています。後ほど、キーペアをCDKで作成する場合について述べます。
WEBサーバーのセキュリティグループはHTTPのためのポート80番、SSH接続のためのポート22番についてはどのIPからも接続を許可しています。
キーペアについてはコンソール画面で作成した名前をfromKeyPairNameの3つ目の引数に入れます。

    // コンソール画面から作成しているEC2のキーペア(my-key)を取得
    const keyPair = ec2.KeyPair.fromKeyPairName(this, 'KeyPair', 'my-key');

    // EC2 インスタンス(Web Server)の作成
    // Web Serverのセキュリティグループを作成
    const webServerSecurityGroup = new ec2.SecurityGroup(this, 'WebServerSecurityGroup', {
      vpc: vpc,
      securityGroupName: 'WEB-SG',
    });

    // Web Serverのセキュリティグループにインバウンドルールを追加
    webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP traffic from anywhere');
    webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH traffic from anywhere');

    // Web Serverのインスタンスを作成
    const webServer = new ec2.Instance(this, 'WebServer', {
      instanceName: 'WEBサーバー',
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
      vpc: vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC, // パブリックサブネットを指定
      },
      securityGroup: webServerSecurityGroup,
      keyPair: keyPair,
    });

DBサーバーの作成

続いてDBサーバーを作成します。
DBサーバーを作成するコードは下記のようになります。
WEBサーバーと異なる部分は2点です。

  • 1点目はサブネットにプライベートを指定する。
  • 2点目はセキュリティグループの設定です。DBサーバーのセキュリティグループではWEBサーバーからのSSH接続のためのポート22番、MariaDBの通信のためのポート3306番を開け、pingコマンドで疎通する際に使用するICMPプロトコルを許可するようにします。addIngressRuleの第一引数をWEBサーバーのセキュリティグループにすることでWEBサーバーからの接続のみを許可する形になります。
    // EC2 インスタンス(DB server)の作成
    // DB serverのセキュリティグループを作成
    const dbServerSecurityGroup = new ec2.SecurityGroup(this, 'DBServerSecurityGroup', {
      vpc: vpc,
      securityGroupName: 'DB-SG',
    });

    // DB serverのセキュリティグループにインバウンドルールを追加
    // MariaDB(MySQL)のポートを開放(Web Serverからのアクセスのみを許可)
    dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(3306), 'Allow MySQL traffic from Web Server');
    // SSHのポートを開放(Web Serverからのアクセスのみを許可)
    dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(22), 'Allow SSH traffic from Web Server');
    // ICMPのポートを開放(Web Serverからのアクセスのみを許可)
    dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.icmpPing(), 'Allow ICMP traffic from Web Server');

    // DB serverのインスタンスを作成
    const dbServer = new ec2.Instance(this, 'DBServer', {
      instanceName: 'DBサーバー',
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
      vpc: vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, // プライベートサブネットを指定
      },
      securityGroup: dbServerSecurityGroup,
      keyPair: keyPair,
    });

キーペアをCDKで作成

キーペアは下記のように作成することができます。
また、秘密鍵の中身はパラメータストアに保存されます。Stack出力のGetSSHKeyCommandの値はコマンドになっており、コマンドを実行することで秘密鍵の情報を取得することができます。

    // キーペア作成
    const cfnKeyPair = new ec2.CfnKeyPair(this, 'CfnKeyPair', {
      keyName: 'key-pair-by-cdk',
    })
    cfnKeyPair.applyRemovalPolicy(RemovalPolicy.DESTROY)

    // キーペア取得コマンドアウトプット
    new CfnOutput(this, 'GetSSHKeyCommand', {
      value: `aws ssm get-parameter --name /ec2/keypair/${cfnKeyPair.getAtt('KeyPairId')} --region ${this.region} --with-decryption --query Parameter.Value --output text`,
    })

EC2サーバーインスタンスのコンストラクトを定義

AWSのワークショップを参考にEC2サーバーインスタンスのコンストラクトを定義し、スタックファイルから切り出します。
下記のようにlib/constructs/ec2-server-instance.tsを作成します。
Construct props に vpc instanceName subnetType securityGroup keyNameを渡すように設計しました。

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

// Construct props を定義
export interface EC2ServerInstanceProps {
  readonly vpc: ec2.IVpc
  readonly instanceName: string
  readonly subnetType: ec2.SubnetType
  readonly securityGroup: ec2.ISecurityGroup
  readonly keyName: string
}

// EC2 インスタンスを含む Construct を定義
export class EC2ServerInstance extends Construct {
  // 外部からインスタンスへアクセスできるように設定
  public readonly instance: ec2.Instance;

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

    // Construct props から vpc, instanceName, subnetType, securityGroup, keyName を取り出す
    const { vpc, instanceName, subnetType, securityGroup, keyName } = props;

    const instance = new ec2.Instance(this, "Instance", {
      instanceName,
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
      vpcSubnets: { subnetType },
      securityGroup,
      keyName,
    });

    // 作成した EC2 インスタンスをプロパティに設定
    this.instance = instance;
  }
}

まとめ

キーペアをCDKで作成、EC2サーバーのコンストラクタを定義して別ファイルにした場合のStack全体のコードは下記のようになります。

import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs';
// ec2 に関するパッケージを import
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// 自作コンストラクトを import
import { EC2ServerInstance } from './constructs/ec2-server-instance';

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

    // VPC の作成
    const vpc = new ec2.Vpc(this, 'VPC', {
      vpcName: 'VPC',
      maxAzs: 1,
      createInternetGateway: true,
      natGateways: 1,
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),

      // サブネットの設定
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'パブリックサブネット',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'プライベートサブネット',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    // キーペア作成
    const cfnKeyPair = new ec2.CfnKeyPair(this, 'CfnKeyPair', {
      keyName: 'key-pair-by-cdk',
    })
    cfnKeyPair.applyRemovalPolicy(RemovalPolicy.DESTROY)

    // キーペア取得コマンドアウトプット
    new CfnOutput(this, 'GetSSHKeyCommand', {
      value: `aws ssm get-parameter --name /ec2/keypair/${cfnKeyPair.getAtt('KeyPairId')} --region ${this.region} --with-decryption --query Parameter.Value --output text`,
    })

    // Web Serverのセキュリティグループを作成
    const webServerSecurityGroup = new ec2.SecurityGroup(this, 'WebServerSecurityGroup', {
      vpc: vpc,
      securityGroupName: 'WEB-SG',
    });

    // Web Serverのセキュリティグループにインバウンドルールを追加
    webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP traffic from anywhere');
    webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH traffic from anywhere');

    // DB serverのセキュリティグループを作成
    const dbServerSecurityGroup = new ec2.SecurityGroup(this, 'DBServerSecurityGroup', {
      vpc: vpc,
      securityGroupName: 'DB-SG',
    });

    // DB serverのセキュリティグループにインバウンドルールを追加
    // MariaDB(MySQL)のポートを開放(Web Serverからのアクセスのみを許可)
    dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(3306), 'Allow MySQL traffic from Web Server');
    // SSHのポートを開放(Web Serverからのアクセスのみを許可)
    dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(22), 'Allow SSH traffic from Web Server');
    // ICMPのポートを開放(Web Serverからのアクセスのみを許可)
    dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.icmpPing(), 'Allow ICMP traffic from Web Server');

    
    // Web Serverのインスタンスを作成
    new EC2ServerInstance(this, 'WebServer', {
      vpc: vpc,
      instanceName: 'WEBサーバー',
      subnetType: ec2.SubnetType.PUBLIC,
      securityGroup: webServerSecurityGroup,
      keyName: cfnKeyPair.keyName,
    });

    // DB serverのインスタンスを作成
    new EC2ServerInstance(this, 'DBServer', {
      vpc: vpc,
      instanceName: 'DBサーバー',
      subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      securityGroup: dbServerSecurityGroup,
      keyName: cfnKeyPair.keyName,
    });
  }
}

参考資料

https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP
https://dev.classmethod.jp/articles/specify-subnet-cidr-on-aws-cdk-l2/
https://dev.classmethod.jp/articles/build-ec2-key-pair-with-aws-cdk/

コード

全体コードは以下のリポジトリにあります。
https://github.com/ShinnosukeSuzuki/aws-basic-network-and-server-build-cdk

Discussion