皆さんこんにちは、小幡です。
この記事は、基礎インフラ備忘録シリーズの第3回目です。2回目は以下のリンクから参照ください。
前回はEC2インスタンスの作成をCloudFormationで自動化してみました。
今回はDockerでコンテナイメージを作成しECRへプッシュ、CloudFormationでECS, RDSの導入をしてみます。
CloudFormationへ構成の記述をしていくのですが、前回のテンプレートよりも大分複雑に見えると思いますが、気長に頑張りましょう。それぞれのリソースについては、ECSとコンテナ化において、外すことのできないものでものです。
自分自身への備忘録的な記事なので、検証を行っていないところや、間違いもあると思いますのでご注意ください。
Dockerでイメージをビルドする
最終的にはDockerで作成したイメージはECRにプッシュします。
そのプッシュ先のECRで、「リポジトリを作成」をします。AWSマネジメントコンソールで作業を行います。
次に、Docker が ECR にアクセスできるように、以下のコマンドで認証トークンを取得してログインを行います。
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <あなたの12桁のAWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
成功を確認したら、次のコマンドでDockerイメージをビルドします。
docker build -t my-app .
ECR のリポジトリ URL に合わせて、ビルドしたイメージに新しいタグを付けます。
docker tag my-app:latest <あなたの12桁のAWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest
最後に、タグ付けしたイメージを AWS 上のリポジトリへ送信します。
docker push <あなたの12桁のAWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest
これでECRの準備は完了しました。
ここまでの作業はローカルで開発したDocker上のソースを、最終的に本番環境でリリースしたり、誰かに共有した場合に行う手順です。万が一ローカルのDockerが壊れてしまっても、クラウド上にあるので安心ですね
RDSの導入
上記でDockerイメージをビルドしていますが、これの前にRDSの導入も進めました。
CloudFormationと合わせてsettings.pyの修正も必要なのですが、結果的に以下のような内容に変わっています。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST'),
'PORT': '5432',
}
}
それぞれの値には環境変数を使用しています。こうすることで、ローカルでの作業と本番でのリリースは、それぞれ異なる環境変数を与えることで正常に動作します。
CloudFormationでECS Fagateを導入
今回も新しくテンプレートファイルを用意します。結果的には以下の状態になりました。
AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Fargate infrastructure with ALB and RDS
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
Description: Select your Default VPC
SubnetIdX:
Type: AWS::EC2::Subnet::Id
Description: Select Subnet A (e.g. ap-northeast-1a)
SubnetIdY:
Type: AWS::EC2::Subnet::Id
Description: Select Subnet B (e.g. ap-northeast-1c)
ImageUri:
Type: String
Default: "710496666739.dkr.ecr.ap-northeast-1.amazonaws.com/my-django-app:latest"
Resources:
# --- ECS Cluster ---
MyECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: MyDjangoCluster
# --- CloudWatch Logs ---
MyLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/my-django-app
RetentionInDays: 7
# --- IAM Role ---
ECSTaskExecutionRole:
Type: AWS::IAM::Role
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
# --- ECS Task Definition ---
MyTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: my-django-task
Cpu: '256'
Memory: '512'
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: django-container
Image: !Ref ImageUri
Environment:
- Name: DB_HOST
Value: !GetAtt MyDBInstance.Endpoint.Address
- Name: DB_NAME
Value: dbname1234
- Name: DB_USER
Value: dbuser5678
- Name: DB_PASSWORD
Value: dbpassword9012
PortMappings:
- ContainerPort: 8000
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref MyLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
# --- Application Load Balancer ---
MyALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
SecurityGroups: [ !GetAtt ALBSecurityGroup.GroupId ]
Subnets: [ !Ref SubnetIdX, !Ref SubnetIdY ]
MyTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref VpcId
Protocol: HTTP
Port: 8000
TargetType: ip
HealthCheckPath: /
MyALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref MyALB
Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupArn: !Ref MyTargetGroup
# --- ECS Service ---
MyECSService:
Type: AWS::ECS::Service
DependsOn: MyALBListener
Properties:
Cluster: !Ref MyECSCluster
TaskDefinition: !Ref MyTaskDefinition
DesiredCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups: [ !GetAtt ContainerSecurityGroup.GroupId ]
Subnets: [ !Ref SubnetIdX, !Ref SubnetIdY ]
LoadBalancers:
- ContainerName: django-container
ContainerPort: 8000
TargetGroupArn: !Ref MyTargetGroup
HealthCheckGracePeriodSeconds: 300
# --- Security Groups ---
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP to ALB
VpcId: !Ref VpcId
SecurityGroupIngress:
- { IpProtocol: tcp, FromPort: 80, ToPort: 80, CidrIp: 0.0.0.0/0 }
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow ALB to Container
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8000
ToPort: 8000
SourceSecurityGroupId: !GetAtt ALBSecurityGroup.GroupId
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow access to RDS from ECS
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !GetAtt ContainerSecurityGroup.GroupId
# --- RDS Instance ---
MyDBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS
SubnetIds: [ !Ref SubnetIdX, !Ref SubnetIdY ] # プロパティ名を修正
MyDBInstance:
Type: AWS::RDS::DBInstance
Properties:
DBName: mydatabase
AllocatedStorage: '20'
DBInstanceClass: db.t4g.micro
Engine: postgres
MasterUsername: dbuser
MasterUserPassword: dpassword123
DBSubnetGroupName: !Ref MyDBSubnetGroup
VPCSecurityGroups: [ !GetAtt DBSecurityGroup.GroupId ]
PubliclyAccessible: false
上記のテンプレートは前回作成したものと、使用しているリソースが違います。
またECSの使用のためにALBの設定なども制約に従い定義していますので、少し複雑に感じられると思います。
実際私も、上記のようなサンプルのテンプレートを最初に見たときは、書いてあることを読み解くだけでもだいぶ時間がかかりましたし、予想通りテンプレートの実行ではスタックが失敗となることが何度もあり、少し大変な思いをしました。
EC2単体での作成と、コンテナ化の作業は大きく違うものとなっていますので、まずはRDSを使わない最小構成で実行してみるのも良いかもしれません。
実際私は、RDSの追加はECSの作成がほとんど成功し、失敗した理由がすでに定義済みであったDBへの接続が失敗していたという状況を確認してから行っています。
またRDS、ALB、ECSの作成にはそれぞれ2分~5分前後の時間がかかっていたように感じます。スタックのプレビューに正確な作成にかかった時間が確認できるのですが、見ていませんでした。なので、一回で全ての実行を試すと、結果が得られるまでとても時間がかかるので、そういう意味でも1つずつテンプレートに記述して実行してみるのが良いと思います。
スタックのタイムラインビュー(作成)
RDS, ALB, ECSが作成されるまでにかかった時間を見てみましょう。
AWSマネジメントコンソールで以下の場所から確認できます。
CloudFormation > スタック > [作製したスタック名] > 上部の「イベント」タブ > 右上の「タイムラインビュー」

これを見ると、どのリソースがどんな順番で、どのくらい時間がかかったかが一目瞭然ですね。
目を引くのが、ALBの作成が約3分、次にRDSの作成が約6分、最後にECSの作成が1分半です。詳細は以下の通りです。
| AWSリソース名 | 合計時間(秒) |
|---|---|
| ALB | 187 |
| RDS | 371 |
| ECS | 94 |
余談ですが、ECSのヘルスチェックで失敗となる場合は、作成中から失敗になるまでもっと時間がかかっていた印象です。
私の場合はECSまで直接確認しに行って、失敗を確認したので、スタックが作成中の状態でしたが、スタックの削除を実行していました。
スタックのタイムラインビュー(削除)
ECSとRDSは起動したままにしておくと、EC2単体で起動していた以上に料金が高くなりますので、テストが完了したら削除するようにしています。早く新しくサービスを開発して、運用などを本格的に行い実験してみたいところです。
削除にもそれなりに時間がかかることがわかりました。(左側半分が作成時間、右側半分が削除時間です。)
今回の削除の際は、ECSが一番時間がかかっていたようですね。

まとめ
今回はCloudFormationのテンプレートにECS, ALB, RDSなどのリソースについて記述し、コンテナでのサービス開発を想定した構成とすることができました。
今回のCloudFormationではスタックの作成と削除で時間がかかっていますが、基本的にはインフラ構成を頻繁に作成したり削除することは少なく、一度整ってしまえば運用で触ることはあまりないものと考えています。それよりもローカルのDockerイメージを再ビルドしたらECRへプッシュして、それをこのインフラ構成に反映させるという作業が入ってきます。
コンテナ化ができたので、次回はローカルでコンテナ内のサービスに機能を追加し、その後リリースをするという事を想定しCI/CDの観点を導入していきたいと思います。
このCI/CDまでを導入できれば、『エクストリームプログラミング』で学ぶことができるプラクティスを実践できる構成としてサービス開発に集中できる状態になれると考えています。
以上です。



