AWS CDKでリソース作成 第3回: ロードバランサー編

こんにちは。 エキサイト株式会社で内定者アルバイトをしています。

今回も、実際のリソースの作成を行いたいと思います。第3回は、ロードバランサーです。

前の記事 tech.excite.co.jp

ロードバランサーとは?

ロードバランサーはアプリケーションへのトラフィックを、一つまたは複数のアベイラビリティゾーン内の複数のターゲットおよび仮想アプライアンスに自動的に分散するリソースです。

AWSではApplication Load Balancer、Network Load Balancer、Gateway Load Balancer、Classic Load Balancerの4つが提供されています。

実際に作成してみる

今回作成するのは以下のような仕様を持つApplication Load Balancerです。

項目 設定
IPアドレスタイプ IPv4
削除保護 無効
HTTP2 有効
アイドルタイムアウト 120秒

また、ロードバランサーにはリスナーを追加することができます。リスナーはポートとプロトコルを使ってコネクションリクエストをチェックします。リスナーに定義したルールはそのリスナーを追加したロードバランサーの負荷分散の方法に影響します。

ここでは、前回定義したインスタンス2つが属するターゲットグループに対してリクエストを転送するように定義します。

前回までのコード

前回はEC2インスタンスとセキュリティグループを作成しました。 そのコードを示しておきます

package com.myorg;

import software.amazon.awscdk.CfnTag;
import software.amazon.awscdk.services.ec2.CfnInternetGateway;
import software.amazon.awscdk.services.ec2.CfnVPCGatewayAttachment;
import software.amazon.awscdk.services.ec2.IMachineImage;
import software.amazon.awscdk.services.ec2.Instance;
import software.amazon.awscdk.services.ec2.InstanceClass;
import software.amazon.awscdk.services.ec2.InstanceSize;
import software.amazon.awscdk.services.ec2.InstanceType;
import software.amazon.awscdk.services.ec2.MachineImage;
import software.amazon.awscdk.services.ec2.PrivateSubnet;
import software.amazon.awscdk.services.ec2.PublicSubnet;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.Vpc;
import software.constructs.Construct;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

import java.util.List;

public class MyProjectStack extends Stack {
    public MyProjectStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyProjectStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        Vpc vpc = Vpc.Builder.create(this, "SampleVPC")
                .vpcName("sample-vpc")
                .cidr("cidr")
                .subnetConfiguration(List.of())
                .build();

        // Availability zone: ap-northeast-1a
        PrivateSubnet privateSubnetA = PrivateSubnet.Builder.create(this, "PrivateSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetA = PublicSubnet.Builder.create(this, "PublicSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // Availability zone: ap-northeast-1c
        PrivateSubnet privateSubnetB = PrivateSubnet.Builder.create(this, "PrivateSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetB = PublicSubnet.Builder.create(this, "PublicSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // NAT gatewayの追加
        publicSubnetA.addNatGateway();
        publicSubnetB.addNatGateway();

        // Internet Gateway
        CfnInternetGateway internetGateway = CfnInternetGateway.Builder.create(this, "InternetGateway")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-internet-gateway")
                        .build()))
                .build();

        // Internet gatewayをVPCにattachする
        CfnVPCGatewayAttachment.Builder.create(this, "VpcGateAwayAttachment")
                .vpcId(vpc.getVpcId())
                .internetGatewayId(internetGateway.getAttrInternetGatewayId())
                .build();

        SecurityGroup securityGroup = SecurityGroup.Builder.create(this, "SampleSecurityGroup")
                .securityGroupName("sample-security-group")
                .vpc(vpc)
                .build();

        IMachineImage machineImage = MachineImage.latestAmazonLinux();

        Instance instance01 = Instance.Builder.create(this, "ec2-instance-01")
                .instanceName("ec2-instance-01")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1a")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        Instance instance02 = Instance.Builder.create(this, "ec2-instance-02")
                .instanceName("ec2-instance-02")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetB)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1c")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();
    }
}

ロードバランサーを作成し、リスナーを追加するコード

        // Load Balancer
        // Load Balancer用のSecurity Group ①
        SecurityGroup securityGroupEC2LB = SecurityGroup.Builder.create(this, "SecurityGroupEC2LoadBalancer")
                .securityGroupName("ec2-lb-security-group")
                .vpc(vpc)
                .description("security-group-of-ec2-load-balancer")
                .allowAllOutbound(true)
                .build();

        securityGroupEC2LB.addIngressRule(Peer.ipv4("0.0.0.0/0"), Port.tcp(80));

        // ②
        ApplicationLoadBalancer applicationLoadBalancer = ApplicationLoadBalancer.Builder.create(this, "SampleEC2ApplicationLoadBalancer")
                .loadBalancerName("sample-ec2-load-balancer")
                .vpc(vpc)
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA, privateSubnetB)).build())
                .securityGroup(securityGroupEC2LB)
                .deletionProtection(false)
                .ipAddressType(IpAddressType.IPV4)
                .idleTimeout(Duration.seconds(120))
                .http2Enabled(true)
                .build();

        // ③
        ApplicationTargetGroup ec2TargetGroup = ApplicationTargetGroup.Builder.create(this, "EC2TargetGroup")
                .targetGroupName("ec2-target-group")
                .targetType(TargetType.INSTANCE)
                .port(80)
                .vpc(vpc)
                .targets(List.of(new InstanceTarget(instance01, 80), new InstanceTarget(instance02, 80)))
                .protocol(ApplicationProtocol.HTTP)
                .protocolVersion(ApplicationProtocolVersion.HTTP1)
                .healthCheck(HealthCheck.builder()
                        .enabled(true)
                        .path("/")
                        .healthyThresholdCount(3)
                        .unhealthyThresholdCount(2)
                        .interval(Duration.seconds(60))
                        .timeout(Duration.seconds(5))
                        .protocol(Protocol.HTTP)
                        .port("80")
                        .build())
                .build();

        // ④
        ApplicationListener applicationListener = ApplicationListener.Builder.create(this, "ApplicationListener")
                .loadBalancer(applicationLoadBalancer)
                .protocol(ApplicationProtocol.HTTP)
                .port(80)
                .defaultAction(ListenerAction.forward(List.of(ec2TargetGroup)))
                .build();

少し長くなりましたので、以下でそれぞれのリソース作成について簡単に解説します。

まず、①ではロードバランサー用のセキュリティグループを作成しています。セキュリティグループについては前回作成しましたので、こちらを参照してください。

tech.excite.co.jp

②では、ロードバランサー本体を作成しています。vpcSubnets()ではロードバランサーを設置するサブネットを指定します。

③では、ターゲットグループを作成しています。ターゲットグループはロードバランサーによるリクエスト(Application Load Balancerの場合)の転送先をまとめたグループです。ロードバランサーが、リクエストをあるターゲットグループに転送することを決めた場合、このグループ内のどこかに転送することになります。
ここでは、プロトコルとしてHTTPを使い、前回作成したEC2インスタンス2つをターゲットとしています。
また、healthCheck()でターゲットグループのヘルスチェックの設定を行います。ヘルスチェックでは、登録されたターゲットに対して定期的にリクエストを送信することで正常に稼働しているかどうかを判定します。ロードバランサーは正常に稼働しているリソースに対してのみリクエストをルーティングします。

④では、リスナーを作成しています。このリスナーはHTTPでポート80に到着したリクエストを、先ほど作成したターゲットグループ内のEC2インスタンスのいずれかにルーティングするようにしています。

デプロイ

ここまでできたら、あとはいつものようにデプロイするだけです。

cdk deploy

エラーなく生成されれば成功です!

補足

ルーティングの重みづけ

Application Load Balancerではルーティングの重みづけによって、ターゲットグループごとにルーティングされるリクエスト量を変更することができます。 この機能はブルー/グリーンデプロイ(AWS のブルー/グリーンデプロイ – クイックスタート)でよく利用されます。

ターゲットグループを複数(以下の例ではec2TargetGroup1ec2TargetGroup2)用意し、以下のように記述することで重みづけが可能になります。ここではEC2インスタンスec2-instance-01に全体の2/3のリクエストを、EC2インスタンスec2-instance-02に全体の1/3のリクエストをルーティングするように設定しています。

        WeightedTargetGroup weightedEc2TargetGroup1 = WeightedTargetGroup.builder()
                .targetGroup(ec2TargetGroup1)
                .weight(2)
                .build();

        WeightedTargetGroup weightedEc2TargetGroup2 = WeightedTargetGroup.builder()
                .targetGroup(ec2TargetGroup2)
                .weight(1)
                .build();

        ApplicationListener applicationListener = ApplicationListener.Builder.create(this, "ApplicationListener")
                .loadBalancer(applicationLoadBalancer)
                .protocol(ApplicationProtocol.HTTP)
                .port(80)
                .defaultAction(ListenerAction.weightedForward(List.of(weightedEc2TargetGroup1, weightedEc2TargetGroup2)))
                .build();

HTTPリクエストをHTTPSリクエストへ

Application Load BalancerではHTTPリクエストをHTTPSリクエストに変換することもできます。 これは、HTTPリクエストとしてロードバランサーに入ってきたリクエストをHTTPSリクエストとして元のパスにリダイレクトすることで達成できます。

        // HTTPリクエストをHTTPSリクエストとしてリダイレクト
        ApplicationListener httpListener = ApplicationListener.Builder.create(this, "SampleHttpListener")
                .protocol(ApplicationProtocol.HTTP)
                .port(80)
                .loadBalancer(applicationLoadBalancer)
                .defaultAction(ListenerAction.redirect(RedirectOptions.builder()
                        .protocol("HTTPS")
                        .port("443")
                        .build()))
                .build();

        ApplicationListener httpsListener = ApplicationListener.Builder.create(this, "SampleHttpsListener")
                .protocol(ApplicationProtocol.HTTPS)
                .port(443)
                .defaultAction(ListenerAction.forward(List.of(ec2TargetGroup)))
                .loadBalancer(applicationLoadBalancer)
                .sslPolicy(SslPolicy.RECOMMENDED)
                .certificates(List.of(ListenerCertificate.fromCertificateManager(Certificate.fromCertificateArn(this, "certificate", "arn:aws:acm:ap-northeast-1:xxxxxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"))))
                .build();

HTTPSリクエストを処理するにはSSL証明書が必要なので、ARNを指定して取得しています。
(xxxxの部分にはご自身が使用されている証明書のARNに変換してください。)

終わりに

今回は、AWS CDKを用いてApplication Load Balancerを作成しました。

では、また次回。

次の記事

tech.excite.co.jp

参考文献