👏

AWS CDKで作るEC2 ー 初歩の初歩

に公開

初歩の記事が一本あった方がいろいろいいなという事でハンズオン形式で書いてみた。

AWS CDKとは

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

これを読んでいきなり理解するのも結構難しいとは思うんだけど、要するにCDKというのはAWSリソースをプログラムコードで管理するものだ。例えば何も考えずにEC2を起動するとDefault VPCというネットワークが共有され何も考えずネットワークが利用可能となる。一方でプロジェクト向けに独立したネットワーク空間を、新たなVPCを作成して定義することも可能である。これによりネットワーク空間を完全に分離する事で他のリソースによる干渉を制限する事ができるわけなのだが、ここで独自のEC2をカスタムVPCで1つ起動しようと思うと、手作業でやる場合結構しんどい思いをする事になるだろう(このしんどさを理解する意味ではCDKを学ぶ前に、一度手積みのAWS環境を構築する事を推奨するのではあるが)。

EC2を1つインターネットに出すだけでそこそこのネットワーク構成が必要となるし、そこに至るまでの設定が多いと何をどう設定したのかを忘れがちになってAWSリソースが結構汚れるという経験を手積みで設定するとそのような事になりがちだろう。これをある程度解消するのがCDKなどのIaC(Infrastructure as Code)であり、プログラムコードでインフラ環境を構築させる仕組みである。IaCに関してはCDKの他にterraformといったもの等が存在するが、AWSに限ってIaCするのであれば現状ではCDKを最初に触れてみる事がAWSを主に利用する環境においてはROI投資対効果の点からも有益と筆者は考える。

CDKの特徴

CDKtypescriptpythonといった比較的モダンな軽量言語で記述が可能である。これにおいて、typescriptpythonの深い知識(要するに大規模アプリケーション設計など)は特に必要としないが、基本的な制御構造くらいは理解している必要がある。さらに、CDKというのはCloudFormationというAWSの機能をトランスパイルするものに過ぎないという点もまた重要だ。従ってプログラムレベルで全てインフラを自由自在に構築できるわけではなく、あくまでCloudFormationの「スタック」とよばれるものを排出する中間レイヤーであるから、最終的なCloudFormationスタックの形を意識して作成しなくてはならない事が学習あるいは体験が深まると多々登場する事となるはずだ。ただ、最初は軽量のスタックを作成し破壊する事で検知が深まると思われるので、次のセクションからハンズオン形式で学習してみよう。

CDK Bootstrapの必要性

CDKを使用してインフラストラクチャをデプロイするには、まず最初にCDK Bootstrappingが必ず必要となる、これはCDKを実行するために必要なAWSリソースを一元的に作成するものであり、細かい事は沢山あるのだが、とにかく1アカウント1リージョンごと一度は実行しなければならない。

これをもっとも簡単に行う方法はAdministratorAccess権限のアカウントでAWSにログインした画面でCloudShellを使う事である。

CloudShellの開き方とcdk bootstrapの実行

CloudShellは各webコンソールの画面の左下に簡易アクセスアイコンが用意されているので、そちらから起動するのがわかりやすいだろう。


CloudShellへの動線をクリックしてシェルを開いた画面

ここで、それぞれのアカウントに応じたシェル環境が起動するのだが、awsコマンドはもちろんcdkも事前にシェル内の環境に組み込まれている。


type cdkcdk --versionの実行結果

以上を確認したら以下のように入力してみる

cdk bootstrap aws://<アカウントID>/ap-northeast-1


bootstrappingが初まり各種AWSリソースの作成が始まる

ここでのbootstrapはそこそこの量のリソースが作成されるのだが、ここで何が行われているかちょっと見てみよう。これはAWSのメニューよりCloudFormation → スタックを見る


CDKToolkitスタックが作成されている

このCDKToolkitスタックが作成したリソースが全てである。

具体的に作成されたリソースを見る

CloudFormationに慣れていない場合は、ここで作成されたリソースの確認方法について学習しておこう、といっても当該スタックの中に入ってリソースのタブを見るだけだ。


CloudFormationリソースタブを確認している

ここにおいて、作成されているのはほとんどロールポリシーである事が確認できるだろう。一部S3が含まれるため、微量ではあるが、Bootstrapにて課金が発生する可能性があるが、ただし、無視できるような課金量である(あるいはほとんどの場合は無料枠内で収まる)

具体的なスタックの記述

ここから具体的にスタックを記述してく。新規VPCネットワークを作成し、表題の通りEC2を一本起動してみるが、まず進め方を確認しておこう。

どこでコードを書くのか

もちろん、CloudShellの中でshell上のエディターを使って書く猛者も居るだろうが、これはあまりにも一般的ではない。通常はgitリポジトリーを経由する事になるはずだ。これにおいては、githubでもgltlabでもいいのでインフラ専用のリポジトリーを用意するのが好ましいだろう。このケースにおいては以下のような開発フローになるのではないだろうか

まあこれは実際悪くないフローではあるがcloudshellにpullとかして実行しないと即座に結果がわからないというのはある。その場合手元で差分確認やデプロイする方法も確かにあるのだが、その辺の環境構築は慣れてから各自考えてみるのがよろしいのだろうと思う。

どうやってコードを書くのか

これは最初はAI出力でほぼ事足りるはずだ、というかAIの時代になって、よりIaCが効くようになった(とくにCDK) と感じる。AWS内で細かな設定1つやるにせよ手動で行わずIaCでやった方がよいまである。

そしてこのコードについては冒頭述べたように大規模アプリケーションの開発をするわけではないのでコードレベルでの複雑な設計はほとんど発生しないので、いわば書き捨てのようなコードの積み重ねで動作する(どのようなリソース変更があるのかは注意深く見る必要がもちろんある)。設計という意味で重要になるのは 「スタック設計」 につきる。いかにスタックを分割し、再現性を取ったり取らなかったりするのかというのはある程度経験が必要となるだろうから、ここは何度かトライアンドエラーで掴んでいくしかない(スタック設計は個々の環境に極めて依存するので、最終的なスタック設計をこうすればいいというベストな戦略は存在しない事がほとんどである)

EC2でsshする場合は事前にキーペアが必要

これは最初に導入しておいて欲しい。詳細は割愛。まあここまで読めてる人なら問題ないと思うが。

cdk initで初期プロジェクトの雛形を作成する

ここでいよいよCDKによるコードの作成だ。最初にイニシャライズが必要となるが、これはcdkコマンドが簡単に使えるcloudshellで行っちゃうのがいいかもしれない。もちろん手元でも環境を構築すれば実行する事はできるのだが。

ここではcdktestというプロジェクトで行う事にする。以下のようにcdktestディレクトリを作成し、cdk initを行う。

mkdir cdktest
cd cdktest/
cdk init --language=typescript # typescriptをここで指定している。


cdk initの時点でgitリポジトリが作成されている

デフォルトでcdk initすると、gitリポジトリーが作成されている。IaCはバージョン管理で強力に効果を発揮するという意味もあり最初からgitリポジトリーの生成を行うという親切設計なのだろう。

さて、initializeが完成し、git statusを入力すると以下のようになった。

cdktest $ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   .gitignore
        new file:   .npmignore
        new file:   README.md
        new file:   bin/cdktest.ts
        new file:   cdk.json
        new file:   jest.config.js
        new file:   lib/cdktest-stack.ts
        new file:   package.json
        new file:   test/cdktest.test.ts
        new file:   tsconfig.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        package-lock.json

このようなファイル一式が作成されgit addされた状態となっている

重要なファイルの確認

このままそっくりコミットしてしまってもいいのだが、その前にここで一度現在のファイル構成を確認しておこう。

  • .gitignore (プロジェクト管理用)
  • .npmignore (プロジェクト管理用)
  • README.md (プロジェクト管理用)
  • bin/cdktest.ts
  • cdk.json
  • jest.config.js (テスト用)
  • lib/cdktest-stack.ts
  • package.json (ライブラリ管理用)
  • test/cdktest.test.ts (テスト用)
  • tsconfig.json

ここでテストとプロジェクト管理を除くと以下のようになる

  • bin/cdktest.ts
  • cdk.json
  • lib/cdktest-stack.ts
  • tsconfig.json

ここで太字で強調したファイルを主に変更していくことになるのであるが、まずその2ファイルの内容を見てみよう

bin/cdktest.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdktestStack } from '../lib/cdktest-stack';

const app = new cdk.App();
new CdktestStack(app, 'CdktestStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
lib/cdktest-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

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

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'CdktestQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

このように最初はexampleが入っているだけであるので、基本的にlib/cdktest-stack.tsは不要である。そしてbin/cdktest.tsは良くみればlib/cdktest-stack.tsを呼び出しているだけの「エントリーポイント」に過ぎない。またbin/cdktest.tsという名前はディレクトリに応じて自動生成されるため、これを変更したくなるかもしれない、が、いずれにせよ最初はこれをgitリポジトリーに転送し、その後で作業してみることにする。

# 初回コミット
git commit -m "Initial commit: CDK TypeScript app initialized"

# githubに転送
git branch -M main
git remote add origin https://github.com/catatsumuri/cdktest.git
git push -u origin main

作業環境でclone

というわけで作業環境として好ましいところ、つまりvscodeなど好きなエディターが利用できる編集環境等々でcloneしよう。ここでまず、最初にやるのはlib/cdktest-stack.tsを捨てることだ。git rmするとlibが空になるので作り直している

cd cdktest
npm install
git rm lib/cdktest-stack.ts
mkdir lib

npm installはしておくこと。これにより編集環境でもcdkコマンドなどが手に入る(ただしnpx cdkで起動したりはあるかもしれない)

vpc-stack.tsを作成する

ここでlib/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 {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // NATなし、パブリックサブネットのみのVPC
        const vpc = new ec2.Vpc(this, 'MyVpc', {
            maxAzs: 2, // 2つのAZに配置
            natGateways: 0, // NAT Gatewayは作らない
            subnetConfiguration: [
                {
                    subnetType: ec2.SubnetType.PUBLIC,
                    name: 'PublicSubnet',
                    cidrMask: 24,
                },
            ],
        });
    }
}

こういうのはAIに雛形を作ってもらえばよいのだが、重要なのは

maxAzs: 2, // 2つのAZに配置
natGateways: 0, // NAT Gatewayは作らない

AZの数とnatGatewayである。AZはap-northeast-1なら

  • ap-northeast-1a
  • ap-northeast-1c
  • ap-northeast-1d

の最大3つ作れるが、ここでは2とした。1だと分散構成にするとき等ちょっと面倒な事になるので何もアイデアが無い場合は2をセットしよう。さらにnatGatewayはここでは指定しない。これはプライベートネットを作成するときのみ利用する「ことがある」。ただし高価になりがちなのでこのリソース作成に関してはコスト面で注意が必要となるため、このチュートリアルでは作成しない。それ以外VPCを作成するにあたっては追加の費用は必要ない。

さらにbin/以下を変更する。現在bin/cdktest.tsとなっていてファイル名が微妙と感じられた場合はbin/app.tsとかにしてもよい。というかここではそのようにする

git mv bin/cdktest.ts bin/app.ts

同時にbin/app.tsを変更し、vpc-stackを呼び出せるようにしておこう。ここでは以下のようにimportnewする

bin/app.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';

const app = new cdk.App();
new VpcStack(app, 'VpcStack', {
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});


結局ts書くならvscodeが楽だよなって話...

envの中は、まあここではコメント付きでそのままにしといた。

さらにcdk.jsonを変更し、呼び出しをbin/app.tsに変更した事を伝える必要がある。

cdk.json
@@ -1,5 +1,5 @@
 {
-  "app": "npx ts-node --prefer-ts-exts bin/cdktest.ts",
+  "app": "npx ts-node --prefer-ts-exts bin/app.ts",

この環境で現状確認出来る事

この環境ってのはcloneしてきた編集作業環境だが、結局コードをcloneしてきた場所に過ぎないので、cdkコマンドを現状で打っても権限が足りず何もできないため、やれる事はほとんどない。tsが間違ってないか確認するとかくらいなもんだろう。これは以下のようにtsc --noEmitで確認するとよいだろう(もちろんvscodeを使っているならエディターの支援によりエラーを潰すとかはある)

npx tsc --noEmit


コンパイルエラーなし

エラーが無い事を確認したら、この段階でcommit、pushする

git add .
git commit
git push

https://github.com/catatsumuri/cdktest/commit/343db02e409fb3c2c417def2c431ed9ca679a0ef

CloudShellでpullして確認する

cloudshell側でpullしたら、以下の2コマンドが確認のためによく用いられる

  • cdk synth
  • cdk diff

synthはCloudStackのYAMLに変換するコマンドである


膨大なYAMLが出力される

出力が膨大なのと、人が読んでも微妙にわかり辛いのでCloudformationの最終形態を確認するにはいいのかもしれないが実際にはcdk diffで確認する事の方が多いだろう。これを実行すると

cdktest $ cdk diff
current credentials could not be used to assume 'arn:aws:iam::****:role/cdk-hnb659fds-lookup-role-****-ap-northeast-1', but are for the right account. Proceeding anyway.
Lookup role arn:aws:iam::****:role/cdk-hnb659fds-lookup-role-****-ap-northeast-1 was not assumed. Proceeding with default credentials.
Lookup role arn:aws:iam::****:role/cdk-hnb659fds-lookup-role-****-ap-northeast-1 was not assumed. Proceeding with default credentials.
Stack VpcStack
IAM Statement Changes
┌───┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────┬───────────────────────────────────┬────────────────────────────────────────────────────────────────┬───────────┐
│   │ Resource                                                                                                        │ Effect │ Action                            │ Principal                                                      │ Condition │
├───┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────┼───────────────────────────────────┼────────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${Custom::VpcRestrictDefaultSGCustomResourceProvider/Role.Arn}                                                  │ Allow  │ sts:AssumeRole                    │ Service:lambda.amazonaws.com                                   │           │
├───┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────┼───────────────────────────────────┼────────────────────────────────────────────────────────────────┼───────────┤
│ + │ arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/${MyVpcF9F0CA6F.DefaultSecurityGroup} │ Allow  │ ec2:AuthorizeSecurityGroupEgress  │ AWS:${Custom::VpcRestrictDefaultSGCustomResourceProvider/Role} │           │
│   │                                                                                                                 │        │ ec2:AuthorizeSecurityGroupIngress │                                                                │           │
│   │                                                                                                                 │        │ ec2:RevokeSecurityGroupEgress     │                                                                │           │
│   │                                                                                                                 │        │ ec2:RevokeSecurityGroupIngress    │                                                                │           │
└───┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────┴───────────────────────────────────┴────────────────────────────────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                                                   │ Managed Policy ARN                                                                           │
├───┼────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Custom::VpcRestrictDefaultSGCustomResourceProvider/Role} │ {"Fn::Sub":"arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"} │
└───┴────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
(略)

Conditions
(略)

Resources
[+] AWS::EC2::VPC MyVpc MyVpcF9F0CA6F
[+] AWS::EC2::Subnet MyVpc/PublicSubnetSubnet1/Subnet MyVpcPublicSubnetSubnet1Subnet60D1320D
[+] AWS::EC2::RouteTable MyVpc/PublicSubnetSubnet1/RouteTable MyVpcPublicSubnetSubnet1RouteTable00654ADB
[+] AWS::EC2::SubnetRouteTableAssociation MyVpc/PublicSubnetSubnet1/RouteTableAssociation MyVpcPublicSubnetSubnet1RouteTableAssociation2CCE9CDC
[+] AWS::EC2::Route MyVpc/PublicSubnetSubnet1/DefaultRoute MyVpcPublicSubnetSubnet1DefaultRoute2D379878
[+] AWS::EC2::Subnet MyVpc/PublicSubnetSubnet2/Subnet MyVpcPublicSubnetSubnet2Subnet122ADB1B
[+] AWS::EC2::RouteTable MyVpc/PublicSubnetSubnet2/RouteTable MyVpcPublicSubnetSubnet2RouteTableC647F413
[+] AWS::EC2::SubnetRouteTableAssociation MyVpc/PublicSubnetSubnet2/RouteTableAssociation MyVpcPublicSubnetSubnet2RouteTableAssociation7AF8666E
[+] AWS::EC2::Route MyVpc/PublicSubnetSubnet2/DefaultRoute MyVpcPublicSubnetSubnet2DefaultRouteAFC76296
[+] AWS::EC2::InternetGateway MyVpc/IGW MyVpcIGW5C4A4F63
[+] AWS::EC2::VPCGatewayAttachment MyVpc/VPCGW MyVpcVPCGW488ACE0D
[+] Custom::VpcRestrictDefaultSG MyVpc/RestrictDefaultSecurityGroupCustomResource MyVpcRestrictDefaultSecurityGroupCustomResourceA4FCCD62
[+] AWS::IAM::Role Custom::VpcRestrictDefaultSGCustomResourceProvider/Role CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0
[+] AWS::Lambda::Function Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E

✨  Number of stacks with differences: 1
(略)

このような形でどのリソースが変更されるか見易くなっている

cdk diff のよく使う実行例

コマンド例 説明・用途
cdk diff プロジェクトの 全スタック の差分を確認。単一スタック構成ではこれでOK。
cdk diff VpcStack 複数スタック構成で、特定スタックだけ を比較。
cdk diff VpcStack --exclusively 依存スタックを含めず、完全にそのスタックだけ を比較。ノイズ削減に便利。
cdk diff --security-only セキュリティ関連の変更のみ を表示。IAMポリシーの権限拡張などを事前に確認。
cdk diff --fail 差分があったら終了コード1 を返す。CI/CDで差分検出時に処理を止めたい場合に有効。

  • 日常の確認は cdk diff または cdk diff スタック名 が基本。
    • 要するにstackを引数にするか、しないかの違い
  • 本番前やCIパイプラインでは --security-only--fail の組み合わせがよく使われる。
  • マルチスタック構成では --exclusively を使うと差分出力がすっきりする。

deployしてみる

diffの出力に納得したらいよいよcdk deployする。これはスタックが1つの時は何も考えずシンプルに実行してok

cdk deploy


作成されるリソース最終確認が行われる

これを適用しよう。

作成されたリソースを目視で確認する


作成されたリソースをCloudFormationスタックから確認

冒頭で触れたようにCDKCloudFormationスタックを作って適用している「だけ」なので、まずはCloudFormationスタックから確認する。ここではVpcStackというネーミングとなっている。

また、作成されたVPCを実際に確認するのも忘れずに行っておこう。VPCの画面からはリソースマップを確認することができるので、これは便利だから一度目視で必ず確認しておくこと。


識別子とリソースマップの確認

VpcStack/MyVpcとかいう命名がまた微妙なことになっている気がする点も気になるかもしれないが、ここでは割愛。このハンズオンでは名前よりもまずはリソースマップをよく確認してネットワーク構成を吟味しておくとよいだろう。

また内部IPアドレスとして 10.0.0.0/16 が利用されている点にも注目しておこう。これは192.168.0.0/16とかでもいいっちゃいいんだけどdefaultでは10の方が0が揃って拡張性が一番高いという意味で10が使われているだけ。プライベートIPはcidr指定によって変更する事もできるはず。

ともあれネットワークが作成できたので

ここで1本「インターネットにアクセス可能な」パブリックサブネットが作成された。ここにEC2を配置すればEC2をインターネットに剥き出しになるはずだ。つまりINもOUTも設定によってやり放題という事になる。

EC2を配置する

今、VpcStackを用意したので、次はEc2Stackを作成する事になる。ここで、Ec2はVpcで作成されたネットワークにのっかるのだから、ここは「依存」関係となるはずだ。これをプログラムでどう解決していくのかをまず注目しよう。

まず作成したvpcをエントリポイントで受けとれるようにする

これにおいては以下のようにlib/vpc-stack.tsを変更する

lib/vpc-stack.ts
@@ -3,11 +3,13 @@ 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);

         // NATなし、パブリックサブネットのみのVPC
-        const vpc = new ec2.Vpc(this, 'MyVpc', {
+        this.vpc = new ec2.Vpc(this, 'MyVpc', {
             maxAzs: 2, // 2つのAZに配置
             natGateways: 0, // NAT Gatewayは作らない
             subnetConfiguration: [

このようにec2.Vpcの結果を自身のプロパティーにreadonlyで格納し、エントリポイントで取り出せるようにする。

bin/app.ts
@@ -3,6 +3,12 @@ import * as cdk from 'aws-cdk-lib';
 import { VpcStack } from '../lib/vpc-stack';

 const app = new cdk.App();
-new VpcStack(app, 'VpcStack', {
+const vpcStack = new VpcStack(app, 'VpcStack', {
   // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
 });
+
+/* Ec2Stackの呼び出し例
+new Ec2Stack(app, 'Ec2Stack', {
+  vpc: vpcStack.vpc,
+});
+*/

というようにEc2Stack(... vpc: vpcStack.vpc)的な依存関係が作れるというわけだ。

vpcを受け取ってEc2を起動するStack

そしたらこれを記述していく

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

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

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

        const instance = new ec2.Instance(this, 'WebServer', {
            vpc: props.vpc,
            instanceType: new ec2.InstanceType('t3.micro'),
            machineImage: ec2.MachineImage.latestAmazonLinux2023(),
            keyName: 'your-keypair', // <--------------- **これは修正すること**
        });

        instance.connections.allowFromAnyIpv4(ec2.Port.tcp(22), 'SSH');
        instance.connections.allowFromAnyIpv4(ec2.Port.tcp(80), 'HTTP');
    }
}

最終的なbin/app.tsの形も置いておこう

bin/app.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { Ec2Stack } from '../lib/ec2-stack';

const app = new cdk.App();
const vpcStack = new VpcStack(app, 'VpcStack', {
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});
new Ec2Stack(app, 'Ec2Stack', {
  vpc: vpcStack.vpc,
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

これをcommit

git add .
git commit
git push

https://github.com/catatsumuri/cdktest/commit/792ac36bcc4326ba491cf93996bf56ef51bfb159

これが完了したらCloudShellでpullすること

cdk diffとdeploy

CloudShellでプルした後

cdk diff

の結果

Stack Ec2Stack
IAM Statement Changes
┌───┬───────────────────────────────┬────────┬────────────────┬───────────────────────────┬───────────┐
│   │ Resource                      │ Effect │ Action         │ Principal                 │ Condition │
├───┼───────────────────────────────┼────────┼────────────────┼───────────────────────────┼───────────┤
│ + │ ${WebServer/InstanceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:ec2.amazonaws.com │           │
└───┴───────────────────────────────┴────────┴────────────────┴───────────────────────────┴───────────┘
Security Group Changes
┌───┬────────────────────────────────────────────┬─────┬────────────┬─────────────────┐
│   │ Group                                      │ Dir │ Protocol   │ Peer            │
├───┼────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${WebServer/InstanceSecurityGroup.GroupId} │ In  │ TCP 22     │ Everyone (IPv4) │
│ + │ ${WebServer/InstanceSecurityGroup.GroupId} │ In  │ TCP 80     │ Everyone (IPv4) │
│ + │ ${WebServer/InstanceSecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │
└───┴────────────────────────────────────────────┴─────┴────────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter SsmParameterValue:--aws--service--ami-amazon-linux-latest--al2023-ami-kernel-6.1-x86_64:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"}
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}

Conditions
(略)

Resources
[+] AWS::EC2::SecurityGroup WebServer/InstanceSecurityGroup WebServerInstanceSecurityGroup044089BE
[+] AWS::IAM::Role WebServer/InstanceRole WebServerInstanceRoleEEE3F4CD
[+] AWS::IAM::InstanceProfile WebServer/InstanceProfile WebServerInstanceProfile7A5DA8F6
[+] AWS::EC2::Instance WebServer WebServer99EDD300



✨  Number of stacks with differences: 2

ここでStack2つに変更がかかることが確認できた。これをdeployするのだが、このようなとき

cdk deploy

とすると

Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: VpcStack · Ec2Stack

など、どちらをdeployするのか聞かれる。stackを引数で指定してもよいし、指示のように --allを付けてもよい。ここでは --all を付けた

接続確認とwebサーバーのインストール、疎通確認


パブリックIPが付いたEC2。ロールも適当に作成されている点にも注目

これでパブリックIPに向けてssh接続を行う。AmazonLinuxなのでec2-userでログインすること


sshログインできた

このようにログインが可能となっているだろう。さらにwebサーバー(apache)を導入して疎通確認を行う。

sudo dnf update -y
sudo dnf install -y httpd
sudo systemctl enable httpd
sudo systemctl start httpd

ここでパブリックIPにhttp接続すると


web(http)アクセスを確認

以上のようにapacheのdefaultページが見えれば成功だ。

破壊する

ここまででEC2を起動し、webサーバーの疎通を確認したので本稿の趣旨はほぼ完了しているのだが、最後に作成したリソースを片付けることにしよう。CDK内でのスタックは2つ作っただけ、なのにもかかわらず作成されたリソースは結構な数に登る、これを全て削除するにはcdk destroyする
(stack単位で指定して破壊する場合はcdk destroy <stack>する


destroy中

なお、デストロイの様子はcloudformationの画面でも見る事ができる


cloudformation管理画面からみたスタックの破壊

つまり、cdk destroyは実のところはスタックを手動削除しているのと大して変わらないのだが、cdk deployは複数の依存stackをまとめてdestroyするという指示をしているということになる。実際にここではVpcStackEc2Stackの2つを作成したのであるがこれを手作業で消してもほぼ同じような効果になる。というかcdkのコマンドラインから削除に失敗することも割と普通にあるので、その場合はCloudFormationのstackに移動し手動で削除するという事を試みることになるだろう。

次回

さらなるEC2のカスタム(sshを使わないEC2とか)とかチューニングとか行ってみるかも。ここまでのボリュームにはならないようにしたいですねえ...

Discussion