AWS CDKでNLB + ALB + EC2の構成を作ってみる

2022.09.20

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

「NLBのターゲットにALBを作成する構成をサクッと試したい」

NLBのターゲットにはALBを登録することもできます。

やったこと無かったので、試してみました。

せっかくなので、AWS CDKを使って構築してみました。

やってみた

構成図

リソース作成

コードは以下になります。

コードを実行してのリソース作成には、手元の環境で10分ほどかかりました。

作成される構成は上記の図のとおりです。

Webサーバ用のEC2では、ユーザーデータを使ってapacheのインストールとテスト用にindex.htmlを作成しています。

import * as cdk from 'aws-cdk-lib';
import { CfnOutput } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as elbv2_tg from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

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

    const natGatewayProvider = ec2.NatProvider.instance({
      instanceType: new ec2.InstanceType('t3.small'),
    });

    const vpc = new ec2.Vpc(this, "Vpc", {
      cidr: "10.0.0.0/16",
      availabilityZones: [ "ap-northeast-1a", "ap-northeast-1c"],
      natGatewayProvider,
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "public",
          subnetType: ec2.SubnetType.PUBLIC
        },
        {
          cidrMask: 24,
          name: "private",
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
        },
      ]
    })
    // Security Group
    const albSg = new ec2.SecurityGroup(this, "alb-sg", {
      vpc,
      allowAllOutbound: true,
      description: "security group for a alb"
    })
    albSg.connections.allowInternally(ec2.Port.tcp(80))

    const webServerSg = new ec2.SecurityGroup(this, "web-server-sg", {
      vpc,
      allowAllOutbound: true,
      description: "security group for a web server"
    })
    webServerSg.connections.allowFrom(albSg, ec2.Port.tcp(80), 'Allow alb access')

    // EC2
    const userData = ec2.UserData.forLinux({
      shebang: "#!/bin/bash",
    })
    userData.addCommands(
      "yum update -y",
      "yum install -y httpd",
      "systemctl start httpd",
      "systemctl enable httpd",
      "echo test > /var/www/html/index.html"
    )

    const role = iam.Role.fromRoleName(this, "Ec2Role", "AmazonSSMRoleForInstancesQuickSetup")

    const machineImage = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      edition: ec2.AmazonLinuxEdition.STANDARD,
      virtualization: ec2.AmazonLinuxVirt.HVM,
      storage: ec2.AmazonLinuxStorage.GENERAL_PURPOSE,
    });

    const webServer = new ec2.Instance(this, "Instance", {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
      machineImage,
      vpcSubnets: {
        subnets: vpc.privateSubnets
      },
      role,
      userData
    })
    webServer.addSecurityGroup(webServerSg)
    // nat instance作成前にuserdataを実行してしまうとyumでエラーになる
    webServer.node.addDependency(vpc)

    // ALB(private)
    const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
      internetFacing: false,
      vpc,
      vpcSubnets: {
        subnets: vpc.privateSubnets
      }
    })
    alb.addSecurityGroup(albSg)

    const instanceTarget = new elbv2_tg.InstanceTarget(webServer)

    const albListener = alb.addListener("AlbHttpListener", {
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP
    })
    albListener.addTargets("WebServerTarget", {
      targets: [ instanceTarget ],
      port: 80
    })

    const albTarget = new elbv2_tg.AlbTarget(alb, 80 )

    // NLB(public)
    const nlb = new elbv2.NetworkLoadBalancer(this, "Nlb", {
      vpc,
      internetFacing: true,
      vpcSubnets: {
        subnets: vpc.publicSubnets
      }
    })
    // ALBが存在しない時点でTargetGroupを作るとエラーになるため
    nlb.node.addDependency(alb)

    const nlbListener = nlb.addListener("NlbHttpListener", {
      port: 80,
      protocol: elbv2.Protocol.TCP
    })
    nlbListener.addTargets("AlbTarget", {
      targets: [ albTarget ],
      port: 80
    })

    new CfnOutput(this, "NlbDnsName",{
      value: nlb.loadBalancerDnsName
    })
  }
}

疎通確認

NLBのエンドポイントにcurlをして、Webサーバで表示しているコンテンツを確認します。

NLBのエンドポイントは、CDKのOutputで出しているためCDK実行時にコンソール上からも確認できます。

 % curl <NLBのエンドポイント>
test

少しハマったところ

主にリソース作成の依存関係でハマりました。

nat instance作成前にEC2が作成されて、EC2のユーザーデータのyumが動かなかった

業務で利用する場合はVPCのスタックとNLBやALBのスタックは、分割することが多いと思うのであまり発生しないとは思います。

最初は、EC2とVPC関連のリソースを明示的に依存関係を作らずに作成していました。

そのため、EC2でインターネット接続が可能になる前にユーザーデータが実行されてユーザーデータが動かないことがありました。

以下のように依存関係を追加することで、この事象は回避できました。

    // nat instance作成前にuserdataを実行してしまうとyumでエラーになる
    webServer.node.addDependency(vpc)

ALBが存在しない時点でNLBのターゲットグループを作成してしまう

今回はNLBのターゲットグループでALBを指定しています。

依存関係を明示していないと、CloudFormatino実行時にALBが存在しない旨のエラーがでます。

    // ALBが存在しない時点でTargetGroupを作るとエラーになるため
    nlb.node.addDependency(alb)

おわりに

AWS CDKでNLB + ALB + EC2の構成を作成してみました。

似たような構成をCDKで作成する際に参考になると嬉しいです。

以上、AWS事業本部の佐藤(@chari7311)でした。