実践!AWS CDK #17 EC2

題字・息子たち
2021.07.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回は EC2 を構築します。
意外とあっさりできました。

前回の記事はこちら。

AWS 構成図

各 AZ のプライベートなアプリケーション層サブネットに、1 台ずつ EC2 インスタンスを配置します。

1

設計

プロパティは以下の通りです。

リソース名 Availability Zone
devio-stg-ec2-1a ap-northeast-1a
devio-stg-ec2-1c ap-northeast-1c

以下詳細。

devio-stg-ec2-1a

項目
Availability Zone ap-northeast-1a
IAM ロール devio-stg-role-ec2
AMI ID ami-06631ebafb3ae5d34
インスタンスタイプ t2.micro
セキュリティグループ devio-stg-sg-ec2
サブネット devio-stg-subnet-app-1a

devio-stg-ec2-1c

項目
Availability Zone ap-northeast-1c
IAM ロール devio-stg-role-ec2
AMI ID ami-06631ebafb3ae5d34
インスタンスタイプ t2.micro
セキュリティグループ devio-stg-sg-ec2
サブネット devio-stg-subnet-app-1c

リソース名, AZ, サブネット以外は共通です。
AMI ID は現時点で最新の Amazon Linux 2 のものを指定します。

2

なお、今回はキーペアを使用せずに EC2 インスタンスを作成します。
SSH でのインスタンス接続等を行う場合は事前にマネジメントコンソールや AWS CLI などでキーペアを作成してください。AWS CDK や CFn ではキーペアの作成はサポートされていません。

実装

EC2 に関する処理を行うクラスはこちら。

lib/resource/ec2.ts

import * as cdk from '@aws-cdk/core';
import { CfnInstance, CfnSubnet, CfnSecurityGroup } from '@aws-cdk/aws-ec2';
import { CfnInstanceProfile } from '@aws-cdk/aws-iam';
import { Resource } from './abstract/resource';

interface ResourceInfo {
    readonly id: string;
    readonly availabilityZone: string;
    readonly resourceName: string;
    readonly subnetId: () => string;
    readonly assign: (instance: CfnInstance) => void;
}

export class Ec2 extends Resource {
    public instance1a: CfnInstance;
    public instance1c: CfnInstance;

    private static readonly latestImageIdAmazonLinux2 = 'ami-06631ebafb3ae5d34';
    private static readonly instanceType = 't2.micro';
    private readonly subnetApp1a: CfnSubnet;
    private readonly subnetApp1c: CfnSubnet;
    private readonly instanceProfileEc2: CfnInstanceProfile;
    private readonly securityGroupEc2: CfnSecurityGroup;
    private readonly resources: ResourceInfo[] = [
        {
            id: 'Ec2Instance1a',
            availabilityZone: 'ap-northeast-1a',
            resourceName: 'ec2-1a',
            subnetId: () => this.subnetApp1a.ref,
            assign: instance => this.instance1a = instance
        },
        {
            id: 'Ec2Instance1c',
            availabilityZone: 'ap-northeast-1c',
            resourceName: 'ec2-1c',
            subnetId: () => this.subnetApp1c.ref,
            assign: instance => this.instance1c = instance
        }
    ];

    constructor(
        subnetApp1a: CfnSubnet,
        subnetApp1c: CfnSubnet,
        instanceProfileEc2: CfnInstanceProfile,
        securityGroupEc2: CfnSecurityGroup
    ) {
        super();
        this.subnetApp1a = subnetApp1a;
        this.subnetApp1c = subnetApp1c;
        this.instanceProfileEc2 = instanceProfileEc2;
        this.securityGroupEc2 = securityGroupEc2;
    };

    createResources(scope: cdk.Construct) {
        for (const resourceInfo of this.resources) {
            const instance = this.createInstance(scope, resourceInfo);
            resourceInfo.assign(instance);
        }
    }

    private createInstance(scope: cdk.Construct, resourceInfo: ResourceInfo): CfnInstance {
        const instance = new CfnInstance(scope, resourceInfo.id, {
            availabilityZone: resourceInfo.availabilityZone,
            iamInstanceProfile: this.instanceProfileEc2.ref,
            imageId: Ec2.latestImageIdAmazonLinux2,
            instanceType: Ec2.instanceType,
            securityGroupIds: [this.securityGroupEc2.attrGroupId],
            subnetId: resourceInfo.subnetId(),
            tags: [{
                key: 'Name',
                value: this.createResourceName(scope, resourceInfo.resourceName)
            }]
        });

        return instance;
    }
}

いつものように createInstance() メソッド内で CfnInstance の生成処理を実行しています。

このクラス、プロパティは多いのですが必須プロパティは一つもないんですよね。恐れることなく設定したい値だけを入力し、あとはトライアンドエラーで進めていきましょう。

AMI ID とインスタンスタイプについては以下のようにプライベートなクラス定数としました。

private static readonly latestImageIdAmazonLinux2 = 'ami-06631ebafb3ae5d34';
private static readonly instanceType = 't2.micro';

キーペアを指定する場合は以下のようにしてください。

const instance = new CfnInstance(scope, resourceInfo.id, {
    availabilityZone: resourceInfo.availabilityZone,
    iamInstanceProfile: this.instanceProfileEc2.ref,
    imageId: Ec2.latestImageIdAmazonLinux2,
    instanceType: Ec2.instanceType,
    keyName: 'devio-stg-key-pair',
    securityGroupIds: [this.securityGroupEc2.attrGroupId],
    subnetId: resourceInfo.subnetId(),
    tags: [{
        key: 'Name',
        value: this.createResourceName(scope, resourceInfo.resourceName)
    }]
});

keyName の型は string なのでキー名を指定するだけで OK です。

メインのプログラムはこちら。
ハイライト部分を追記しました。

lib/devio-stack.ts

import * as cdk from '@aws-cdk/core';
import { Vpc } from './resource/vpc';
import { Subnet } from './resource/subnet';
import { InternetGateway } from './resource/internetGateway';
import { ElasticIp } from './resource/elasticIp';
import { NatGateway } from './resource/natGateway';
import { RouteTable } from './resource/routeTable';
import { NetworkAcl } from './resource/networkAcl';
import { IamRole } from './resource/iamRole';
import { SecurityGroup } from './resource/securityGroup';
import { Ec2 } from './resource/Ec2';

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

    // VPC
    const vpc = new Vpc();
    vpc.createResources(this);

    ~ 省略 ~

    // EC2
    const ec2 = new Ec2(
      subnet.app1a,
      subnet.app1c,
      iamRole.instanceProfileEc2,
      securityGroup.ec2
    );
    ec2.createResources(this);
  }
}

テスト

テストコードはこちら。

test/resource/ec2.test.ts

import { expect, countResources, haveResource, anything } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as Devio from '../../lib/devio-stack';

test('Ec2', () => {
    const app = new cdk.App();
    const stack = new Devio.DevioStack(app, 'DevioStack');

    expect(stack).to(countResources('AWS::EC2::Instance', 2));
    expect(stack).to(haveResource('AWS::EC2::Instance', {
        AvailabilityZone: 'ap-northeast-1a',
        IamInstanceProfile: anything(),
        ImageId: 'ami-06631ebafb3ae5d34',
        InstanceType: 't2.micro',
        SecurityGroupIds: anything(),
        SubnetId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-ec2-1a' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::Instance', {
        AvailabilityZone: 'ap-northeast-1c',
        IamInstanceProfile: anything(),
        ImageId: 'ami-06631ebafb3ae5d34',
        InstanceType: 't2.micro',
        SecurityGroupIds: anything(),
        SubnetId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-ec2-1c' }]
    }));
});

以下を確認しています。

  • EC2 インスタンスのリソースが 2 つあること
  • 各リソースのプロパティが正しいこと

確認

マネジメントコンソール上でリソースを確認してみましょう。

3

インスタンスが指定した名前で立ち上がっていますね。

各項目も OK です。(セキュリティグループは省略。もちろん OK でした)

4

ついでに SSM のセッションマネージャーでアクセスできることも確認してみましょう。
AWS Systems Manager > セッションマネージャーセッションの開始 をクリックします。

5

このように ターゲットインスタンス に今回作成した EC2 インスタンスが表示されていれば OK です。
(ネットワーク構成や権限まわりが上手く設定できていない場合はここに表示されません)

最後にセッションを開始できるところまで確認して完了です。

6

CloudFormation 版

今回のコードを CFn で書くと以下のようになります。

Ec2Instance1a:
  Type: AWS::EC2::Instance
  Properties:
    AvailabilityZone: ap-northeast-1a
    IamInstanceProfile:
      Ref: InstanceProfileEc2
    ImageId: ami-06631ebafb3ae5d34
    InstanceType: t2.micro
    SecurityGroupIds:
      - Fn::GetAtt:
          - SecurityGroupEc2
          - GroupId
    SubnetId:
      Ref: SubnetApp1a
    Tags:
      - Key: Name
        Value: devio-stg-ec2-1a
Ec2Instance1c:
  Type: AWS::EC2::Instance
  Properties:
    AvailabilityZone: ap-northeast-1c
    IamInstanceProfile:
      Ref: InstanceProfileEc2
    ImageId: ami-06631ebafb3ae5d34
    InstanceType: t2.micro
    SecurityGroupIds:
      - Fn::GetAtt:
          - SecurityGroupEc2
          - GroupId
    SubnetId:
      Ref: SubnetApp1c
    Tags:
      - Key: Name
        Value: devio-stg-ec2-1c

GitHub

今回のソースコードは コチラ です。

おわりに

AWS CDK の実装にこなれてきたせいか、ずいぶんシンプルに CDK っぽい(と私が勝手に思っている)コードが書けるようになってきた気がします。うれしい

次回のお題は「ALB」です。

リンク