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
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)
}
※今回は、Lambda標準のパッケージを使っているため、追加するパッケージはありません。
GET /todo/{id}
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)
}
GET /todo
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)
}
PUT /todo/{id}
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'])
}
DELETE /todo/{id}
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': ''
}
SAMによるデプロイ
SAMを使って、API Gateway、Lambda、DynamoDBをデプロイします。
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を作成してもよいですし、以下のような定義を事前に作成することも可能です。
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に変換します。
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を呼び出します。
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)
boto3
fastapi
uvicorn
Dockerfile
上記で作成したmain.pyとlambda_adapter.pyをコンテナ内にコピーします。
また、利用するLambdaのコードもあわせてコンテナ内にコピーしています。
# ベースイメージとして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
エントリーポイント
#!/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
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
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
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
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を作成します。(一例です)
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"context": {}
}
{
"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"
}
}
{
"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によるデプロイ
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