実践!AWS CDK #18 ALB

題字・息子たち
2021.07.12

はじめに

今回は ALB を構築します。
前回作成した EC2 をこの ALB に紐付けます。

前回の記事はこちら。

AWS 構成図

パブリックなサブネットに ALB を配置します。

1

設計

ALB、リスナー、ターゲットグループを作成します。

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

devio-stg-alb(ALB)

項目
種類 application
スキーム internet-facing
IP アドレスタイプ ipv4
サブネット devio-stg-subnet-public-1a
devio-stg-subnet-public-1c
セキュリティグループ devio-stg-sg-alb

リスナー

項目
リスナー ID HTTP : 80
ルール デフォルト: 転送先 devio-stg-tg

devio-stg-tg(ターゲットグループ)

項目
ターゲットタイプ Instance
プロトコル HTTP
ポート 80
VPC devio-stg-vpc
ターゲット devio-stg-ec2-1a
devio-stg-ec2-1c

実装

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

lib/resource/alb.ts

import * as cdk from '@aws-cdk/core';
import { CfnLoadBalancer, CfnTargetGroup, CfnListener } from '@aws-cdk/aws-elasticloadbalancingv2';
import { CfnVPC, CfnSubnet, CfnSecurityGroup, CfnInstance } from '@aws-cdk/aws-ec2';
import { Resource } from './abstract/resource';

export class Alb extends Resource {
    public loadBalancer: CfnLoadBalancer;

    private readonly vpc: CfnVPC;
    private readonly subnetPublic1a: CfnSubnet;
    private readonly subnetPublic1c: CfnSubnet;
    private readonly securityGroupAlb: CfnSecurityGroup;
    private readonly ec2Instance1a: CfnInstance;
    private readonly ec2Instance1c: CfnInstance;

    constructor(
        vpc: CfnVPC,
        subnetPublic1a: CfnSubnet,
        subnetPublic1c: CfnSubnet,
        securityGroupAlb: CfnSecurityGroup,
        ec2Instance1a: CfnInstance,
        ec2Instance1c: CfnInstance
    ) {
        super();
        this.vpc = vpc;
        this.subnetPublic1a = subnetPublic1a;
        this.subnetPublic1c = subnetPublic1c;
        this.securityGroupAlb = securityGroupAlb;
        this.ec2Instance1a = ec2Instance1a;
        this.ec2Instance1c = ec2Instance1c;
    };

    createResources(scope: cdk.Construct) {
        this.loadBalancer = this.createLoadBalancer(scope);
        const targetGroup = this.createTargetGroup(scope);
        this.createListener(scope, this.loadBalancer, targetGroup);
    }

    private createLoadBalancer(scope: cdk.Construct): CfnLoadBalancer {
        const loadBalancer = new CfnLoadBalancer(scope, 'Alb', {
            ipAddressType: 'ipv4',
            name: this.createResourceName(scope, 'alb'),
            scheme: 'internet-facing',
            securityGroups: [this.securityGroupAlb.attrGroupId],
            subnets: [this.subnetPublic1a.ref, this.subnetPublic1c.ref],
            type: 'application'
        });

        return loadBalancer;
    }

    private createTargetGroup(scope: cdk.Construct): CfnTargetGroup {
        const targetGroup = new CfnTargetGroup(scope, 'AlbTargetGroup', {
            name: this.createResourceName(scope, 'tg'),
            port: 80,
            protocol: 'HTTP',
            targetType: 'instance',
            targets: [
                {
                    id: this.ec2Instance1a.ref
                },
                {
                    id: this.ec2Instance1c.ref
                }
            ],
            vpcId: this.vpc.ref
        });

        return targetGroup;
    }

    private createListener(scope: cdk.Construct, loadBalancer: CfnLoadBalancer, targetGroup: CfnTargetGroup) {
        new CfnListener(scope, 'AlbListener', {
            defaultActions: [{
                type: 'forward',
                forwardConfig: {
                    targetGroups: [{
                        targetGroupArn: targetGroup.ref,
                        weight: 1
                    }]
                }
            }],
            loadBalancerArn: loadBalancer.ref,
            port: 80,
            protocol: 'HTTP'
        });
    }
}

まずは ALB に関する Construct を利用するために @aws-cdk/aws-elasticloadbalancingv2 をインストールします。

$ npm install @aws-cdk/aws-elasticloadbalancingv2

@aws-cdk/aws-elasticloadbalancing と間違えないようにご注意ください。末尾に v2 が付きます。

今回は各リソース 1 つずつなので ResourceInfo のインタフェースは作成しません。
それぞれリソースを作成するための createXxx() メソッドを用意し、その中でプロパティを直接設定しています。

また、動作確認をしやすいように EC2 に Apache をインストールするユーザーデータを設定しました。これで EC2 インスタンス起動時に自動的に Apache がインストールされ、Web ブラウザから ALB にアクセスすると Apache のテストページが表示されます。

lib/script/ec2/userData.sh

#!/bin/bash
sudo yum -y install httpd
sudo systemctl enable httpd
sudo systemctl start httpd

lib/resource/ec2.ts

import * as fs from 'fs';

~ 省略 ~

private static readonly userDataFilePath = `${__dirname}/../script/ec2/userData.sh`;

~ 省略 ~

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)
        }],
        userData: fs.readFileSync(Ec2.userDataFilePath, 'base64')
    });

    return instance;
}

ハイライト部分を追加しました。
やっていることは次の通りです。

  • ユーザーデータのスクリプトを別ファイルに分離
  • EC2 インスタンス作成時のプロパティに userData を追加
  • ファイルから読み込むため fs モジュールの readFileSync() メソッドを利用(エンコーディングは Base64 を指定)
  • ユーザーデータのファイルパスは定数化

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

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';
import { Alb } from './resource/alb';

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

    ~ 省略 ~

    // ALB
    const alb = new Alb(
      vpc.vpc,
      subnet.public1a,
      subnet.public1c,
      securityGroup.alb,
      ec2.instance1a,
      ec2.instance1c
    );
    alb.createResources(this);
  }
}

テスト

テストコードはこちら。

test/resource/alb.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('Alb', () => {
    const app = new cdk.App();
    const stack = new Devio.DevioStack(app, 'DevioStack');

    expect(stack).to(countResources('AWS::ElasticLoadBalancingV2::LoadBalancer', 1));
    expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', {
        IpAddressType: 'ipv4',
        Name: 'undefined-undefined-alb',
        Scheme: 'internet-facing',
        SecurityGroups: anything(),
        Subnets: anything(),
        Type: 'application'
    }));

    expect(stack).to(countResources('AWS::ElasticLoadBalancingV2::TargetGroup', 1));
    expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', {
        Name: 'undefined-undefined-tg',
        Port: 80,
        Protocol: 'HTTP',
        TargetType: 'instance',
        Targets: anything(),
        VpcId: anything()
    }));

    expect(stack).to(countResources('AWS::ElasticLoadBalancingV2::Listener', 1));
    expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', {
        DefaultActions: [{
            Type: 'forward',
            ForwardConfig: {
                TargetGroups: [{
                    TargetGroupArn: anything(),
                    Weight: 1
                }]
            }
        }],
        LoadBalancerArn: anything(),
        Port: 80,
        Protocol: 'HTTP'
    }));
});

以下を確認しています。

  • ALB のリソースが 1 つあること
  • ターゲットグループのリソースが 1 つあること
  • リスナーのリソースが 1 つあること
  • 各リソースのプロパティが正しいこと

確認

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

2

ALB が正しく作成されています。

3

お次はリスナー。

こちらもデフォルトルールでターゲットグループへ転送されるようになっていますね。

4

ターゲットグループも指定したインスタンスが登録されています。

5

6

Health status が unhealthy になっていますが、これはインスタンス内に /var/www/html/index.html を作成することで解消されます。(Apache のテストページを表示したいのでこのままにします)

そしてこちらがそのテストページです。

7

ALB のドメイン(ここでは devio-stg-alb-1622770308.ap-northeast-1.elb.amazonaws.com)にアクセスすることで表示されます。
これで ALB に対する通信が EC2 までルーティングされていることがわかりますね。

これにて確認完了です!

CloudFormation 版

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

Alb:
  Type: AWS::ElasticLoadBalancingV2::LoadBalancer
  Properties:
    IpAddressType: ipv4
    Name: devio-stg-alb
    Scheme: internet-facing
    SecurityGroups:
      - Fn::GetAtt:
          - SecurityGroupAlb
          - GroupId
    Subnets:
      - Ref: SubnetPublic1a
      - Ref: SubnetPublic1c
    Type: application
AlbTargetGroup:
  Type: AWS::ElasticLoadBalancingV2::TargetGroup
  Properties:
    Name: devio-stg-tg
    Port: 80
    Protocol: HTTP
    Targets:
      - Id:
          Ref: Ec2Instance1a
      - Id:
          Ref: Ec2Instance1c
    TargetType: instance
    VpcId:
      Ref: Vpc
AlbListener:
  Type: AWS::ElasticLoadBalancingV2::Listener
  Properties:
    DefaultActions:
      - ForwardConfig:
          TargetGroups:
            - TargetGroupArn:
                Ref: AlbTargetGroup
              Weight: 1
        Type: forward
    LoadBalancerArn:
      Ref: Alb
    Port: 80
    Protocol: HTTP

なお、EC2 の UserData に関しては Base64 でエンコードしたので、AWS CDK からのテンプレート出力では次のように表示されてしまいます。

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
    UserData: IyEvYmluL2Jhc2gKc3VkbyB5dW0gLXkgaW5zdGFsbCBodHRwZApzdWRvIHN5c3RlbWN0bCBlbmFibGUgaHR0cGQKc3VkbyBzeXN0ZW1jdGwgc3RhcnQgaHR0cGQK

このタイミングで中身が見れないのは残念ですが、確認したいときはファイルを見に行くようにしましょう。

GitHub

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

おわりに

ALB を設置することによってインターネット経由のアクセスで動作確認ができるようになりました。
マネジメントコンソール以外で確認できるのは嬉しいですね。ようやく 構築できてるんだな ということが実感できます。

ではゴールまでもう少し、次回からは DB まわりを構築していきましょう。

リンク