[ECS初心者向け] docker-compose.yml の uWSGI, Nginx, Flask アプリを ECS Fargateで動かす

2021.02.28

はじめに

おはようございます、もきゅりんです。

ローカル環境などで docker-compose.yml で実行させていたアプリを Amazon ECS (以下、ECS) で動かしたくなることもあると思います。

ということで、docker-compose.yml で実行させていた uWSGI, Nginx, Flask アプリを ECS(Fargate) に移行しようと思いました。

下記ブログのように docker compose コマンドを使って、docker-compose.yml を使って直接 ECSを構築するのではなく、これまで通りのECSクラスターを構築するやり方です。

Docker ComposeによるAmazon ECS対応がGAに!コンテナをローカル環境と同じノリでECS環境で起動できるぞ!! | DevelopersIO

本稿では、基本的にコンソールを使って構築していきます。

念のため、ECS の主要構成要素をAmazon ECS Deep Dive から再確認しておきましょう。

ecs_components

なお、ECS(Fargate)の概要を一通り把握したい方は、下記ブログを参照下さい。

EC2環境で実行するECSとの比較もまとまっています。

基礎から応用までじっくり学ぶECS Fargateを利用したコンテナ環境構築 #Fargate | DevelopersIO

概要

本稿の例のdocker-compose.ymlで構築される環境は下図のようになります。

local_docker_compose

中身の詳細については、本稿の本筋ではないため、あくまで参考としてこちらに置いておきます。

構成の概要は下記で伝わるかと思います。

.
├── app_flask
│   ├── Dockerfile
│   ├── app.py
│   ├── requirements.txt
│   └── uwsgi.ini
├── docker-compose.yml
└── web_server
    ├── Dockerfile
    └── nginx.conf
version: '3'

services:
  web_server:
    build:
      context: .
      dockerfile: ./web_server/Dockerfile
    ports:
      - 8080:80
    depends_on:
      - app_service
    restart: always
  app_service:
    build:
      context: .
      dockerfile: ./app_flask/Dockerfile
    expose:
      - 5000
    volumes:
      - './app_flask:/project'
    restart: always
    environment:
      TZ: Asia/Tokyo

それを、ECSを使って下図のような環境にします。

ecs_app_architecture

そもそも Webサーバーとアプリケーションのタスク定義を分ける必要があるかどうかで述べると、コンテナの単一プロセスの共通原則はありつつも、特段分けたい理由がなければ分ける必然性はないように考えていますが、今回はサービス連携させたいのが目的なので、敢えて分けた設計としています。

その際、ECSにおける各サービス間の連携には、ロードバランサーベースとDNSサービスの2つがありますが、当環境においては特にロードバランサーによる機能を利用しないため、シンプルな ECS サービスディスカバリを利用します。

なお、サービスディスカバリについては、下記ブログが詳しいので別途ご参照下さい。

ECSのサービスディスカバリーが東京にやってきて、コンテナ間通信の実装が簡単になりました! | DevelopersIO

やってみる

構成が決まれば後は作成するだけなので、下記のように進めます。

  1. docker-compose.ymlからそれぞれのコンポーネントのDockerfileをECRに登録する
  2. ALBおよびターゲットグループを作成する
  3. ECSクラスター / タスク定義を作成
  4. サービスを作成

前提

  • VPCは作成済みで各種AWSサービスには通信可能である

1 docker-compose.ymlからそれぞれのDockerfileをECRに登録する

ECRへのDockerイメージ登録については、下記ブログの Dockerfile準備と、ECRの作成 を参照にしてください。

CloudformationでFargateを構築する | DevelopersIO

nginx.conf には注意があります。

  1. SERVICE_DISCOVERY_NAME.NAMESPACE にはサービスディスカバリに設定するサービス検出名. 名前空間にします。
  2. サービス検出名は、英数字とアンダースコアの文字列 (中にピリオドを含む) なので注意します。
  3. 変数を使用して proxy_pass ディレクティブでドメイン名を指定させないと、NGINXはTTL値を無視して、次の再起動または構成の再読み込みまでDNSレコードをキャッシュしてしまうため、resolver ディレクティブを追記しておきます。(本稿では、ECSサービスディスカバリのTTL値で対応させます)

詳細は、DNS for Service Discovery with NGINX and NGINX Plus をご参照下さい。

events {
}
http {
    server {
            location / {
                # VPCのネットワーク範囲(CIDR)のアドレスに+2をプラスしたIP
                resolver 10.0.0.2;
                set $backend_servers SERVICE_DISCOVERY_NAME.NAMESPACE; 
                proxy_pass http://$backend_servers:5000;
            }
        }
}

2 ALBおよびターゲットグループを作成する

通常のようにALBとターゲットグループを作成すればいいのですが、ターゲットの種類にだけ注意です。

サービスのタスク定義で、awsvpc ネットワークモード (起動タイプが Fargate の場合に必要) が使用されている場合は、instance ではなく、ip をターゲットタイプ です。

ECS のネットワークモードが不透明な場合、 ECSでEC2インスタンスを利用する際のネットワークモードについて調べてみた | DevelopersIO を参照下さい。

この環境でのターゲットグループのプロトコル/ポートはHTTP/80です。

3 ECSクラスターを作成 / タスク定義を作成

クラスターは Fargate を選択するだけなので特に問題ないと思います。

タスク定義は項目が多く、煩雑になるので、 AWS CLIから作成します。その前にロググループとストリームを作成してしまいます。コンソールで作成した場合は、自動で作成してくれます。

aws logs create-log-group --log-group-name /ecs/nginx-revpro
aws logs create-log-group --log-group-name /ecs/uwsgi-flask

タスク定義を作成します。

aws ecs register-task-definition --cli-input-json file://nginx-task.json
aws ecs register-task-definition --cli-input-json file://uwsgi-flask-task.json
# nginx-task.json
{
  "family": "nginx-revpro",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "image": "xxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-revpro:1.0.0",
      "name": "nginx-revpro-task",
      "portMappings": [
        {
          "hostPort": 80,
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/nginx-revpro",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "cpu": "256",
  "memory": "512",
  "requiresCompatibilities": ["FARGATE"],
  "executionRoleArn": "arn:aws:iam::xxxxxxxxxxx:role/ecsTaskExecutionRole"
}
# uwsgi-flask-task.json
{
  "family": "uwsgi-flask",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "image": "xxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/uwsgi-flask:1.0.0",
      "name": "uwsgi-flask-task",
      "portMappings": [
        {
          "hostPort": 5000,
          "protocol": "tcp",
          "containerPort": 5000
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/uwsgi-flask",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "cpu": "256",
  "memory": "512",
  "requiresCompatibilities": ["FARGATE"],
  "executionRoleArn": "arn:aws:iam::xxxxxxxxxx:role/ecsTaskExecutionRole"
}

4 サービスを作成

以下の設定で app-service から作成します。 記載したもの以外は、デフォルトです。

  • app-service
    • 起動タイプ: Fargate
    • タスク定義: 上記作成したもの
    • サービス名: app-service
    • タスクの数: 1
    • クラスターVPC: デプロイするVPCネットワーク
    • サブネット: デプロイするサブネット
    • セキュリティグループ: ポート5000を許可するセキュリティグループ
    • サービスの検出の統合の有効化: 有効
    • 名前空間: eg.com
    • サービスの検出名: app_service
    • DNSレコード: A
    • TTL: 10
  • web-service
    • 起動タイプ: Fargate
    • タスク定義: 上記作成したもの
    • サービス名: web-service
    • タスクの数: 1
    • クラスターVPC: デプロイするVPCネットワーク
    • サブネット: デプロイするサブネット
    • セキュリティグループ: ポート80を許可するセキュリティグループ
    • ロードバランサーの種類: ALB
    • ロードバランサー名: 作成したALB名
    • ロードバランサー用のコンテナに追加
    • プロダクションリスナーポート: 80
    • ターゲットグループ名: 作成したTG名

確認すると、文言表示されているかと思います。

curl hoge-alb-xxxxxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com
<h1 style='color:blue'>Hello There!</h1>

最後に

簡単な例ですが、 docker-compose.yml のリソースをECS環境で実行させる一例をまとめてみました。

サービス間のコンテンツルーティングの機能が欲しい場合には、ALBを使った連携、各サービス間の切り分けの考慮には、独立してリソースをスケーリングさせる必要性があるかどうかなどを検討すると宜しいかなと考えます。

以上です。

どこかのどなたかのお役に立てば幸いです。

参考