0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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.本ブログの構築イメージ

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

image.png

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イメージを使用
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イメージを使用
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.サンプルコード

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?