AWS Lambda から ECS on Fargate コンテナ移行ハンズオン

1.はじめに

1.1.はじめに

前回 Lambda コンテナのハンズオンを実施しました。
こちらの、コンテナイメージをベースにどのような変更を加えれば ECSでデプロイ出来るのかと気になり、
本ハンズオンを実施しようと考えました。

Lambda コンテナのコードをコメントアウトしながら比較しますが、基本前回の構築は不要です。
参考として、以下 GitHub に前回構築したリポジトリへのリンクを記載します。

1.2.Amazon ECSとは?

  • AWS上でDockerコンテナを簡単にデプロイ・管理・スケーリングできる、フルマネージド型のコンテナオーケストレーションサービス

1.3.AWS Fargateとは?

  • Amazon ECSの実行基盤の1つである、ECSの実行基盤としては以下 2種類がある。
項番 種類 メリット デメリット
1 EC2 EC2インスタンスを自身で管理するため柔軟な設定が可能 管理コストがかかる
2 Fargate AWSが完全管理 柔軟さが低い

1.4.本ブログの構築イメージ

  • 本ブログの構成を記載する。

file

2.ハンズオン

2.1.前提

2.1.1.実行環境

  • AWS CloudShell 環境
    • Docker、AWS CLIはプリインストール済み
  • リージョン:バージニア北部
  • Amazon ECS/AWS Fargate

2.2.アプリケーションコード作成

2.2.1.LambdaとECSとのアプリケーションコード比較

クリック で コード表示

凡例
■:どちらにも存在しているが書き方が異なる(同じだが追加してる部分)
●:ECS だけにしか存在しない記載
★:同内容の記載

Lambda:


===== import文 ===== 
import json
from datetime import datetime
import pytz
 
===== 関数定義 =====
def lambda_handler(event, context):
 
===== 共通処理 =====
    # 東京時間の取得
    tokyo_tz = pytz.timezone('Asia/Tokyo')
    current_time = datetime.now(tokyo_tz).strftime('%Y-%m-%d %H:%M:%S %Z')
 
===== メッセージ取得 =====
    message = event.get('message', 'Default message from Lambda!')
 
===== レスポンス =====  
    return {
        'statusCode': 200,  # HTTP ステータスコード
        'body': json.dumps({
            'greeting': f'Hello from {message}!',
            'event': event,  # 受け取ったイベント全体を返す(デバッグ用)
            'requested_at': datetime.now(tokyo_tz).isoformat(),  # ISO形式の日時
            'display_time': current_time,  # 見やすい形式の日時
            'request_id': context.aws_request_id  # Lambdaリクエストの一意なID
        })
    }
  
ECS:


===== (■同定義あり 追加あり)import文 =====
import json
from datetime import datetime
import pytz
from fastapi import FastAPI  # Webフレームワーク用
from pydantic import BaseModel  # リクエスト検証用
 
===== (●ECSのみの定義)FastAPI初期化 =====
app = FastAPI()
 
===== (●ECSのみの定義)リクエストモデル定義 =====
class EventModel(BaseModel):
    message: str = "Default message from ECS!"
    key: str = ""
 
===== (●ECSのみの定義)ヘルスチェックエンドポイント =====
@app.get("/health")
async def health_check():
    return {"status": "healthy"}
 
===== (■同定義あり 書き方修正)関数定義 =====
@app.post("/invoke")
async def invoke(event: EventModel):
 
===== (★同内容)共通処理 =====
    # 東京時間の取得
    tokyo_tz = pytz.timezone('Asia/Tokyo')
    current_time = datetime.now(tokyo_tz).strftime('%Y-%m-%d %H:%M:%S %Z')
 
===== (■同定義あり 書き方修正)メッセージ取得 =====    
    Emessage = event.message  # pydanticモデルとして直接アクセス
 
===== (■同定義あり 書き方修正)レスポンス =====
    return {
        'statusCode': 200,
        'body': {
            'greeting': f'Hello from {event.message}!',
            'event': event.dict(),
            'requested_at': datetime.now(tokyo_tz).isoformat(),
            'display_time': current_time,
            'server_type': 'ECS',  # ECSであることを明示
        }
    }
 
===== (●ECSのみの定義)HTTPサーバ起動 =====
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8080)
  

2.2.2.ECSで必要になった定義について

2.2.2.1.FastAPIについて
  • Lambdaと異なり、HTTPリクエストの受信・処理・レスポンス返却機能を実装する必要がある
  • FastAPIは、HTTPリクエストとPython関数を結びつけるフレームワークとして機能
    2.2.2.2.リクエストモデル定義について
  • Lambdaでは任意の形式のeventを自由に受け取れたが、ECSではリクエストボディの形式を明示的に定義する必要がある
  • 送信可能なデータ構造を明確にするために定義
    2.2.2.3.ヘルスチェックエンドポイントについて
  • ECSサービスの運用に必須の機能で、コンテナが正常に起動・稼働しているかを継続的に監視
    2.2.2.4.HTTPサーバ起動について
  • LambdaはAWSがリクエスト時のみ実行する構造だが、ECSは継続的に稼働しているため、リクエストを常時待ち受ける実装が必須

2.2.3.app.py作成

  • Hello from {message}を返す ECS
# プロジェクトディレクトリに移動
mkdir my-python-ecs && cd my-python-ecs

# アプリケーション作成
cat > app.py << EOF
import json
from datetime import datetime
import pytz
from fastapi import FastAPI  # Webフレームワーク用
from pydantic import BaseModel  # リクエスト検証用

# FastAPI初期化
app = FastAPI()

# リクエストモデル定義
class EventModel(BaseModel):
    message: str = "Default message from ECS!"
    key: str = ""

# ヘルスチェックエンドポイント
@app.get("/health")
async def health_check():
    return {"status": "healthy"}

# 関数定義
@app.post("/invoke")
async def invoke(event: EventModel):

    # 処理内容
    # 東京時間の取得
    tokyo_tz = pytz.timezone('Asia/Tokyo')
    current_time = datetime.now(tokyo_tz).strftime('%Y-%m-%d %H:%M:%S %Z')

    # メッセージ取得    
    Emessage = event.message  # pydanticモデルとして直接アクセス

    #レスポンス
    return {
        'statusCode': 200,
        'body': {
            'greeting': f'Hello from {event.message}!',
            'event': event.dict(),
            'requested_at': datetime.now(tokyo_tz).isoformat(),
            'display_time': current_time,
            'server_type': 'ECS',  # ECSであることを明示
        }
    }

# HTTPサーバ起動
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8080)
EOF

2.2.4.requirements.txt作成

  • 新たに追加されたものに関してコメントを記載する
# 必要パッケージを記載
cat > requirements.txt << EOF
pytz       # ★同内容タイムゾーン処理に特化したライブラリ
fastapi    # ●ECSのみ:HTTPサーバーフレームワーク 
uvicorn    # ●ECSのみ:ASGIサーバー(FastAPI実行用)
pydantic   # ●ECSのみ:データバリデーション用 (FastAPIが依存)
requests   # ●ECSのみ:DockerfileのHEALTHCHECKで使用
EOF

2.2.5.LambdaとECSとの Dockerfile比較

クリック で コード表示

凡例
■:どちらにも存在しているが書き方が異なる(同じだが追加してる部分)
●:ECS だけにしか存在しない記載
★:同内容の記載

Lambda:


===== 実行環境 =====   
# AWS公式よりPython 3.11 環境を利用
FROM public.ecr.aws/lambda/python:3.11
 
===== アプリケーションをコピー =====
COPY lambda_function.py requirements.txt ./
 
===== 依存関係をインストール =====    
RUN pip install --no-cache-dir -r requirements.txt
 
===== コマンド実行 =====
CMD ["lambda_function.lambda_handler"]
  
ECS:


===== (■同定義あり 書き方修正)実行環境 =====    
# 汎用的なPythonイメージを使用(Lambda特化じゃない)
FROM public.ecr.aws/docker/library/python:3.11-slim
 
===== (●ECSのみの定義)作業ディレクトリ設定 =====
WORKDIR /app
 
===== (★同内容)アプリケーションをコピー =====
COPY app.py requirements.txt ./
 
===== (★同内容)依存関係をインストール =====    
RUN pip install --no-cache-dir -r requirements.txt
 
===== (■同定義あり 書き方修正)コマンド実行 =====    
# コンテナ起動時に実行するコマンド
CMD ["python", "app.py"]
 
===== (●ECSのみの定義)ECS ヘルスチェック =====
HEALTHCHECK --interval=30s --timeout=10s \
  CMD python -c "import requests; requests.get('http://localhost:8080/health').raise_for_status()"
  

2.2.6.ECSで必要になった定義について

2.2.6.1.作業ディレクトリ設定について
  • Lambdaは専用イメージで暗黙的に設定されているが、ECSは明示的に設定が必要(柔軟性が高い)
2.2.6.2.ECS ヘルスチェックについて

2.2.7.Dockerfile作成

cat > Dockerfile << EOF
# 汎用的なPythonイメージを使用(Lambda特化じゃない)
FROM public.ecr.aws/docker/library/python:3.11-slim

# 作業ディレクトリを設定
WORKDIR /app

# アプリケーションをコピー
COPY app.py requirements.txt ./

# 依存関係をインストール
RUN pip install --no-cache-dir -r requirements.txt

# コンテナ起動時に実行するコマンド
CMD ["python", "app.py"]

# ECSヘルスチェック用
HEALTHCHECK --interval=30s --timeout=10s \
  CMD python -c "import requests; requests.get('http://localhost:8080/health').raise_for_status()"
EOF

2.3.ECR作成

2.3.1.ECRリポジトリ作成

# 変数設定
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=us-east-1
ECR_REPO_NAME="lambda-to-ecs"
ECR_IMAGE_URI=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPO_NAME

# ECRリポジトリ作成
aws ecr create-repository --repository-name $ECR_REPO_NAME --region $REGION

# ECRにログイン
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com

2.4.Dockerイメージ作成

2.4.1.ビルドとプッシュ手順

# Dockerイメージビルド
docker build -t $ECR_IMAGE_URI:latest .

# ECRにプッシュ
docker push $ECR_IMAGE_URI:latest

2.5.Amazon ECSの設定

2.5.1.ECSクラスター作成

  • Amazon ECSでタスクやサービスを実行する コンテナ実行基盤をまとめる論理グループ の作成
# 変数設定
CLUSTER="lambda-to-ecs-cluster"

# ECSクラスター作成
aws ecs create-cluster --cluster-name $CLUSTER

2.5.2.IAMロール(TaskExecutionRole)作成

2.5.2.1.IAMロールの違いについて
  • ECSには以下2種類のIAMロールがあり、今回はTaskExecutionRole(タスク実行ロール)のみを作成する。
  • 今回のコンテナ(Fargate)は、AWSサービスの権限が必要な挙動はないためecsTaskRole(タスクロール)は作成していない。
項番 ロールの種類 アタッチ先 概要
1 ecsTaskExecutionRole(タスク実行ロール) ECS ECSがコンテナを起動する際に使用
CloudWatch Logsに書き込み等の権限
2 ecsTaskRole(タスクロール) コンテナ(Fargate, EC2) コンテナ内のアプリが実行中に使用
AWSサービス(S3やDynamoDBなど)の権限
2.5.2.2.IAM((ecsTaskExecutionRole(タスク実行ロール))作成
# 変数設定
ROLE_NAME="ecsTaskExecutionRole"

# IAMロール作成
aws iam create-role --role-name $ROLE_NAME --assume-role-policy-document '{
  "Version": "2012-10-17",
  "Statement": [
  {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}'

# ポリシーをアタッチ
aws iam attach-role-policy --role-name $ROLE_NAME --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

# 実行ロールARNを変数に保存
EXECUTION_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}"

2.5.3.タスク定義(Fargate用)作成

2.5.3.1.タスク定義について
  • コンテナの設定や起動方法などを記述した コンテナの設計書の作成
  • 2.2.アプリケーションコード作成の Dockerfileは「アプリケーション本体の基盤の設計図」で、本設定は「その基盤を載せるためのサーバの設計図」である
  • 上記を分けることにより アプリケーションインフラの設定を切り分けることが可能となる
2.5.3.2.タスク定義の設定内容
項番 項目 設定値 説明
1 family lambda-to-ecs-task タスク定義のグループ名(リビジョン管理用)
2 networkMode awsvpc Fargate必須。タスクごとに独立したENIを割り当て
3 requiresCompatibilities FARGATE Fargateで実行することを明示
4 cpu 256 CPUユニット(0.25 vCPU)
5 memory 512 メモリサイズ(MB)
6 containerDefinitions.name lambda-to-ecs-container コンテナ名
7 containerDefinitions.image ${ECR_IMAGE_URI} 使用するDockerイメージ
8 containerDefinitions.essential true このコンテナがタスクの必須コンポーネント
9 portMappings.containerPort 8080 コンテナが公開するポート番号
10 portMappings.protocol tcp プロトコル種類
11 healthCheck.command ["CMD-SHELL", "python -c \"import requests; requests.get(‘http://localhost:8080/health’).raise_for_status()\""] requestsライブラリを使用し、HTTPリクエストを送信
コンテナのヘルス状態を監視
12 healthCheck.interval 30 ヘルスチェック間隔(秒)
13 healthCheck.timeout 5 ヘルスチェックタイムアウト(秒)
14 healthCheck.retries 3 失敗時のリトライ回数
2.5.3.3.タスク定義作成
# タスク定義JSONを作成
cat > lambda-to-ecs-task-definition.json << EOF
{
  "family": "lambda-to-ecs-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "${EXECUTION_ROLE_ARN}",
  "containerDefinitions": [
    {
      "name": "lambda-to-ecs-container",
      "image": "${ECR_IMAGE_URI}:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "python -c \"import requests; requests.get('http://localhost:8080/health').raise_for_status()\""],
        "interval": 30,
        "timeout": 5,
        "retries": 3
      }
    }
  ]
}
EOF

2.5.4.コンテナ(Fargate)基盤のタスク定義 登録

# グループとタスク定義ファイルの紐づけ
aws ecs register-task-definition --cli-input-json file://lambda-to-ecs-task-definition.json

2.5.5.SG(コンテナ(タスク)毎の)作成

  • タスクごとに独立したENI(ネットワークインターフェース)に付与される
  • 今回 PoCのため 0.0.0.0/0(全開放) から port 8080へのアクセスを許可している(※ 本番環境では特定のセキュリティグループのみを許可するような設計ください)
# 変数設定
SG_NAME="ecs-sg"

# セキュリティグループ作成
SG_ID=$(aws ec2 create-security-group --group-name $SG_NAME --description "ECS Security Group" --query 'GroupId' --output text)

# 8080ポート開放
aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port 8080 --cidr 0.0.0.0/0

2.5.6.ECS サービス作成

# サブネットID取得(ECSサービスの実行サブネット)
SUBNET_IDS=$(aws ec2 describe-subnets --query 'Subnets[*].SubnetId' --output text | tr '\t' ',')

# ECS サービス作成
aws ecs create-service \
  --cluster $CLUSTER \
  --service-name lambda-to-ecs-service \
  --task-definition lambda-to-ecs-task \
  --desired-count 1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[$SUBNET_IDS],securityGroups=[$SG_ID],assignPublicIp=ENABLED}"
  • 2回目以降(タスク定義を更新した場合など)は、以下コマンドで実施
# (2回目以降)コマンド
aws ecs update-service \
  --cluster $CLUSTER \
  --service lambda-to-ecs-service \
  --task-definition lambda-to-ecs-task \
  --force-new-deployment

2.6.挙動確認

2.6.1.コマンド実行

# ECSタスクのIPアドレスを取得
TASK_ARN=$(aws ecs list-tasks --cluster lambda-to-ecs-cluster --service-name lambda-to-ecs-service --query 'taskArns[0]' --output text)
ENI_ID=$(aws ecs describe-tasks --cluster lambda-to-ecs-cluster --tasks $TASK_ARN --query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' --output text)
PUBLIC_IP=$(aws ec2 describe-network-interfaces --network-interface-ids $ENI_ID --query 'NetworkInterfaces[0].Association.PublicIp' --output text)

# テストイベントを作成
echo '{"key": "value", "message": "ECS on Fargate"}' > test-event.json

# ECSテスト
curl -X POST http://$PUBLIC_IP:8080/invoke \
  -H "Content-Type: application/json" \
  -d @test-event.json \
  -o response.json

2.6.2.実行結果

2.6.2.1.コンソールの出力

項番 表示名 詳細内容
1 % Total 転送の全体的な進捗パーセンテージ(100%完了)
2 % Received 受信したデータのパーセンテージ(229バイト、100%)
3 % Xferd 送信したデータのパーセンテージ(45バイト、100%)
4 Average Speed Dload ダウンロード平均速度(43199バイト/秒)
5 Average Speed Upload アップロード平均速度(8488バイト/秒)
6 Time Total/Spent/Left 合計時間/経過時間/残り時間(すべて0秒)
7 Speed 現在の速度(54800バイト/秒)
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   274  100   229  100    45  43199   8488 --:--:-- --:--:-- --:--:-- 54800

2.6.2.2.response.jsonの内容

  • 適宜改行して記載
  • messageが「ECS on Fargate」となっていることが確認できる
  • keyに「value」が設定されてることが確認できる
  • server_typeは「ECS」になってることが確認できる
{
  "statusCode": 200,
  "body": {
    "greeting": "Hello from ECS on Fargate!",
    "event": {
      "message": "ECS on Fargate",
      "key": "value"
    },
    "requested_at": "2025-05-06T21:30:09.060311+09:00",
    "display_time": "2025-05-06 21:30:09 JST",
    "server_type": "ECS"
  }
}

3.クリーンアップ

# ECSリソース削除
aws ecs delete-service --cluster lambda-to-ecs-cluster --service lambda-to-ecs-service --force
aws ecs delete-cluster --cluster lambda-to-ecs-cluster

# ECRイメージ削除
aws ecr delete-repository --repository-name lambda-to-ecs --force

# IAMロール削除
# ポリシーをデタッチ
aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

# ロールを削除
aws iam delete-role --role-name $ROLE_NAME

# セキュリティグループ削除
aws ec2 delete-security-group --group-id $SG_ID

4. 終わりに

4.1. 得られた知見

  • 同じコンテナでも実行環境で大きな違いがある

    • Lambda は完全なイベントドリブン
    • ECS は HTTP サーバーとしての継続動作が必要
  • 使い分けの目安

    • Lambda を選ぶ場合:
    • 短時間・不定期な処理
    • 運用コストを最小化したい
    • ECS を選ぶ場合:
    • 常時稼働が必要なサービス
    • より細かいリソース制御が必要

4.2.今後の課題

  • CI/CDパイプラインの構築
  • Task Role を使用した他のAWSサービスとの連携

4.3.サンプルコード

本ハンズオンで使用したコードは以下のリポジトリで公開しています:

Last modified: 2025-05-06

Author