ECS(Fargate)でDynamoDBを使用したWebアプリを作成する
以下のページでプライベートサブネット内のECS(Fargate)をALBにアタッチする方法を紹介しました。
上記のページでは、NginxでWebサーバを立ち上げ、静的なHTMLファイルを返すコンテナをご紹介しました。
本ページでは、PythonでDynamoDBにアクセスする簡単なWebアプリをコンテナ化し、これをALBに関連付けます。
DynamoDBにアクセスし、全データを返すことを目指します。
構築する環境
基本的な構成は上述のページと同様です。
変更点は3点です。
1点目はFargate上で動作するコンテナです。
Python製Webフレームワークbottleを使って、コンテナをWebサーバとして動作させ、DynamoDBに保存されているデータを返します。
2点目はECRリポジトリへのイメージのプッシュ方法です。
今回は上述のコンテナ用のイメージを、CodeBuildを使ってビルドします。
CodeBuildの開始は、Lambda関数でトリガーします。
この関数をCloudFormationカスタムリソースに関連づけ、CloudFormationスタック作成時に自動的に実行されるように設定します。
3点目はDynamoDBです。
こちらもCloudFormationカスタムリソースに関連付けたLambda関数によって、スタック作成時に、自動的にDyanamoDBテーブルにアイテムを保存するように設定します。
なお保存するアイテム情報は、S3バケットにJSON形式で保存したものを参照します。
環境構築用のCloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置してます。
https://github.com/awstut-an-r/awstut-dva/tree/main/02/004
テンプレートファイルのポイント解説
本ページは、ECS(Fargate)コンテナにおいて、DynamoDBにアクセスする方法を中心に取り上げます。
プライベートサブネット内のリソースを、ALBにアタッチする方法については、以下のページをご確認ください。
Fargateの基本については、以下のページをご確認ください。
DynamoDB
VPCエンドポイント
Resources:
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref CidrIp3
VpcId: !Ref VPC
AvailabilityZone: !Sub "${AWS::Region}${AvailabilityZone1}"
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref CidrIp4
VpcId: !Ref VPC
AvailabilityZone: !Sub "${AWS::Region}${AvailabilityZone2}"
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PrivateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
DynamoDBEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
VpcId: !Ref VPC
Code language: YAML (yaml)
DynamoDBにアクセスするECSコンテナはプライベートサブネットに設置されているため、同リソースに接続するための経路を用意する必要があります。
今回はDynamoDB用のVPCエンドポイントを作成します。
プライベートサブネットからDynamoDBテーブルに接続する方法については、以下のページをご確認ください。
DynamoDBテーブル
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: Artist
AttributeType: S
- AttributeName: SongTitle
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: Artist
KeyType: HASH
- AttributeName: SongTitle
KeyType: RANGE
TableClass: STANDARD
TableName: !Sub "${Prefix}-Music"
Code language: YAML (yaml)
特別な設定は行いません。
以下のAWS公式ページで紹介されている構成を、CloudFormationテンプレートに書き出しました。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/getting-started-step-1.html
DynamoDBに関する基本的な事項に関しては、以下のページもご確認ください。
ECS
Resources:
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub "${Prefix}-cluster"
FargateTaskExecutionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
TaskRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: AllowSQSSendMessage
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- dynamodb:Scan
Resource:
- !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${Table}"
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryName}:latest"
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref Prefix
Name: !Sub "${Prefix}-bottle"
PortMappings:
- ContainerPort: !Ref BottlePort
HostPort: !Ref BottlePort
Environment:
- Name: TABLE_NAME
Value: !Ref Table
Cpu: !Ref TaskCpu
ExecutionRoleArn: !Ref FargateTaskExecutionRole
Memory: !Ref TaskMemory
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
RuntimePlatform:
CpuArchitecture: ARM64
OperatingSystemFamily: LINUX
TaskRoleArn: !Ref TaskRole
Service:
Type: AWS::ECS::Service
Properties:
Cluster: !Ref Cluster
DesiredCount: 1
LaunchType: FARGATE
LoadBalancers:
- ContainerName: !Sub "${Prefix}-bottle"
ContainerPort: !Ref BottlePort
TargetGroupArn: !Ref ALBTargetGroup
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ContainerSecurityGroup
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
ServiceName: !Sub "${Prefix}-service"
TaskDefinition: !Ref TaskDefinition
Code language: YAML (yaml)
ポイントは2点です。
1点目はタスク用ロールです。
インラインポリシーでDynamoDBにスキャンする権限を与えます。
2点目はタスク定義です。
Environmentプロパティでコンテナ内でアクセスできる環境変数が設定できますが、DynamoDBテーブルの名前を設定します。
(参考)CloudFormationカスタムリソースとCodeBuildを使用して、CloudFormationスタック作成時に、ECRリポジトリに自動的にテストイメージをプッシュする
Resources:
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: NO_ARTIFACTS
Cache:
Type: NO_CACHE
Environment:
ComputeType: !Ref ProjectEnvironmentComputeType
EnvironmentVariables:
- Name: BOTTLE_PORT
Type: PLAINTEXT
Value: !Ref BottlePort
- Name: DOCKERHUB_PASSWORD
Type: SECRETS_MANAGER
Value: !Sub "${Secret}:password"
- Name: DOCKERHUB_USERNAME
Type: SECRETS_MANAGER
Value: !Sub "${Secret}:username"
Image: !Ref ProjectEnvironmentImage
ImagePullCredentialsType: CODEBUILD
Type: !Ref ProjectEnvironmentType
PrivilegedMode: true
LogsConfig:
CloudWatchLogs:
Status: DISABLED
S3Logs:
Status: DISABLED
Name: !Ref Prefix
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
Type: NO_SOURCE
BuildSpec: !Sub |
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws --version
- aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
- REPOSITORY_URI=${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryName}
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${!COMMIT_HASH:=latest}
- echo Logging in to Docker Hub...
- echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
- |
cat << EOF > Dockerfile
FROM amazonlinux
RUN yum update -y && yum install python3 python3-pip -y
RUN pip3 install boto3 bottle
COPY main.py ./
CMD ["python3", "main.py"]
EXPOSE $BOTTLE_PORT
EOF
- |
cat << EOF > main.py
from bottle import Bottle, route, run
import boto3
import json
import os
TABLE_NAME = os.environ['TABLE_NAME']
app = Bottle()
dynamodb_client = boto3.client('dynamodb')
@app.route('/')
def scan():
response = dynamodb_client.scan(
TableName=TABLE_NAME
)
return json.dumps(response['Items'], indent=2)
if __name__ == '__main__':
run(app=app, host='0.0.0.0', port=$BOTTLE_PORT)
EOF
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
Visibility: PRIVATE
Code language: YAML (yaml)
ECS上で実行させるコンテナ用イメージは、手動でECRリポジトリにプッシュしたものを使用することができます。
ただし本ページでは、CodeBuildを使ってイメージを自動的にビルドし、ECRリポジトリにプッシュします。
ビルドの開始のトリガーは、CloudFormationカスタムリソースに紐づいたLambda関数を使用します。
詳細につきましては、以下のページをご確認ください。
ポイントはbuildspec.yml内の、pre_buildフェーズで実行する2つのコマンドです。
1点目はcatコマンドでDockerfileを作成する箇所です。
最新版のAmazon Linuxイメージをベースとして、Pythonやパッケージをインストール後、後述のスクリプトを実行する内容となります。
公開するポートの指定では、EnvironmentVariablesプロパティで指定した環境変数を使用します。
2点目は同じくcatコマンドでPythonスクリプトmain.pyを作成する箇所です。
Webフレームワークbottleを使用して、簡易的なWebサーバとして動作させる内容です。
ルートページにアクセスされた際に、DynamoDBテーブルをスキャンして、全アイテムを取得後、レスポンスとして返します。
アクセスするテーブルの名前は、環境変数を参照します。
(参考)CloudFormationカスタムリソースでDynamoDBを初期セットアップする
Resources:
Function2:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
JSON_S3_BUCKET: !Ref JsonS3Bucket
JSON_S3_KEY: !Ref JsonS3Key
TABLE_NAME: !Ref Table
Code:
ZipFile: |
import boto3
import cfnresponse
import json
import os
JSON_S3_BUCKET = os.environ['JSON_S3_BUCKET']
JSON_S3_KEY = os.environ['JSON_S3_KEY']
TABLE_NAME = os.environ['TABLE_NAME']
CREATE = 'Create'
response_data = {}
s3_client = boto3.client('s3')
dynamodb_client = boto3.client('dynamodb')
def lambda_handler(event, context):
try:
if event['RequestType'] == CREATE:
s3_response = s3_client.get_object(
Bucket=JSON_S3_BUCKET,
Key=JSON_S3_KEY)
body = s3_response['Body'].read()
print(body)
json_data = json.loads(body.decode('utf-8'))
print(json_data)
for item in json_data:
dynamodb_response = dynamodb_client.put_item(
TableName=TABLE_NAME,
Item=item
)
print(dynamodb_response)
cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
except Exception as e:
print(e)
cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
FunctionName: !Sub "${Prefix}-function2"
Handler: !Ref Handler
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole2.Arn
Timeout: !Ref Timeout
Code language: YAML (yaml)
CloudFormationスタック作成時に、自動的にDynamoDBにテストデータを保存します。
上記のLambda関数をCloudFormationカスタムリソースに関連づけることで、これを実現します。
詳細につきましては、以下のページをご確認ください。
ALBおよびECSのポート番号まとめ
今回の構成における通信を整理すると、以下の図となります。
ユーザからALBまでは80番ポート向けに通信が行われますが、ALBからECS(Fargate)までの通信はbottleアプリで使用するポート(8080)向けに通信が行われます。
上記の構成を実現するために、各リソースの各プロパティを以下の通りに設定します。
- AWS::ElasticLoadBalancingV2::Listener Port:80
- AWS::ElasticLoadBalancingV2::TargetGroup Port:8080
- AWS::ECS::Service LoadBalancers ContainerPort:8080
- AWS::ECS::TaskDefinition PortMappings ContainerPort:8080
- AWS::ECS::TaskDefinition PortMappings HostPort:8080
両リソースにアタッチするセキュリティグループは以下の通りです。
Resources:
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${Prefix}-ALBSecurityGroup
GroupDescription: Allow HTTP Only.
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref HTTPPort
ToPort: !Ref HTTPPort
CidrIp: 0.0.0.0/0
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub "${Prefix}-ContainerSecurityGroup"
GroupDescription: Allow HTTP from ALBSecurityGroup.
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref BottlePort
ToPort: !Ref BottlePort
SourceSecurityGroupId: !Ref ALBSecurityGroup
Code language: YAML (yaml)
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- ECRリポジトリ:dva-02-004
- DynamoDBテーブル:dva-02-004-Music
- ECSクラスター:dva-02-004-cluster
- ALBのドメイン:dva-02-004-alb-909547272.ap-northeast-1.elb.amazonaws.com
AWS Management Consoleから各リソースを確認します。
ECRリポジトリを確認します。
正常にリポジトリが作成されて、イメージがプッシュされています。
つまりCloudFormationカスタムリソースに紐づくLambda関数が実行されて、CodeBuildによってイメージのビルドが開始され、リポジトリにプッシュされたということです。
DynamoDBテーブルを確認します。
テーブルにデータが保存されています。
つまりCloudFormationカスタムリソースに紐づくLambda関数が実行されて、S3バケットに保存されているJSONデータが読み込まれて、DynamoDBテーブルにアイテムが保存されたということです。
ECSを確認します。
まずクラスターを確認します。
クラスター内に、1つのECSサービスが作成されています。
サービス内のタスクを確認します。
ALBに関連づいていることがわかります。
サービス内に1つのタスクが実行されていることも確認できます。
タスク内に、先述のECRリポジトリからイメージをプルし、コンテナが生成されています。
動作確認
準備が整いましたので、ALBにアクセスします。
% curl http://dva-02-004-alb-909547272.ap-northeast-1.elb.amazonaws.com/
[ { "AlbumTitle": { "S": "Somewhat Famous" }, "Awards": { "N": "1" }, "Artist": { "S": "No One You Know" }, "SongTitle": { "S": "Call Me Today" } }, { "AlbumTitle": { "S": "Somewhat Famous" }, "Awards": { "N": "2" }, "Artist": { "S": "No One You Know" }, "SongTitle": { "S": "Howdy" } }, { "AlbumTitle": { "S": "Songs About Life" }, "Awards": { "N": "10" }, "Artist": { "S": "Acme Band" }, "SongTitle": { "S": "Happy Day" } }, { "AlbumTitle": { "S": "Another Album Title" }, "Awards": { "N": "8" }, "Artist": { "S": "Acme Band" }, "SongTitle": { "S": "PartiQL Rocks" } } ]
Code language: Bash (bash)
ALBにアクセスすると、正常に応答がありました。
応答として、DynamoDBに保存されているアイテム情報が返ってきました。
ECS(Fargate)からDynamoDBにアクセスすることができました。
まとめ
DynamoDBにアクセスするECS(Fargate)コンテナアプリを作成しました。