はじめに
社内システムとして稼働している Web アプリケーションの実行基盤を EC2 から ECS Fargate に載せ替えるプロジェクトを担当しました。 長い間コンテナ技術にふれていなかったため理解に苦労しましたが、オンボーディングの一環として、その成果を自分の理解の整理を兼ねて記事化してみようと思います。
前提事項
この記事での言及は、私(筆者)が直近の技術検証で触れた範囲に限定します。 今回想定する構成図はいわゆる典型的なプロダクションユースにそぐわない部分もありますが、以下を省いています。
- 今回はデータベースを利用しないアプリケーション構成です
- NAT Gateway や VPC Endpoint (Interface Gateway) は考慮外
- TLS/SSL 対応は考慮外
Amazon VPC や IAM の関連要素についてはすでにご理解あるものとして省略いたします。 また、VPC、セキュリティグループ、ALB、ECRリポジトリ、ECSクラスタはすでに作成してあるものとします。
本記事で想定するサービス構成
今回は構成要素が少ない、ごく簡単な構成を用いて説明します。
ECS Fargate とは
ECS Fargateは、Amazon ECSで使用できる機能の名称です。サーバーの管理をしたりOSなどがAWSによって管理されており、コンテナの開発のみに注力できるようにしてくれます。
AWS Fargate はAmazon ECSで使用できるテクノロジーであり、サーバーやAmazon EC2インスタンスの クラスターを管理することなくコンテナを実行できます。AWS Fargate を使用すると、コンテナを実行するために仮想マシンのクラスターをプロビジョニング、設定、スケールする必要がありません。これにより、サーバータイプの選択、クラスターをスケールするタイミングの決定、クラスターのパッキングの最適化を行う必要がなくなります。
出典:Amazon Elastic Container Service とは - Amazon Elastic Container Service
主要概念
ここでは、ECSサービスを構築する上での主要な専門用語について、自分なりに認識していることをまとめます。
Cluster (クラスター)
タスクやサービスをひとまとめにするグループのことを指しています。サービスをまとめたタスクをさらにまとめたもののイメージです。
Task Definition (タスク定義)
コンテナ群を作る雛形を自動化できるファイルのことで、JSONを用いて記述します。スペックのほか、環境変数なども情報に含めることができ便利です。
Task
タスクはタスク定義の内容をインスタンス化したものです。
Service
コンテナ化したアプリケーションを実行したり、管理したりするものです。
デプロイまでの流れ
おおまかな流れはこんな感じです。
- ファイルを用意する
- Dockerfileを書く
- タスクを定義を登録する
- ECRにコンテナイメージをプッシュ
- ECSサービスを作成する
それぞれの詳細をサブセクションで解説します
ファイルを用意する
初期で用意するファイルとその構成は以下のような感じです。
ecs-example ├── ecs-task-def.json ├── front │ ├── conf │ │ └── templates │ │ └── app.conf.template │ ├── jquery.js │ ├── main.js │ ├── index.html │ └── Dockerfile └── api ├── app.py ├── requirements.txt └── Dockerfile
- ecs-task-def.json
- frontコンテナとapiコンテナのタスク定義です。後述のセクションで取り上げます。
- confディレクトリ
- nginxの設定ファイルを格納するディレクトリです。後述する作業でnginx.confを格納します。
- front/conf/templates/app.conf.template
- nginxの設定ファイルの一部です。今回のアプリケーションに関するリクエストルーティングの設定ファイルです。
server { listen 80 default_server; listen [::]:80 default_server; root /var/www/html; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; server_name _; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://${API_HOST}:${API_PORT}/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }
- jquery.js
- jqueryを使用する際の関数の定義されたファイルです。公式サイトからダウンロードします。
- main.js
- webサイト中のボタンをクリックした時の動作を記述しているファイルです。
$(document).ready(function() { $('#fetchButton').click(function() { $.ajax({ url: '/api/hello', type: 'GET', dataType: 'json', success: function(data) { console.log('Success:', data); }, error: function(xhr, status, error) { console.error('There was a problem with the request:', error); } }); }); });
- index.html
- webページのレイアウトを記述したファイルです。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Fetch API Example</title> </head> <body> <h1>Fetch API Example</h1> <button id="fetchButton">Fetch</button> <script src="./jquery.js"></script> <script src="./main.js"></script> </body> </html>
- Dockerfile
- Dockerでコンテナを作成するときに付随する操作を記述したものです。appコンテナとfrontコンテナで2つ必要です。後述のセクションを参照してください。
- app.py
- apiサーバーの実装部分で、Flaskフレームワークを使用します。frontコンテナから呼びされたときに"Hello, World!"を返します。
from flask import Flask app = Flask(__name__) @app.route("/hello") def hello(): return "Hello, World!"
- requirements.txt
- 必要なフレームワークとそのバージョンを記述したファイルです。
Flask~=3.0.3 gunicorn~=22.0.0
AWS ECRリポジトリから設定ファイルを得る
ECRリポジトリhttps://gallery.ecr.aws/nginx/nginxからNGINXコンテナにある公式の設定ファイルを取得します。
ecs-exampleディレクトリから以下のコマンドを実行してください。
$ docker run -it -v $(pwd)/front/conf:/work public.ecr.aws/nginx/nginx sh # 以下ではnginxコンテナ内のshellでコマンドを実行しています $ cp /etc/nginx/nginx.conf /work
Dockerfileの作成
まず、Dockerfileを作成します。
- front/Dockerfile
-
FROM public.ecr.aws/nginx/nginx:1.27 ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx COPY ./index.html /var/www/html/ COPY ./main.js /var/www/html/ COPY ./jquery.js /var/www/html/ COPY ./conf /etc/nginx/
- api/Dockerfile
-
FROM python:3.9.19-slim WORKDIR /app RUN apt update && apt install -y curl COPY ./requirements.txt . COPY ./app.py . RUN pip install -r requirements.txt # run application on port 5000 using gunicorn CMD [ "gunicorn", "-b", "0.0.0.0:5000", "app:app" ]
作成したら、Dockerを起動してdockerイメージを作成します。
ECRにコンテナイメージをプッシュ
Amazon ECR (Elastic Container Registry)にコンテナをプッシュします。
user@yourPC ~ $ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com user@yourPC ~ $ docker build -t cdk-ecs-infra-example-api:0.1.0 . user@yourPC ~ $ docker tag cdk-ecs-infra-example-api:0.1.0 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-ecs-infra-example-api:0.1.0 user@yourPC ~ $ docker push 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-ecs-infra-example-api:0.1.0
タスク定義を登録する
タスク定義を入力する項目はAmazon ECSのサイドバーに位置しています。
新しいタスク定義の作成をクリックし、JSONを使用した新しいタスク定義の作成に行きます。 JSONファイルのコードを入力するテキストボックス画面が現れます。
このテキストボックスに以下のコードを入力してください。
{ "containerDefinitions": [ { "name": "front", "cpu": 256, "essential": true, "healthCheck": { "command": [ "CMD-SHELL", "curl -f http://localhost:80/" ], "interval": 10, "retries": 3, "startPeriod": 30, "timeout": 5 }, "environment": [ { "name": "API_HOST", "value": "localhost" }, { "name": "API_PORT", "value": "5000" }, { "name": "NGINX_ENVSUBST_OUTPUT_DIR", "value": "/etc/nginx" } ], "image": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-ecs-infra-example-front:0.1.0", "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/cdk-ecs-infra-example", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "web" } }, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "dependsOn": [ { "condition": "HEALTHY", "containerName": "api" } ] }, { "name": "api", "cpu": 256, "essential": true, "healthCheck": { "command": [ "CMD-SHELL", "curl -f http://localhost:5000/hello" ], "interval": 10, "retries": 3, "startPeriod": 30, "timeout": 5 }, "image": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-ecs-infra-example-api:0.1.0", "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/cdk-ecs-infra-example", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "api" } } } ], "cpu": "512", "executionRoleArn": "arn:aws:iam::000000000000:role/ecsTaskExecutionRole", "family": "sample-fargate", "memory": "1024", "networkMode": "awsvpc", "requiresCompatibilities": [ "FARGATE" ], "runtimePlatform": { "cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX" } }
ECSサービスを作成する
ECSのページからタスク定義を登録した後、デプロイ からサービスの作成 をクリックします。
すでに用意しているクラスターを選択し、Fargate起動タイプ にしておきます。
デプロイ設定に行きます。アプリケーションタイプは サービス です。 サービス名は自由に決めて構いませんが、ここではexample-appとしています。
サービスタイプは レプリカ で、必要なタスクは 1 です。
VPCとサブネット、セキュリティグループの設定を行います。
作成するECSサービスにALBを紐づけます。
リスナーはHTTP、80番を指定します。
作成のボタンを押した後、サービス一覧で確認してみます。
進捗バーが緑色になったらサービスの作成が完了したことになります。
ALBのドメイン名をブラウザのアドレスバーに入力してアクセスして、目的のwebサービスを作成することができました。
まとめ
今回は入門をしてみたということで、同じ入門者が迷わないように自分の踏んだ足跡を詳しく記述したものになります*1 。サイトをできるだけ早く簡単に世に出すためにうまく使えるといいですね。
*1:個人情報とかはもちろんマスクをしていますので、コピペして使用する際は、適宜自分のアカウントに合うように変更をしてみてください。