🙆

AWS CDKで作るEC2(2) ー AWS CDKでSSHレスなUbuntu EC2をSSM経由で構築し、dev/prod環境を切り替える

に公開

https://zenn.dev/catatsumuri/articles/1be7f443ab010b

の続き

現状の確認

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');
    }
}

このようにキーペアを設定してssh可能なホストを用意しているが、ここでsshを塞ぎamazonのコンソールで作業可能のようにする。これはSSM Session Managerを利用する

SSM Session Managerを利用するためには...

  • IAMロール作成 → 信頼ポリシー:ec2.amazonaws.com
  • アタッチポリシー:AmazonSSMManagedInstanceCore
  • EC2起動時にそのロールをインスタンスプロファイルとして付与

など、まあまあ深い設定を施す必要があるのだが、これをCDKにやらせるというのが今回の趣旨だ。またAmazonLinuxからUbuntuに変更するみたいなこともやってみよう。

現状のソースコード管理

今は https://github.com/catatsumuri/cdktest において全てmainで行っている

これに対する変更なども行ってみよう

feature/no-ssh-ec2 ブランチを作成する

今回はgitで管理しているのだから小変更でもbranchを作るべきだ。

git checkout -b feature/no-ssh-ec2

ubuntuのAMIを取る

これはcloudshellで行うとよい。詳細は割愛するが以下のコマンドでubuntu22/amd64のIDを取得する

AMI_ID=$(
  aws ec2 describe-images \
    --region ap-northeast-1 \
    --owners 099720109477 \
    --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*" \
    --query 'sort_by(Images, &CreationDate)[-1].ImageId' \
    --output text
)
echo "$AMI_ID"


AMI IDami-01ff1fcabf5f7572cが取れた

AMI IDを利用し、イメージを修正

ここでec2を修正

lib/ec2-stack.ts
@@ -1,6 +1,7 @@
 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.IVpc;
@@ -10,14 +11,24 @@ export class Ec2Stack extends cdk.Stack {
     constructor(scope: Construct, id: string, props: Ec2StackProps) {
         super(scope, id, props);

+        const role = new iam.Role(this, 'WebServerRole', {
+            assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
+        });
+        role.addManagedPolicy(
+            iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
+        );
+
+        const ami = ec2.MachineImage.genericLinux({
+            'ap-northeast-1': 'ami-01ff1fcabf5f7572c',
+        });
+
         const instance = new ec2.Instance(this, 'WebServer', {
             vpc: props.vpc,
             instanceType: new ec2.InstanceType('t3.micro'),
-            machineImage: ec2.MachineImage.latestAmazonLinux2023(),
-            keyName: 'your-keypair',
+            machineImage: ami,
+            role,
         });

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

AmazonSSMManagedInstanceCoreはAWSが用意しているポリシーで、

  • SSM AgentがSSMサービスと通信するための権限
  • CloudWatch LogsやS3などSSM機能で使う基本的な権限

がまとめて入っている。さらに

const ami = ec2.MachineImage.genericLinux({
    'ap-northeast-1': 'ami-01ff1fcabf5f7572c', // これと
});

const instance = new ec2.Instance(this, 'WebServer', {
    vpc: props.vpc,
    instanceType: new ec2.InstanceType('t3.micro'),
    machineImage: ami, // <-ここ
    role,
});

さらにここでUbuntuのAMI ID ami-01ff1fcabf5f7572cに差し替えている

適用

git add lib/ec2-stack.ts
git commit -m "EC2: switch AMI to fixed ID ami-01ff1fcabf5f7572c; remove SSH; add SSM role"
git push -u origin feature/no-ssh-ec2

プルリクエスト

github使ってるならPRもしますか。ghコマンドが使えるという前提で

gh pr create \
  --base main \
  --head feature/no-ssh-ec2 \
  --title "EC2: switch AMI to fixed ID ami-01ff1fcabf5f7572c; remove SSH; add SSM role" \
  --body "Switch AMI to ami-01ff1fcabf5f7572c for ap-northeast-1; remove SSH access; add SSM Session Manager role."

https://github.com/catatsumuri/cdktest/pull/1

ができる。あらかた確認してMerge。cliで一括ブランチ削除とかまでやるなら

gh pr merge feature/no-ssh-ec2 --merge --delete-branch

でもいいかも


マージされた

本番環境に適用

ここからはcloudshellで

cd cdktest
git pull origin main

ここでは前回の記事通りだとcdk destroyで掃除しちゃってるのでcdk diffがちょっとうるさいが、cdk diffした後

cdk deploy --all

確認


IAMロールから


AmazonSSMManagedInstanceCore

これが付いている事を確認する。

ログインするには

AWS Systems Managerにアクセス https://ap-northeast-1.console.aws.amazon.com/systems-manager/home?region=ap-northeast-1


`セッションマネージャー


セッションを開始する

以上のように遷移すると


Ec2Stack/WebServerがターゲットされている

このように当該インスタンスが出てくるのでセッションを開始からログインできる


apt updateを実行してみた結果

EC2デプロイと同時にwebサーバーも起動するようにする

前回は

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

と手入力してwebサーバーを起動したが、これをuserdataに持っていくという手法がある。ともあれ、やってみよう。ただし、dnfはredhat系のコマンドなので今回ubuntuに切り替えたことによりaptに変更する必要がある。

lib/ec2-stack.ts
@@ -22,11 +22,20 @@ export class Ec2Stack extends cdk.Stack {
             'ap-northeast-1': 'ami-01ff1fcabf5f7572c',
         });

+        const userData = ec2.UserData.forLinux();
+        userData.addCommands(
+            'export DEBIAN_FRONTEND=noninteractive',
+            'apt-get update -y',
+            'apt-get install -y apache2',
+            'systemctl enable apache2',
+            'systemctl start apache2',
+        );
+
         const instance = new ec2.Instance(this, 'WebServer', {
             vpc: props.vpc,
             instanceType: new ec2.InstanceType('t3.micro'),
             machineImage: ami,
             role,
+            userData,
         });

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

そしたら

git checkout -b feature/apache2-userdata
git add lib/ec2-stack.ts
git commit -m "EC2: add UserData to install & start Apache2"
git push -u origin feature/apache2-userdata

こんな感じにfeature/apache2-userdataブランチをpushして

gh pr create \
  --base main \
  --head feature/apache2-userdata \
  --title "EC2: add UserData to install & start Apache2" \
  --body "Install Apache2 via UserData and enable/start service at boot for EC2 instance in ap-northeast-1."

プルリクを出す。確認したら

gh pr merge feature/apache2-userdata --squash --delete-branch

とかのサイクルでやっていく

cloudshellで適用

pullしてcdk diff

Stack Ec2Stack
Resources
[~] AWS::EC2::Instance WebServer WebServer99EDD300 may be replaced
 └─ [~] UserData (may cause replacement)
     └─ [~] .Fn::Base64:
         ├─ [-] #!/bin/bash
         └─ [+] #!/bin/bash
apt update -y
apt install -y apache2
systemctl enable apache2
systemctl start apache2



✨  Number of stacks with differences: 1

Ec2Stackをdeploy

cdk deploy Ec2Stack

これでapache付きのEc2が起動してくる。


apache2が起動した

環境の切り分け

ここでEc2StackをdestroyするとEBSもろとも消滅する。これは実運用が初まったときはこのようにdestroyと同時に消滅していいとは思えない。ただ、開発環境のときは消滅していいということもあるだろう。ここで、IaCを使う利点として本番用のprodと開発用のdevを分けたりというような運用が考えられるので、やってみよう。

ハンズオン的にやる場合は一度ここで全てdestroyしておくのを推奨

cdk destroy --all

cdk の -cオプション

ここで重要になるのはcdkコマンドの -c オプションである。これは CDKアプリケーションに任意のキーと値のペアを渡すための仕組みであり、AWS CDK ではこれを コンテキスト (Context) と呼ぶ。まずはこれを利用してみよう。

bin/app.ts
@@ -4,10 +4,18 @@ import { VpcStack } from '../lib/vpc-stack';
 import { Ec2Stack } from '../lib/ec2-stack';
 
 const app = new cdk.App();
-const vpcStack = new VpcStack(app, 'VpcStack', {
+const envName = app.node.tryGetContext('env') || 'dev';
+console.log('Deploying environment:', envName);
+
+// 共通タグ
+cdk.Tags.of(app).add('Env', envName);
+
+// スタック名にenvを含めてdev/prod共存可能に
+const vpcStack = new VpcStack(app, `VpcStack-${envName}`, { 
+  envName,
   // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
 });
-new Ec2Stack(app, 'Ec2Stack', {
+
+new Ec2Stack(app, `Ec2Stack-${envName}`, {
   vpc: vpcStack.vpc,
-  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
 });

ここで

const envName = app.node.tryGetContext('env') || 'dev';

によりenvというキーを渡せばそれがenvNameに代入される。ここで

cdk.Tags.of(app).add('Env', envName);

この処理により「全てのリソースに」env=envNameがセットされる。たとえばenvprodとかdevとか

VpcStackの改造

VpcStackに渡したので、そっちも改造する。とりあえずoutputするだけ

lib/vpc-stack.ts
@@ -2,10 +2,14 @@ import * as cdk from 'aws-cdk-lib';
 import { Construct } from 'constructs';
 import * as ec2 from 'aws-cdk-lib/aws-ec2';
 
+interface VpcStackProps extends cdk.StackProps {
+  envName: 'dev' | 'prod';
+}
+
 export class VpcStack extends cdk.Stack {
     public readonly vpc: ec2.Vpc;
 
-    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
+    constructor(scope: Construct, id: string, props: VpcStackProps) {
         super(scope, id, props);
 
         // NATなし、パブリックサブネットのみのVPC
@@ -20,5 +24,9 @@ export class VpcStack extends cdk.Stack {
                 },
             ],
         });
+        new cdk.CfnOutput(this, 'CurrentEnv', {
+            value: props.envName,
+            description: 'Current environment name (dev or prod)',
+        });
     }
 }

環境付きdeploy

この段階でEC2をdeployするといろいろ面倒だからVPCを単独deployしてみよう。たとえば何も考えず --allでdeployすると

cdk deploy VpcStack-dev

fallbackでenvNamedevに落ちるのでこんな感じになる。


cdk.CfnOutputのcurrentEnvでdevが出力されている

さらに


NameタグにVPC名、Envタグにdevが含まれる

prodをdeployするには

cdk deploy -c env=prod VpcStack-prod

こんな感じになるわけだ


VPCが2本


スタックも2つ

cdk.json にコンテキストを定義する方法もあるが、まあ割愛。そのうち出てくるかも。

prodのVPCに追加候補の項目

これはVpcフローログとかが考えられるが、トレーニングを多少しないと取るだけで金の無駄になる可能性もあるので一応参考ポインタだけ

https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/flow-logs.html

次回

この辺の環境設定も絡めながらEBSに関する深い話を...する...のか?

Discussion