AWS再入門ブログリレー Amazon ECS編

2020.08.24

どうも、もこ@札幌オフィスです。

当エントリは弊社コンサルティング部による『AWS 再入門ブログリレー 2020』の 15日目のエントリです。

このブログリレーの企画は、普段 AWS サービスについて最新のネタ・深い/細かいテーマを主に書き連ねてきたメンバーの手によって、 今一度初心に返って、基本的な部分を見つめ直してみよう、解説してみようというコンセプトが含まれています。AWS をこれから学ぼう!という方にとっては文字通りの入門記事として、またすでに AWS を活用されている方にとっても AWS サービスの再発見や 2020 年のサービスアップデートのキャッチアップの場となればと考えておりますので、ぜひ最後までお付合い頂ければ幸いです。

では、さっそくいってみましょう。 15日目のテーマは『Amazon ECS』です。

Amazon ECSとは?

Amazon ECSとは「フルマネージドなコンテナオーケストレーションサービス」で、物凄くざっくり言うと「EC2上にコンテナを展開してALBなどと連携してトラフィックを制御して、コンテナのアップデートも良い感じに出来るサービス」です。

Amazon ECSを利用する事でAWS環境でコンテナのクラスターを簡単に作成させることができ、AutoScalingを利用して負荷に応じたコンテナのスケーリングなどもできるサービスになっています。

基本的にはECS Agentが導入されたAMIをベースにEC2のAutoScaling構成を組み、EC2をクラスターに参加させると、ECSからコンテナの実行・停止・デプロイ・各種AWSサービスとの連携が出来るようになります。

上記の図のように、ECSでは主に「クラスター」「サービス」「タスク」の3つで構成されています。それぞれ触って入門していきましょう。

ECSを実際に触ってみる

ECSクラスターを用意してみる

早速となりますが、ECSクラスターに再入門していきましょう。

ECSクラスターは名前の通りECSの骨組みのような物で、ECSクラスターの上に後述する「EC2インスタンス」を起動した上で「タスク定義」を元に作成される「サービス」から「タスク(コンテナ)」をクラスター内のインスタンスにデプロイする事が出来ます。

実践あるのみという事で、ECSのクラスターを作ってみます。

AWS CDKを利用する事で簡単に構築する事ができるので、AWS CDK(TypeScript)の初期設定をしていきます。

$ mkdir ecs
$ cd ecs
$ npm install -g aws-cdk 
$ cdk init app --language typescript
$ npm install @aws-cdk/aws-ec2 @aws-cdk/aws-ecs @aws-cdk/aws-elasticloadbalancingv2

cdk initを実行した後に作成される lib/ecs-stack.ts を下記のように編集します。

import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';

export class EcsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // VPC作成
    const vpc = new ec2.Vpc(this, 'ecs-vpc', {
      cidr: "10.0.0.0/16"
    });
    // ECS Cluster作成
    const cluster = new ecs.Cluster(this, 'cluster', {
      vpc
    });
  }
}

編集が終わりましたら、早速AWS環境にデプロイしていきましょう。

 $ cdk deploy

そうすると、ざっくりこんな感じの構成がAWS上に出来上がります

マネジメントコンソールのECSクラスター一覧から作成されたクラスターを確認する事が出来ます。

まだ何も作成していないの状態なので、サービスやタスク、EC2インスタンスは登録されていない状態です。

とりあえずコンテナを動かすためにEC2を追加してクラスターに取り込みましょう。

先程のCDKに cluster.addCapacity を下記のように追加して、デプロイしていきます。

import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';

export class EcsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // VPC作成
    const vpc = new ec2.Vpc(this, 'ecs-vpc', {
      cidr: "10.0.0.0/16"
    })
    // ECS Cluster作成
    const cluster = new ecs.Cluster(this, 'cluster', {
      vpc
    });
    // ClusterにAutoScalingGroupなt3.microを追加
    cluster.addCapacity('DefaultAutoScalingGroupCapacity', {
      instanceType: new ec2.InstanceType("t3.micro"),
      desiredCapacity: 4
    });
  }
}
$ cdk deploy

そうすると、AutoScaling Groupで作成されたEC2インスタンスが desiredCapacity で指定した数起動してきます。

ECSのクラスター画面からもEC2インスタンスが表示されて、利用可能なCPU/メモリなどの状況が見れるようになります。

CDKでサクッと作ってしまいましたが、手動で作成される場合はAmazon ECS-optimized AMIsなどで提供されてるECS Agentが導入されているAMIを利用してEC2を起動して、UserData等でECS Agentに参加するクラスターの指定をする形となります。

現在の環境はこんな感じで、上記CDKコードで起動したインスタンスで起動する「ECS Agent」にUserData経由でECSクラスターに紐付けられている状態です。

タスク定義とサービスを作る

それでは仕上げに行きましょう。

「タスク定義」と「サービス」を作っていき、実際にコンテナをECSクラスター上で走らせて、サービスとALBを紐付けてコンテナにALB経由でアクセス出来るようにしましょう。

今回はHTTPでの疎通確認のテストを行いたいので、コンテナイメージは nginx:1.19.2-alpine を利用します。(通常ECSを利用する場合は合わせてECRを利用するパターンが多いので、本番環境で利用する際は是非ECRもご活用下さい)

import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';

export class EcsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // VPC作成
    const vpc = new ec2.Vpc(this, 'ecs-vpc', {
      cidr: "10.0.0.0/16"
    })
    // ECS Cluster作成
    const cluster = new ecs.Cluster(this, 'cluster', {
      vpc
    });
    // ClusterにAutoScalingGroupなt3.microを追加
    cluster.addCapacity('DefaultAutoScalingGroupCapacity', {
      instanceType: new ec2.InstanceType("t3.micro"),
      desiredCapacity: 4
    });
    // タスク定義作成
    const taskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef');
    // タスク定義設定
    taskDefinition.addContainer('DefaultContainer', {
      image: ecs.ContainerImage.fromRegistry("nginx:1.19.2-alpine"),
      memoryLimitMiB: 256,
      cpu: 128
    }).addPortMappings({
      containerPort: 80
    });
    // ECS Service作成
    const ecsService = new ecs.Ec2Service(this, 'Service', {
      cluster,
      taskDefinition
    });
    // LoadBalancer作成
    const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
      vpc,
      internetFacing: true
    });
    // TargetGroup作成
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
      vpc,
      port: 80
    });
    // LoadBanacerと紐付け
    loadBalancer.addListener('Listener', {
      port: 80,
      defaultTargetGroups: [targetGroup]
    })
    // TargetGroupとECS Serviceを紐付け
    targetGroup.addTarget(ecsService);
  }
}

編集が終わったら毎度の事ながらcdk deployして環境に適用していきます。

$ cdk deploy

環境への適用が終わったら、マネジメントコンソールのECSからサービスとタスク定義が追加されていることを確認出来ます!

早速、マネジメントコンソールのALB画面からDNS名を拾ってブラウザでアクセスしてみます。

Welcome to Nginx! の画面が表示されました! nginxのコンテナにALB経由でアクセス出来ていることを確認出来ます!

コンテナイメージをアップデートしたときの挙動

さて、Hello Worldが済んだところでタスクアップデート時の挙動を見てみましょう。

上記CDKのコードでさらっと流してしまいましたが、CDKは内部的にコンテナのイメージ情報やポートマッピングの設定を記載する「タスク定義」と呼ばれるものをECSに登録して、サービスがタスク定義の内容を見ながらコンテナを作成をしたりALBなどとの連携をしています。

基本的にはタスク定義の新しいバージョンを作っていき、サービスからバージョンをアップデートしてタスクを更新していく形になります。

今回はCDKを利用する事を想定して、コンテナイメージを nginx:1.19.2-alpine から nginx:1.19.2 に変更してみました。

    // タスク定義作成
    const taskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef');
    // タスク定義設定
    taskDefinition.addContainer('DefaultContainer', {
      image: ecs.ContainerImage.fromRegistry("nginx:1.19.2"),
      memoryLimitMiB: 256,
      cpu: 128
    }).addPortMappings({
      containerPort: 80
    });

この状態で cdk deployして挙動を確認してみましょう。

$ cdk deploy

まずはじめに、新しいコンテナイメージが設定されたタスク定義が作成されていることを確認出来ます。

続いて、サービスのバージョンが上がり新しいタスク定義でタスク(コンテナ)が上がってきます。

ターゲットグループのヘルスチェックで合格してからしばらくすると、古いバージョンのタスクのDrainingが始まります。

このような流れで、最終的には設定したタスク数だけの新しいバージョンのタスクが立ち上がってきました。

「タスク定義を更新してサービスのタスクバージョンを変更」するだけでコンテナのローリングアップデートが可能で、ダウンタイムなしで簡単出来るので、非常に便利です。

まとめ

以上、『AWS 再入門ブログリレー 2020』の 15日目のエントリ『Amazon ECS』編でした。

明日 (8/25) は青柳さんの「Amazon Cognito」の予定です。お楽しみに!!