1
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?

LambdaからECS(Python FastAPI)に移行

Last updated at Posted at 2025-03-30

PoCなどでコストメリットのあるLambdaベースで開発を進め、性能問題などが発生し、ECSに移行するケースがあると思います。その際に、LambdaとECSを並行で進めるため、コードを共通化したいというケースを想定し、Lambda用のアダプターを作成して、ECSで動かす事例を紹介します。

Lambdaを使ったToDo APIな

まずは、API Gateway + Lambda + DynamoDBを使った簡単なAPIを作成します。

API Path HTTP Method CodeUri Handler 説明
/todo POST ./src/create_todo app.lambda_handler リクエストボディから新規ToDo項目を作成し、DynamoDBに保存する。
/todo/{id} GET ./src/get_todo app.lambda_handler 指定されたIDのToDo項目をDynamoDBから取得する。
/todo GET ./src/list_todos app.lambda_handler DynamoDB上の全ToDo項目の一覧を返す。
/todo/{id} PUT ./src/update_todo app.lambda_handler 指定されたIDのToDo項目の内容を更新する。
/todo/{id} DELETE ./src/delete_todo app.lambda_handler 指定されたIDのToDo項目をDynamoDBから削除する。

ディレクトリ構造

ディレクトリ構造は以下のようにしています。
簡単なAPIなので一つのコードにまとめることもできますが、各APIを疎結合にするため、別々のディレクトリとしました。

todo-api-python
├── samconfig.toml
├── src
│   ├── create_todo
│   │   ├── app.py
│   │   └── requirements.txt
│   ├── delete_todo
│   │   ├── app.py
│   │   └── requirements.txt
│   ├── get_todo
│   │   ├── app.py
│   │   └── requirements.txt
│   ├── list_todos
│   │   ├── app.py
│   │   └── requirements.txt
│   └── update_todo
│       ├── app.py
│       └── requirements.txt
└── template.yaml

POST /todo

./src/create_todo/app.py
import boto3
import json
import uuid
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('todos')

def lambda_handler(event, context):
    body = json.loads(event['body'])
    task = body['task']

    todo_id = str(uuid.uuid4())
    created_at = datetime.utcnow().isoformat()

    item = {
        'id': todo_id,
        'task': task,
        'completed': False,
        'created_at': created_at
    }

    table.put_item(Item=item)

    return {
        'statusCode': 201,
        'body': json.dumps(item)
    }
./src/create_todo/requirements.txt

※今回は、Lambda標準のパッケージを使っているため、追加するパッケージはありません。

GET /todo/{id}

./src/get_todo/app.py
import boto3
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('todos')

def lambda_handler(event, context):
    todo_id = event['pathParameters']['id']

    response = table.get_item(Key={'id': todo_id})

    item = response.get('Item')
    if not item:
        return {
            'statusCode': 404,
            'body': json.dumps({'message': 'ToDo not found'})
        }

    return {
        'statusCode': 200,
        'body': json.dumps(item)
    }
./src/get_todo/requirements.txt

GET /todo

./src/list_todos/app.py
import boto3
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('todos')

def lambda_handler(event, context):
    response = table.scan()

    items = response.get('Items', [])

    return {
        'statusCode': 200,
        'body': json.dumps(items)
    }
./src/list_todos/requirements.txt

PUT /todo/{id}

./src/update_todo/app.py
import boto3
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('todos')

def lambda_handler(event, context):
    todo_id = event['pathParameters']['id']
    body = json.loads(event['body'])

    update_expression = "SET task = :task, completed = :completed"
    expression_values = {
        ':task': body['task'],
        ':completed': body['completed']
    }

    response = table.update_item(
        Key={'id': todo_id},
        UpdateExpression=update_expression,
        ExpressionAttributeValues=expression_values,
        ReturnValues="ALL_NEW"
        )

    return {
        'statusCode': 200,
        'body': json.dumps(response['Attributes'])
    }
./src/update_todo/requirements.txt

DELETE /todo/{id}

./src/delete_todo/app.py
import boto3
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('todos')

def lambda_handler(event, context):
    todo_id = event['pathParameters']['id']

    table.delete_item(Key={'id': todo_id})

    return {
        'statusCode': 204,
        'body': ''
    }
./src/delete_todo/requirements.txt

SAMによるデプロイ

SAMを使って、API Gateway、Lambda、DynamoDBをデプロイします。

./template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Description: |
  ToDo application backend with AWS Lambda, API Gateway, and DynamoDB

Globals:
  Function:
    Runtime: python3.9
    MemorySize: 128
    Timeout: 10

Resources:

  TodosTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: todos
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  CreateTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/create_todo
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TodosTable
      Events:
        CreateTodo:
          Type: Api
          Properties:
            Path: /todo
            Method: post

  GetTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/get_todo
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TodosTable
      Events:
        GetTodo:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: get

  ListTodosFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/list_todos
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TodosTable
      Events:
        ListTodos:
          Type: Api
          Properties:
            Path: /todo
            Method: get

  UpdateTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/update_todo
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TodosTable
      Events:
        UpdateTodo:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: put

  DeleteTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/delete_todo
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TodosTable
      Events:
        DeleteTodo:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: delete

Outputs:
  ApiEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"

sam deploy --guidedとして、samconfig.tomlを作成してもよいですし、以下のような定義を事前に作成することも可能です。

./samconfig.toml
version = 0.1
[default.deploy.parameters]
stack_name = "todo-api-python"
resolve_s3 = true
s3_prefix = "todo-api-python"
region = "ap-northeast-1"
capabilities = "CAPABILITY_IAM"
image_repositories = []

以下のようにbuild & deployをします。

sam build
sam deploy

動作確認

export API_URL=<API Gateway endpoint URL>
curl -X POST ${API_URL}/todo \
-H "Content-Type: application/json" \
-d '{"task": "deploy todo api"}'
id=<登録されたToDoのID>
curl -X GET ${API_URL}/todo/${id}
curl -X PUT ${API_URL}/todo/${id} \
-H "Content-Type: application/json" \
-d '{"task": "update todo api", "completed": true}'
curl -X DELETE ${API_URL}/todo/${id}
curl -X GET ${API_URL}/todo

FastAPI to Lambdaアダプターの作成

Lambdaのコードをそのまま再利用してFastAPIに統合する、FastAPI to Lambdaアダプターを作成します。
このアダプターは、FastAPIのリクエストをLambdaのイベント形式に変換し、既存のLambdaハンドラを呼び出して、そのレスポンスをFastAPIのResponseに変換します。

./src/lambda_adapter.py
from fastapi import Request, Response

def lambda_adapter(lambda_handler):
    async def adapter(request: Request):
        body_bytes = await request.body()
        event = {
            "httpMethod": request.method,
            "headers": dict(request.headers),
            "queryStringParameters": dict(request.query_params),
            "pathParameters": request.path_params,
            "body": body_bytes.decode() if body_bytes else None,
        }

        context = {}

        lambda_response = lambda_handler(event, context)

        headers = lambda_response.get('headers', {})
        return Response(
            content=lambda_response.get('body', ''),
            status_code=lambda_response.get('statusCode', 200),
            headers=headers
        )
    return adapter

コンテナを作成

Lambdaのコードを実行するためのコンテナを作成します。

ディレクトリ構造

本当は、./dockerの配下にコンテナで利用するファイルをまとめ、./srcを取り込みたかったのですが、Dockerfileの上位のファイル/ディレクトリを参照することができなかったため、以下のようなディレクトリ構造としました。ビルド用のスクリプトを作成することで解決できますが、今回は、CDK内でコンテナをビルドする方針としたため、以下のような構造としています。

src
├── create_todo
│   ├── app.py
│   └── requirements.txt
├── delete_todo
│   ├── app.py
│   └── requirements.txt
├── Dockerfile
├── get_todo
│   ├── app.py
│   └── requirements.txt
├── lambda_adapter.py
├── list_todos
│   ├── app.py
│   └── requirements.txt
├── main.py
├── requirements.txt
└── update_todo
    ├── app.py
    └── requirements.txt

Lambdaを呼び出す処理

先ほど作成したアダプターを使って、Lambdaを呼び出します。

./src/main.py
from fastapi import FastAPI, Request
from lambda_adapter import lambda_adapter
from list_todos.app  import lambda_handler as list_todos_lambda_handler
from create_todo.app import lambda_handler as create_todo_lambda_handler
from get_todo.app    import lambda_handler as get_todo_lambda_handler
from update_todo.app import lambda_handler as update_todo_lambda_handler
from delete_todo.app import lambda_handler as delete_todo_lambda_handler

app = FastAPI()

# GET /health:
@app.get("/health")
async def health_check():
    return {"status": "ok"}

# GET /todo
@app.get("/todo")
async def list_todos(request: Request):
    return await lambda_adapter(list_todos_lambda_handler)(request)

# POST /todo
@app.post("/todo")
async def todo_get(request: Request):
    return await lambda_adapter(create_todo_lambda_handler)(request)

# GET /todo/{id}
@app.get("/todo/{id}")
async def todo_get(request: Request):
    return await lambda_adapter(get_todo_lambda_handler)(request)

# PUT /todo/{id}
@app.put("/todo/{id}")
async def todo_get(request: Request):
    return await lambda_adapter(update_todo_lambda_handler)(request)

# DELETE /todo/{id}
@app.delete("/todo/{id}")
async def todo_get(request: Request):
    return await lambda_adapter(delete_todo_lambda_handler)(request)
./src/requirements.txt
boto3
fastapi
uvicorn

Dockerfile

上記で作成したmain.pyとlambda_adapter.pyをコンテナ内にコピーします。
また、利用するLambdaのコードもあわせてコンテナ内にコピーしています。

Dockerfile
# ベースイメージとしてpython3.9-slimを使用
FROM python:3.9-slim

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

# ルートのrequirements.txtをコピーして依存関係をインストール
COPY requirements.txt .
RUN pip install --upgrade pip && \
    pip install -r requirements.txt

# main.py と lambda_adapter.py をコピー
COPY main.py .
COPY lambda_adapter.py .

# 各モジュールディレクトリを、ディレクトリ名を維持してコピー
COPY create_todo/ create_todo/
COPY delete_todo/ delete_todo/
COPY get_todo/ get_todo/
COPY list_todos/ list_todos/
COPY update_todo/ update_todo/

# コンテナが使用するポートを開放(FastAPIのデフォルトポート)
EXPOSE 8000

# FastAPIアプリ(main.py内の app)をuvicornで起動
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

CDKでコンテナをデプロイ

次に、コンテナをECS上にデプロイするため、AWS CDKを利用して以下のリソースを作成します。

以下は、それぞれのスタックについての説明です。

VpcStack

  • プライベート(isolated)サブネットのみの VPC を作成します。
  • S3、DynamoDB、ECR、CloudWatch Logs にアクセスするための必要な VPC エンドポイント(Gateway または Interface 型)を構築し、内部通信を効率化・セキュアにしています。

EcrStack

  • ローカルの Docker コンテキストからコンテナイメージをビルドし、プライベートな ECR リポジトリにプッシュする役割を担います。
  • このスタックで作成したイメージは、後続の ECS サービスで利用されます。

NlbStack

  • プライベートサブネットに NLB(Network Load Balancer)を作成します。
  • NLB 自体はセキュリティグループを持たないため、NLB のトラフィックを受ける ECS ターゲットに対して、VPC 内からの HTTP (TCP 80) トラフィックを許可するためのセキュリティグループを別途作成・設定します。

EcsStack

  • ECS クラスターを作成し、Fargate タスク定義とサービスを構築します。
  • タスク定義では、EcrStackでビルドした FastAPI コンテナイメージを利用してコンテナを起動し、FastAPI アプリケーションを実行します。
  • また、対象となる ECS ターゲットのセキュリティグループに、NLB からのトラフィック(例えば、TCP 8000 へのアクセス)を許可するルールを追加し、NLB 経由でのアクセスが確実に届くように設定しています。

ディレクトリ構造

cdk
├── bin
│   └── app.ts
├── cdk.json
├── lib
│   ├── ecr-stack.ts
│   ├── ecs-stack.ts
│   ├── nlb-stack.ts
│   └── vpc-stack.ts
├── package.json
└── tsconfig.json

エントリーポイント

./cdk/bin/app.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { EcrStack } from '../lib/ecr-stack';
import { NlbStack } from '../lib/nlb-stack';
import { EcsStack } from '../lib/ecs-stack';

const app = new cdk.App();

// VPCスタックの作成
const vpcStack = new VpcStack(app, 'VpcStack');

// Dockerイメージビルド&ECRデプロイ用スタックの作成
const ecrStack = new EcrStack(app, 'EcrStack');

// NLBスタック(VPCはVpcStackから)
const nlbStack = new NlbStack(app, 'NlbStack', {
  vpc: vpcStack.vpc,
});

// ECSスタック(VPC、ECRのイメージ、NLBのリスナー・セキュリティグループを参照)
new EcsStack(app, 'EcsStack', {
  vpc: vpcStack.vpc,
  dockerImageAsset: ecrStack.imageAsset,
  nlbListener: nlbStack.listener,
  nlbSecurityGroup: nlbStack.nlbSecurityGroup,
});

VpcStack

./cdk/lib/vpc-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class VpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;
  
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // マルチAZ、プライベート(isolated)サブネットのみのVPCを作成
    this.vpc = new ec2.Vpc(this, 'MyVpc', {
      maxAzs: 2,
      natGateways: 0,
      subnetConfiguration: [
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });

    // S3用ゲートウェイエンドポイントを追加
    this.vpc.addGatewayEndpoint('S3Endpoint', {
      service: ec2.GatewayVpcEndpointAwsService.S3,
      subnets: [{
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      }],
    });

    // DynamoDB用ゲートウェイエンドポイントを追加
    this.vpc.addGatewayEndpoint('DynamoDbEndpoint', {
      service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
      subnets: [{
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      }],
    });

    // ECR API用インターフェースエンドポイントを追加
    this.vpc.addInterfaceEndpoint('EcrApiEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
      subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
    });

    // ECR Docker用インターフェースエンドポイントを追加
    this.vpc.addInterfaceEndpoint('EcrDkrEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
      subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
    });

    // CloudWatch Logs用インターフェースエンドポイントを追加
    this.vpc.addInterfaceEndpoint('CloudWatchLogsEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
      subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
    });
  }
}

EcrStack

./cdk/lib/ecr-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets';

export class EcrStack extends cdk.Stack {
  // DockerImageAssetが作成したイメージを後続スタックで利用
  public readonly imageAsset: ecr_assets.DockerImageAsset;
  
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Dockerfileが配置されているディレクトリ(ここではプロジェクトルート)からDockerイメージをビルド  
    // repositoryNameを指定すると、新規のプライベートECRリポジトリが作成されます
    this.imageAsset = new ecr_assets.DockerImageAsset(this, 'TodoApiImage', {
      directory: '../src', // Dockerfileのあるディレクトリ
    });
  }
}

NlbStack

./cdk/lib/nlb-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';

interface NlbStackProps extends cdk.StackProps {
  vpc: ec2.Vpc;
}

export class NlbStack extends cdk.Stack {
  public readonly loadBalancer: elbv2.NetworkLoadBalancer;
  public readonly nlbSecurityGroup: ec2.SecurityGroup;
  public readonly listener: elbv2.NetworkListener;
  
  constructor(scope: Construct, id: string, props: NlbStackProps) {
    super(scope, id, props);

    // セキュリティグループ
    this.nlbSecurityGroup = new ec2.SecurityGroup(this, 'NlbSecurityGroup', {
      vpc: props.vpc,
      description: 'Security group for ECS targets behind the NLB',
      allowAllOutbound: true,
    });

    // VPC内からのHTTPを許可
    this.nlbSecurityGroup.addIngressRule(
      ec2.Peer.ipv4(props.vpc.vpcCidrBlock),
      ec2.Port.tcp(80),
      'Allow inbound HTTP from within the VPC'
    );

    // プライベートサブネット(isolated)にNLBを作成、マルチAZ対応
    this.loadBalancer = new elbv2.NetworkLoadBalancer(this, 'MyNLB', {
      vpc: props.vpc,
      internetFacing: false,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      },
      crossZoneEnabled: true,
      securityGroups: [this.nlbSecurityGroup]
    });

    // リスナーの作成(例としてTCPポート80)
    this.listener = this.loadBalancer.addListener('Listener', {
      port: 80,
      protocol: elbv2.Protocol.TCP,
    });
  }
}

EcsStack

./cdk/lib/ecs-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';

interface EcsStackProps extends cdk.StackProps {
  vpc: ec2.Vpc;
  dockerImageAsset: ecr_assets.DockerImageAsset;
  nlbListener: elbv2.NetworkListener;
  nlbSecurityGroup: ec2.SecurityGroup;
}

export class EcsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: EcsStackProps) {
    super(scope, id, props);

    // ECSクラスター作成(マルチAZ)
    const cluster = new ecs.Cluster(this, 'EcsCluster', {
      vpc: props.vpc,
    });

    // タスク実行用IAMロール(DynamoDB利用可能な権限を付与)
    const taskRole = new iam.Role(this, 'EcsTaskRole', {
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
    });
    taskRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'));

    // Fargateタスク定義の作成
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
      taskRole: taskRole,
    });

    // ECRに格納したDockerイメージを利用するコンテナの追加  
    // ※main.py内でFastAPIアプリ(ポート8000)を実行する前提
    const container = taskDefinition.addContainer('AppContainer', {
      image: ecs.ContainerImage.fromDockerImageAsset(props.dockerImageAsset),
      logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'ecs' }),
    });
    container.addPortMappings({
      containerPort: 8000,
    });

    // ECS用のセキュリティグループを作成し、同一VPC内からのTCP:8000通信を許可
    const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
      vpc: props.vpc,
      description: 'Allow inbound TCP 8000 from within the VPC',
      allowAllOutbound: true,
    });
    // VPC CIDR を指定して、同一VPC内からのアクセスを許可
    ecsSecurityGroup.addIngressRule(
      ec2.Peer.ipv4(props.vpc.vpcCidrBlock),
      ec2.Port.tcp(8000),
      'Allow inbound TCP 8000 from within the VPC'
    );
    // NLBのセキュリティグループを指定して、NLBからのアクセスを許可
    ecsSecurityGroup.addIngressRule(
      ec2.Peer.securityGroupId(props.nlbSecurityGroup.securityGroupId),
      ec2.Port.tcp(8000),
      'Allow inbound TCP 8000 from NLB security group'
    );

    // Fargateサービスの作成(プライベートサブネット配置、マルチAZ対応)
    const service = new ecs.FargateService(this, 'FargateService', {
      cluster,
      taskDefinition,
      assignPublicIp: false,
      securityGroups: [ecsSecurityGroup],
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      },
      desiredCount: 2,
    });

    // ECSサービス作成後、NLBリスナーに直接サービスを登録し、ヘルスチェック設定を追加
    props.nlbListener.addTargets('EcsTargets', {
      port: 8000,
      targets: [service],
      healthCheck: {
        path: '/health',
        healthyHttpCodes: "200", // 追加:正常時に返されるHTTPコード
        protocol: elbv2.Protocol.HTTP,
        interval: cdk.Duration.seconds(30),
        timeout: cdk.Duration.seconds(5),
        healthyThresholdCount: 2,
        unhealthyThresholdCount: 2,
      },
    });
  }
}

ECSのタスク定義は必要に応じてリソースやカーネルパラメータのチューニングが必要です。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.ContainerDefinitionOptions.html

チューニングの例
    const container = taskDefinition.addContainer('MyContainer', {
      memoryLimitMiB: 512,
      logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'ecs' }),
      systemControls: [
        {
          namespace: 'net.ipv4.ip_forward',
          value: '1',
        },
        {
          namespace: 'net.core.somaxconn',
          value: '1024',
        },
        {
          namespace: 'net.ipv4.tcp_keepalive_time',
          value: '300',
        },
      ],
    });

CDKデプロイ

CDKのデプロイに必要なcdk.json、package.json、tsconfig.jsonを作成します。(一例です)

./cdk/cdk.json
{
  "app": "npx ts-node --prefer-ts-exts bin/app.ts",
  "context": {}
}
./cdk/package.json
{
  "name": "cdk-app",
  "version": "0.1.0",
  "bin": {
    "cdk-app": "bin/app.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "cdk": "cdk"
  },
  "dependencies": {
    "aws-cdk-lib": "^2.186.0",
    "constructs": "^10.4.2"
  },
  "devDependencies": {
    "typescript": "^4.9.5",
    "ts-node": "^10.9.2"
  }
}
./cdk/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2021",
    "module": "commonjs",
    "lib": ["ES2021"],
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": [
    "bin/**/*.ts",
    "lib/**/*.ts"
  ]
}

以下のようにデプロイします。

npm install
npx cdk bootstrap
npx cdk deploy --all

API Gatewayに登録

CDKで作成したECSをAPI Gatewayに登録します。
Lambda用に作成したAPI Gatewayとは別のAPI Gatewayを作成しています。

SAMによるデプロイ

./template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Description: |
  ToDo application backend with AWS Lambda, API Gateway, DynamoDB,
  and ECS integration via VPC Link with proxy+.

Globals:
  Function:
    Runtime: python3.9
    MemorySize: 128
    Timeout: 10

Parameters:
  # CDK でデプロイした ECS の NLB の DNS 名
  NlbDnsName:
    Type: String
    Description: "DNS name of the ECS service's NLB"

  # CDK でデプロイした ECS の NLB の ARN
  NlbArn:
    Type: String
    Description: "ARN of the ECS service's NLB"

Resources:

  TodosTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: todos
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  CreateTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/create_todo
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TodosTable
      Events:
        CreateTodo:
          Type: Api
          Properties:
            Path: /todo
            Method: post

  GetTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/get_todo
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TodosTable
      Events:
        GetTodo:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: get

  ListTodosFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/list_todos
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TodosTable
      Events:
        ListTodos:
          Type: Api
          Properties:
            Path: /todo
            Method: get

  UpdateTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/update_todo
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TodosTable
      Events:
        UpdateTodo:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: put

  DeleteTodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      CodeUri: ./src/delete_todo
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TodosTable
      Events:
        DeleteTodo:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: delete

  # VPCリンク作成: NLB の ARN をターゲットとする
  EcsVpcLink:
    Type: AWS::ApiGateway::VpcLink
    Properties:
      Name: EcsVpcLink
      TargetArns:
        - !Ref NlbArn

  # ECS 統合用 API を追加(proxy+ を利用)
  EcsApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      DefinitionBody:
        openapi: "3.0.1"
        info:
          title: "ECS Proxy API"
          version: "1.0"
        paths:
          /{proxy+}:
            x-amazon-apigateway-any-method:
              parameters:
                - name: proxy
                  in: path
                  required: true
                  schema:
                    type: string
              x-amazon-apigateway-integration:
                uri: !Sub "http://${NlbDnsName}/{proxy}"
                httpMethod: ANY
                type: http_proxy
                connectionType: VPC_LINK
                connectionId: !Ref EcsVpcLink
                requestParameters:
                  integration.request.path.proxy: method.request.path.proxy
                passthroughBehavior: when_no_match
                timeoutInMillis: 29000

Outputs:
  ApiEndpoint:
    Description: "API Gateway endpoint URL for Lambda functions"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"

  EcsApiEndpoint:
    Description: "API Gateway endpoint URL for ECS integration via VPC Link"
    Value: !Sub "https://${EcsApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"

以下のようにビルド&デプロイをします。

sam build
sam deploy \
  --parameter-overrides \
  NlbDnsName=<NLBのFQDN> \   
  NlbArn=<NLBのARN>

動作確認

export API_URL=<API Gateway endpoint URL for ECS integration via VPC Link>
curl -X POST ${API_URL}/todo \
-H "Content-Type: application/json" \
-d '{"task": "deploy todo api"}'
id=<登録されたToDoのID>
curl -X GET ${API_URL}/todo/${id}
curl -X PUT ${API_URL}/todo/${id} \
-H "Content-Type: application/json" \
-d '{"task": "update todo api", "completed": true}'
curl -X DELETE ${API_URL}/todo/${id}
curl -X GET ${API_URL}/todo

上記の例は、API Gatewayを利用していますが、ペイロードサイズの制限が問題となる場合は、API Gateway + NLBではなく、ALB(必要に応じて+CloudFront)で公開するなどの検討が必要です。

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/limits.html

リソース削除

template.yamlが存在するディレクトリに移動し、以下のコマンドを実行します。

sam delete

次に、CDKのディレクトリに移動し、以下のコマンドを実行します。

cd cdk
cdk destroy --all
1
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
1
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?