[ハンズオン] ECS を使った nginxコンテナのデプロイ演習

6月 11, 2023【初学者向け】実践演習,AWS,CloudFormation,Docker,ECS

概要

対象読者

  • 今回は、Amazon ECS + Fargate のハンズオンです。対象読者は、Amazon Elastic Container Service (Amazon ECS) および AWS Fargate の初学者、Docker の初学者になります。

今回のテーマ

  • 今回のハンズオンで経験できることは、以下となります。
    • CloudFormation を使って、VPC および関連リソースを作成します。このVPC が、Fargate (コンテナ)を実行する環境になります。
    • AWS Cloud9 を使って、nginx のDocker イメージを作成し、Amazon ECR (リポジトリ) にプッシュします。Cloud9 はクラウドベースの統合開発環境 (IDE) です。Cloud9の実行環境は、先に作成した VPC のパブリックサブネットに配置します。
    • ECS クラスタおよびタスク定義を作成し、サービス/タスクをデプロイします。タスク定義に指定したコンテナが起動します。タスクは、先に作成した VPC のプライベートサブネットに配置します。今回コンテナへのアクセスは、ALB 経由で行います。このALB およびALB用のセキュリティグループは、CloudFormation によって作成されます。
    • ALB にアクセスし、nginxコンテナから “Hello ECS." が返ることをテストします。
    • 最後に、ECS 関連のリソースを削除、Cloud9の実行環境の削除、VPC および関連リソースの削除を行います。特に、NAT Gateway、ALB は時間単位で費用が掛かります。勉強に使ったら、使わないリソースは削除しましょう。

ECS on Fargateの概念

  • 今回のハンズオンの様に、コンテナをサーバーレスで実行する際に、ECS on Fargate の構成を選択します。Amazon Elastic Container Service (Amazon ECS)はコンテナのオーケストレーションになり、AWS Fargate はコンテナの実行環境になります。
  • コンテナの実行環境にAWS Fargate を使用することで、コンテナホストがマネージド(サーバーレス)になり、実行環境の管理が不要、スケールもシームレスに行われます。
  • ECS on Fargate の概念は、AWS Black Belt の資料も参考にしてください。

 

演習に利用するシステムの構成図

 

 

 

ハンズオン1:VPC および関連リソースの作成

CloudFormation を使ったVPC および関連リソースの作成

  • CloudFormation を使って、VPC および関連リソースを作成します。AWSマネジメントコンソールからCloudFormationを選択します。
  • 画面右上の「スタックの作成」から「新しいリソースを使用 (標準)」を選択します。
  • 「テンプレートの準備完了」を選択します。「テンプレートの指定」から「テンプレートファイルのアップロード」を選択、「ファイルの選択」を選択し、CFnテンプレートファイルを選択します。

 

  • 今回のテンプレートファイルは、以下の YAMLを使用します。この YAMLは、こちらの AWSドキュメントに記載されているAWS CloudFormation VPC テンプレートをカスタマイズし、ALB およびALB用のセキュリティグループの定義を追加したものです。
Description:  This template deploys a VPC, with a pair of public and private subnets spread
  across two Availability Zones. It deploys an internet gateway, with a default
  route on the public subnets. It deploys a pair of NAT gateways (one in each AZ),
  and default routes for them in the private subnets.

Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16

  PublicSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.192.10.0/24

  PublicSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
    Type: String
    Default: 10.192.11.0/24

  PrivateSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
    Type: String
    Default: 10.192.20.0/24

  PrivateSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
    Type: String
    Default: 10.192.21.0/24

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-vpc

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-igw

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-sntpub1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-sntpub2

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnet1CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-sntpri1

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnet2CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-sntpri2

  NatGateway1EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway2EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway1EIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  NatGateway2:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway2EIP.AllocationId
      SubnetId: !Ref PublicSubnet2

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-rtbpub

  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2


  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-rtbpri1

  DefaultPrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1

  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-rtbpri2

  DefaultPrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnet2


  LoadBalancerSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: !Sub ${EnvironmentName}-sgalb1
      GroupName: !Sub ${EnvironmentName}-sgalb1
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
  ALB:
    Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
    Properties:
      LoadBalancerAttributes:
        - Key: access_logs.s3.enabled
          Value: 'false'
        - Key: deletion_protection.enabled
          Value: 'false'
        - Key: idle_timeout.timeout_seconds
          Value: '60'
      Name: !Sub ${EnvironmentName}-alb1
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LoadBalancerSecurityGroup
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-alb1


Outputs:
  VPC:
    Description: A reference to the created VPC
    Value: !Ref VPC

  PublicSubnets:
    Description: A list of the public subnets
    Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]]

  PrivateSubnets:
    Description: A list of the private subnets
    Value: !Join [ ",", [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ]]

  PublicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref PublicSubnet1

  PublicSubnet2:
    Description: A reference to the public subnet in the 2nd Availability Zone
    Value: !Ref PublicSubnet2

  PrivateSubnet1:
    Description: A reference to the private subnet in the 1st Availability Zone
    Value: !Ref PrivateSubnet1

  PrivateSubnet2:
    Description: A reference to the private subnet in the 2nd Availability Zone
    Value: !Ref PrivateSubnet2

  LoadBalancerSecurityGroup:
    Description: Security Group for ALB
    Value: !Ref LoadBalancerSecurityGroup

  ALB:
    Description: ALB (internet facing)
    Value: !Ref ALB

 

  • スタックの名前を入力します。
  • パラメータを指定します。EnvironmentName に任意の名前を指定し、その他のパラメータ (VPC,Subnet のCIDR) は変更しなくても構いません。

 

  • スタックオプションは特に指定しません。
  • レビューを確認し、「送信」を押します。

 

  • スタック一覧より、該当のスタックの「ステータス」が「CREATE_COMPLETE」と表示されたことを確認します。スタックの実行完了には、しばらく時間が掛かります。

 

ハンズオン2:Cloud9 を使ってDocker イメージを作成

ECR のリポジトリ作成

  • AWSマネジメントコンソールからECS(Elastic Container Service) を選択します。続いて、Amazon ECR のリポジトリを選択します。
  • 「リポジトリを作成」を選択します。

 

  • 可視性設定に「プライベート」を選択、リポジトリ名を指定し、「リポジトリを作成」を押します。

 

  • リポジトリが作成されました。

 

Cloud9 のIDE (開発環境) を作成

  • ECR のリポジトリに Dockerイメージを登録するため、Cloud9 を使用します。先ず、Cloud9 のIDE (開発環境) を作成します。
  • AWSマネジメントコンソールからCloud9 を選択します。

 

  • 続いて、「環境を作成」を選択します。

 

  • 環境の名前を入力、環境タイプに「新しい EC2 インスタンス」を指定します。

 

  • 新しい EC2 インスタンスのインスタンスタイプに「t2.micro (1 GiB GiB RAM + 1 vCPU)」、プラットフォームに「Amazon Linux 2」、タイムアウトに「30 分」を指定します。

 

  • ネットワーク設定の接続に「AWS Systems Manager (SSM)」を選択、VPCに先ほど作成したVPC([EnvironmentName]-vpc)を選択、サブネットに先ほど作成したパブリックサブネット([EnvironmentName]-sntpub1)を選択します。「作成」を押します。

 

  • Cloud9 のIDE (開発環境) が作成されました。

 

Cloud9 でDockerイメージを作成してリポジトリにプッシュ

  • 作成されたCloud9 IDE のリンクを押します。以下の通り、IDE が起動しました。

 

  • Cloud9 のターミナルから下記コマンドを実行します。
mkdir ecs_hello
cd ecs_hello/
mkdir conf
  • ecs_hello/ 配下に、Dockerfile ファイルを作成します。
    • Dockerfile ファイルは、コンテナイメージを管理するための定義ファイルです。
    • ベースとして使用する既存のイメージの指定(→ FROM コマンド)、イメージの作成プロセス時に実行されるコマンド(→ ADD コマンド)、コンテナイメージの新しいインスタンスが展開されるときに実行されるコマンド(→ RUN コマンド)などの定義が含まれます。
  • 今回使用するDockerfile ファイルは、下記となります。
FROM nginx:latest

ADD conf/nginx.conf /etc/nginx/
RUN echo "Hello ECS." > /usr/share/nginx/html/index.html
  • ecs_hello/conf/ 配下に、nginx.conf を作成します。
  • 今回使用する nginx.conf は、下記となります。
    • デフォルトから変更している箇所は、worker_processes を1にしています。autoindex on を指定しています。
    • 公開ディレクトリは、/usr/share/nginx/html です。
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            autoindex on;
        }

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
    }
}

 

  • 以下は、エディタの画面となります。

 

  • Docker イメージの作成、プッシュを行うコマンドを確認するため、ECR のコンソールに戻り、作成したECR リポジトリを選択します。
  • 「プッシュコマンドの表示」ボタンを押します。下記の画面が表示されます。

 

  • Cloud9 のターミナルに戻り、Docker イメージの作成、ECR リポジトリへプッシュを行います。先ほどECR リポジトリの画面で確認したコマンド 1~4を使用します。(以下コマンドの111111111111 にはアカウント番号が入ります。リージョンはお使いの環境に合わせて、修正ください。)
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
$ 
$ docker build -t niikawa-testenv .
Sending build context to Docker daemon   5.12kB
Step 1/3 : FROM nginx:latest
latest: Pulling from library/nginx
f03b40093957: Pull complete 
eed12bbd6494: Pull complete 
fa7eb8c8eee8: Pull complete 
7ff3b2b12318: Pull complete 
0f67c7de5f2c: Pull complete 
831f51541d38: Pull complete 
Digest: sha256:af296b188c7b7df1234567890abcdefg1234567890abcdefg1234567890abcde
Status: Downloaded newer image for nginx:latest
 ---> f9c14fe76d50
Step 2/3 : ADD conf/nginx.conf /ec/nginx/
 ---> 099789c1113a
Step 3/3 : RUN echo "Hello ECS." > /usr/share/nginx/html/index.html
 ---> Running in ac1cfb8392fa
Removing intermediate container ac1cfb8392fa
 ---> d2a54dd451ac
Successfully built d2a54dd451ac
Successfully tagged niikawa-testenv:latest
$ 
$ docker tag niikawa-testenv:latest 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/niikawa-testenv:latest
$ docker push 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/niikawa-testenv:latest
The push refers to repository [111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/niikawa-testenv]
4648e8b920c2: Pushed 
72944a72048c: Pushed 
4fd834341303: Pushed 
5e099cf3f3c8: Pushed 
7daac92f43be: Pushed 
e60266289ce4: Pushed 
4b8862fe7056: Pushed 
8cbe4b54fa88: Pushed 
latest: digest: sha256:e72417f8fbbc47f1234567890abcdefg1234567890abcdefg1234567890abcde size: 1985

 

  • ECR リポジトリに、イメージがプッシュがされました。ダイジェストが docker pushコマンドの結果と一致します。

 

ハンズオン3:ECS によるコンテナのデプロイ

ECS クラスタの作成

  • AWSマネジメントコンソールからECS(Elastic Container Service) を選択します。続いて、クラスターを選択します。
  • 「クラスターの作成」を選択します。

 

  • クラスター名を入力します。
  • ネットワーキングのVPCに先ほど作成したVPC([EnvironmentName]-vpc)を選択、サブネットに先ほど作成したプライベートサブネット([EnvironmentName]-sntpri1, [EnvironmentName]-sntpri2)を選択します。

 

  • インフラストラクチャは、デフォルトを使用します。
    • これは、キャパシティープロバイダーに関する設定であり、詳細はこちらのドキュメントを参照ください。

 

  • ECS クラスタが作成されました。

 

ECS タスク定義の作成

  • AWSマネジメントコンソールからECS(Elastic Container Service) を選択します。続いて、タスク定義を選択します。
  • 「新しいタスク定義の作成」を選択します。

 

  • タスク定義ファミリーの名前を入力、コンテナの名前を入力、コンテナイメージの指定(ECR からイメージURL のコピー/貼りつけ)を行います。
  • ポートマッピングはデフォルトのコンテナポート 80 を使用します。awsvpc ネットワークモードを使用するタスク定義では、コンテナポートのみを指定します。

 

  • タスクサイズのCPU、メモリに、Linuxでサポートされる最小の0.25 vCPU、0.5 GBを選択します。タスクサイズの詳細は、こちらのドキュメントを参照ください。
  • タスク実行ロールは、コンテナに付与する権限です。

 

  • ログ収集はデフォルト設定を使用して、CloudWatch Logs にログを送信する様ににタスク内のコンテナを設定します。

 

  • タスク定義の設定値を確認して、問題なければ「作成」を押します。
  • 以下の通り、新規にタスク定義が作成されました。最初のタスク定義は、リビジョン 1 になります。

 

サービスの作成

  • タスク定義の「デプロイ」ボタンを押し、「サービスの作成」を選択します。

 

  • 既存のクラスターから作成済みのクラスタを選択します。

 

  • サービス名を入力、必要なタスクの数に「1」を指定します。

 

  • ネットワーキングのVPCに先ほど作成したVPC([EnvironmentName]-vpc)を選択、サブネットに先ほど作成したプライベートサブネット([EnvironmentName]-sntpri1, [EnvironmentName]-sntpri2)を2つ選択します。

 

  • セキュリティグループは、「新しいセキュリティグループの作成」を選択します。タスクのENI に割り当てるセキュリティグループの名前、説明を入力、インバウンドルールを登録します。このインバウンドルールには、CloudFormationにて作成したALB 用のセキュリティグループ([EnvironmentName]-sgalb1)を指定します。(ALB から転送されるリクエストのみ許可するため)
  • パブリックIP は、オフに設定します。

 

  • ロードバランシング – オプションを設定します。
  • ロードバランサーの種類に「Application Load Balancer」を選択します。Application Load Balancerに「既存のロードバランサーを使用」を指定し、ロードバランサーにCloudFormationにて作成したALB ([EnvironmentName]-alb1)を指定します。
  • リスナーは「新しいリスナーを作成」を選択します。ポートは「80」を指定、プロトコルは「HTTP」を選択します。
  • ターゲットグループに「新しいターゲットグループの作成」を選択します。ターゲットグループ名を入力します。プロトコルは「HTTP」を選択します。ヘルスチェックの設定はデフォルトを使用します。

 

  • サービスのその他の設定はデフォルトを使用します。最後に「作成」を選択します。
  • サービス/タスクがデプロイされ、タスクが起動しました。

 

  • サービス/タスクが正常に動作しているかを確認します。サービスの正常性とメトリクスを選択します。以下の通り、ステータスは正常、タスク 1個が実行中、ALB のヘルスチェックも成功しています。

 

  • ターゲットグループを選択します。ターゲットに、先ほどデプロイされたタスクに割り当てられたENI のIPアドレスが登録されています。

 

nginxコンテナの疎通テスト

  • ブラウザからALB にアクセスし、nginxコンテナから “Hello ECS." が返ることをテストします。

 

ハンズオン4:後片付け

  • 演習の後片付けを行います。先ずECS のタスクを停止させます。対象サービスの「サービスを更新」を選択し、デプロイ設定の必要なタスクの数を「0」に変更します。
  • 次に、対象サービスの「サービスを削除」を選択し、サービスの削除を行います。
  • 続いて、ECSクラスタを削除します。対象クラスタの「クラスターの削除」を選択し、ECSクラスタの削除を行います。
  • Cloud9 の開発環境を削除します。EC2 のインスタンス(aws-cloud9-xxxxxxxxxx) が起動している場合は、インスタンスを選択、「インスタンスの終了」を選択します。(Cloud9 の開発環境にEC2 を使用しているため、VPC および関連リソースを削除する前にインスタンスを削除する必要があります)
  • 最後に、CloudFormation のスタックを削除します。スタックを選択し、「削除」を選択します。事前に、VPC/サブネット上にデプロイされていたリソースが削除されていれば、10分程度でDELETE_COMPLETE になります。EC2 のインスタンス(aws-cloud9-xxxxxxxxxx) が残っている場合、VPC/サブネットが削除されずに残ります。その場合は個別にリソースの削除等を行い、再度CloudFormation のスタック削除を行ってください。
  • これで、費用に関わるリソースの後片付けが出来ました。ECS のタスク定義、ECR リポジトリは残りますが、不要であれば削除してください。

 

参考資料