その他
    ホーム エンジニア AWS CDKでさくっとECSサービスとCapacity Providerを設定する
    CDKでさくっとECSサービスとCapacity Providerを設定する
     

    CDKでさくっとECSサービスとCapacity Providerを設定する

    はじめに

    デジタルイノベーション部の浅田です。

    AWSをはじめとしたクラウドがオンプレミスに対してもつ優位性の一つに、インフラストラクチャーをコードとして管理できることがあります。いわゆる、Infrastucture As a Codeというわけですが、そのためにAWSではCloudFormationを利用することが一般的です。

    CloudFormationも十分に強力な機能ですが、それをさらに強化するものとして、AWS Cloud Development Kit(以下、CDK)があります。

    そこで、今回はCDK(on TypeScript)を使ってECS上のサービスを作成することで、いかに簡単にCDKでAWSのインフラを作成できるかをご紹介したいと思います。なお、動作確認済みのCDKバージョンは執筆時点で最新の2.15.0となります。また、CDKの初期設定などは紙面の都合上省略致します(参考:CDK初期設定)。

    今回、作成するAWSの構成

    今回はECS上のサービスとして、ALBで負荷分散された、EC2起動タイプのNginxのサービスを立てるということをやっていきたいと思います。そこにさらに、ECSのキャパシティプロバイダーを使うことによって、サービスに必要なEC2リソースをAutoScalingで動的に起動させる設定を入れ込みたいと思います。

    構成図としては、以下となります。

    キャパシティプロバイダーって?

    参考:Amazon ECS キャパシティープロバイダー

    キャパシティプロバイダーを簡単に説明すると、ECSのタスクコンテナが必要とするCPU/メモリなどに応じて、ホストされるEC2のリソースをAutoScalingさせる機能です。

    マイクロサービスを多数動かすコンテナ利用では、個々のコンテナごとにEC2が用意されるわけではなく、ひとつのEC2の中に複数のコンテナが稼働するのが当たり前になります。そうしたときにEC2自体のCPU使用状況などをもとにAutoScalingさせるような旧来の運用だと、どれぐらい足りないのか判断がつきません。

    大きなリソースを要求するようなコンテナのタスクを増やす必要があれば、大きなインスタンスが必要となるでしょうし、小さなリソースを要求するコンテナのタスクを増やすのであれば、小さなインスタンスのほうが効率が良くなります。

    そこで、タスク側が「コンテナを××台建てたいから、必要なリソースを用意して」と要求した際に、ECS側が他のコンテナとの兼ね合いを含めて必要なリソースを満たせるようにEC2の台数を調整する、というのがキャパシティプロバイダーの機能になります。

    個々のタスクやサービス側がコンテナのCPU/メモリ使用量であったり、リクエスト量、処理データ量、スケジュールによるバッチ処理などのイベントなどに応じてタスクの数をコントロールし、それに必要なリソースをECS側がコントロールすることによって、担当する範囲が明確化され疎結合になるというのもキャパシティプロバイダーを利用するメリットの一つだと思います。

    ECSのサービスを作るうえで必要となるAWSの要素

    上記のような設定をAWSで構築しようとすると、作成する必要がある要素は意外と多くなります。

    ざっとあげるだけでも以下のようになります。

    1. VPC、サブネット、インターネットゲートウェイ、ルートテーブル、セキュリティグループなどのネットワーク関連リソース
    2. ECSクラスター、クラスターを構成するEC2インスタンス、起動設定、AutoScaling設定などのECSクラスター関連リソース
    3. ALB、リスナールール、ターゲットグループの設定などのALB関連リソース
    4. タスク定義、サービス起動設定などのECS上のサービス関連リソース

    これをCloudFormationで定義しようとすると、数百行の定義を書くことになります。

    L1コンストラクタとL2コンストラクタ

    CDKには、AWSのリソースを簡単に定義するためにコンストラクタが用意されています。その中でも、L1コンストラクタ、L2コンストラクタがあります。

    L1コンストラクタはほぼCloudFormationと同じ要素を同じようにコントロールできる柔軟性を持っていますが、その分設定しなきゃいけない要素が多くなります。

    L2コンストラクタは簡単にAWSリソースを定義できるように用意された機能です。細かいコントロールはL1コンストラクタ程できませんが、その分圧倒的に簡単にリソースの定義ができます。以下ではL2コンストラクタを中心に扱います。

    クラスター側のスタック作成

    まず、サービスを動かすための基盤を作成するスタックを作ります。使っているモジュールは以下です。

    import * as ec2 from 'aws-cdk-lib/aws-ec2';
    import * as ecs from "aws-cdk-lib/aws-ecs";
    import * as autoscaling from "aws-cdk-lib/aws-autoscaling";

    VPCの作成

    まずはVPCです。

    const vpc = new ec2.Vpc(this, "VPC", {
        vpcName: `Vpc`,
        cidr: "10.0.0.0/16",
        natGateways: 0,
        maxAzs: 99,
        subnetConfiguration: [
            {
                cidrMask: 24,
                name: "public",
                subnetType: ec2.SubnetType.PUBLIC,
            },
        ],
    });

    これだけの記述で、サブネットやルートテーブル、インターネットゲートウェイなどの諸々を作ってくれます。

    今回はNatGatewayを使わないのでnatGatewaysを0に設定していますが、これを指定してあげればNatGateway、およびそのルートテーブルなども作成してくれます。

    また、maxAzsを指定することで、サブネットを作るアベイラビリティゾーンの数の指定ができますが、大きな値を指定することで、そのリージョンに存在するアベイラビリティゾーン分サブネットを作ってくれます。東京リージョンとバージニアリージョンなど、アベイラビリティゾーンの個数が異なるリージョンに適用するさいに、意識せずに済むようになります。

    AutoScalingの作成

    続いてECSクラスターを構成するEC2のAutoScaling設定です。

    const ec2SecurityGroup = new ec2.SecurityGroup(this, "Ec2SecurityGroup", {
        vpc,
    });
    ec2SecurityGroup.addIngressRule(
        ec2.Peer.ipv4("10.0.0.0/16"),
        ec2.Port.tcpRange(32768, 65535)
    );
    const autoScalingGroup = new autoscaling.AutoScalingGroup(this, "ASG", {
        vpc,
        instanceType: new ec2.InstanceType("t3.small"),
        machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
        vpcSubnets: {
            subnetType: ec2.SubnetType.PUBLIC,
        },
        minCapacity: 0,
        maxCapacity: 10,
        securityGroup: ec2SecurityGroup,
    });
    

    ここでは、AutoScalingで起動されたEC2で許容するセキュリティグループを作っています。ポートはALBの動的ポートの範囲を指定してます。また、AutoScalingで起動されるEC2のスペックや、サブネット、最大起動数なをの設定を入れています。

    ECSをEC2で運用する際には、EC2上のECSエージェント設定ファイルにクラスター名を書き込む設定をEC2起動時にユーザデータで設定する、などを定型的に行いますが、上記のAutoScaling設定を後続のECSクラスター設定と組み合わせることで、ユーザデータによるクラスター名設定もCDKが自動でやってくれます。また、ECS運用で必要となる権限である、ECRからのイメージ取得権限のIAMロールなども自動作成してくれます。便利ですね。

    クラスターの作成

    続いて、ECSクラスターの設定です。

    readonly cluster: ecs.Cluster;
    readonly capacityProvider: ecs.AsgCapacityProvider;
    
    ...(省略)
    this.capacityProvider = new ecs.AsgCapacityProvider(this, "CapacityProvider", {
        capacityProviderName: "CapacityForT3Small",
        autoScalingGroup,
    });
    this.cluster = new ecs.Cluster(this, "Cluster", {
        vpc,
        clusterName: `AppCluster`,
    });
    this.cluster.addAsgCapacityProvider(this.capacityProvider);
    

    やっていることは3点です。

    1. キャパシティプロバイダーを作成します。といっても、前節で作成したAutoScalingグループの設定を紐づけて作成しています。
    2. クラスターを作成します。作成済みのvpcを指定して名前を付けています。
    3. 1で作成したキャパシティプロバイダーを2のクラスターと紐づけています。

    ポイントとして、次節で説明するサービス用のスタックで使用するため、コンストラクタのプロパティとしてcluster, capacityProvider変数を使用しているので、thisのプロパティに代入しています。こうすることで、複数のスタック感で意識せずに内容を連携できます。

    実際には、裏でCloudFormationのExport Value/Import Valueを使って連携されています。そこらへんを細かく意識しなくてもいいのは楽ですね。

    サービス側のスタック作成

    続いて、サービスのスタックを作成します。使用しているモジュールは以下です。

    import * as ec2 from "aws-cdk-lib/aws-ec2";
    import * as ecs from "aws-cdk-lib/aws-ecs";
    import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns";

    サービスの作成

    つづいてECSのサービスです。

    const securityGroup = new ec2.SecurityGroup(this, "ALBSecurityGroup", {
        vpc: cluster.vpc,
    });
    securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic());
    const loadBalancedEcsService =
        new ecsPatterns.ApplicationLoadBalancedEc2Service(this, "NginxService", {
            cluster,
            cpu: 256,
            memoryLimitMiB: 256,
            taskImageOptions: {
                image: ecs.ContainerImage.fromRegistry("nginx:latest"),
            },
            listenerPort: 80,
            publicLoadBalancer: true,
            desiredCount: 1,
        });
    loadBalancedEcsService.loadBalancer.addSecurityGroup(securityGroup);
    

    まず、ALBで使用するセキュリティグループを作成します。後続の処理で自動でセキュリティグループが作成されるのですが、EgressのルールがALBの動的ポートの設定が通らないようになっているので、Egressルールを追加しています。

    続いてALBとサービスの設定です。CDKの標準モジュールとしてecs-patternsというものがあります。これは、ECSのサービス設定としてよくあるパターンをモジュール化したもので、ただでさえ簡略化されているCDKによるリソース作成がさらに簡単になっています。

    上記の十数行のコードで、ALB、リスナールール、ターゲットグループなどのALB関連リソースを作ってくれるだけでなく、ECSのタスク定義、サービスの作成、ALBとの紐づけなどを作成してくれます。

    ここでは設定していませんが、証明書の設定、HTTPからHTTPSへのリダイレクト、ドメイン名の設定なども指定できます。また、ログルーターの設定、環境変数の設定なども上記のコンストラクタのパラメータとして指定できる柔軟性も持っています。素晴らしいですね!

    キャパシティプロバイダーの設定

    といった具合に、便利なecs-patternsモジュールなのですが、EC2上のALBで負荷分散されたサービス(コンストラクタ名としてはApplicationLoadBalancedEc2Service)を作る際に、キャパシティプロバイダーの設定ができません。ecs-patternsではないecsモジュールを使用してサービスを作成すればもちろん設定可能ですが、ecs-patternsの便利さを利用できなくなります。

    そこで、以下のように追加の設定を行うことで、キャパシティプロバイダーを利用することができます。

    const service = loadBalancedEcsService.service.node
        .defaultChild as ecs.CfnService;
    service.launchType = undefined;
    service.capacityProviderStrategy = [
        {
            weight: 1,
            capacityProvider: capacityProvider.capacityProviderName,
        },
    ];
    

    やっていることは3点です。

    1. 前節で作成したloadBalancedEcsServiceのサービスリソース(service)をL1コンストラクタのCfnServiceにキャストして取り出します。
    2. serviceのlaunchTypeがEC2に設定されているので、undefinedに設定することでリセットします。
    3. そして、キャパシティプロバイダーとしてクラスター側の設定で作成したcapacityProviderを使用するように設定します。

    上記の設定を追加することにより、作成したキャパシティプロバイダーを使用して、サービスを起動するように設定できます。

    終わりに

    いかがでしたでしょうか。CDKを利用することで、すごく簡単にECSのサービスを立ち上げられることがご理解いただけたのではないでしょうか。

    CDKには、ほかにもいろいろと便利なコンストラクタが標準で用意されていますし、サードパーティのCDKコンストラクタも存在します。それらを利用することで、より最小限の労力でAWSのリソースを定義することができます。積極的に活用することでハッピーなAWSライフを送りましょう!