Dockerをさわってみる④ (ECS+CloudFormation)
はじめに
以前は、こちらの記事でdjangoのDockerコンテナをAWSのEC2上で動かしてみました。
今回は、AWSのコンテナオーケストレーションサービスであるECSを使って、上記で作成したDockerコンテナを動かしてみたいと思います。
前提
- Windows 10 Pro
- Docker for Windows 20.10.6 (ローカルでDockerコンテナをビルドするために必要)
- aws-cli 2.1.38
構成
- VPC内に、パブリックサブネットとプライベートサブネットを作成します。
- パブリックサブネット内に、ALB (Application Load Balancer)とNAT Gatewayを配置します。
- プライベートサブネット内に、ECSのサーバーレスの実行環境であるFargateを配置します。プライベートサブネットからのインターネットへのルーティングには、NAT Gatewayを指定します。
- 作成したコンテナイメージはAWSのコンテナレジストリサービスであるECRを利用します。
- 上記環境をCloudFormationを使って作成していきます。
※最近はAWSのすべてのサービスをCloudFormationで作成するように意識しています。
ECSとは
ECS概要
Amazon Elastic Container Service (Amazon ECS) は、フルマネージド型のコンテナオーケストレーションサービスです。
コンテナオーケストレーションとは
- 管理対象のコンテナが増えるとそれぞれのコンテナに対して運用やデプロイなどの管理を行うことが難しくなります。
- そこで登場するのがコンテナオーケストレーションです。
- 代表的なコンテナオーケストレーションサービスとしてGoogle社が社内で利用していたBorgというコンテナオーケストレーションツールをOSSとして利用できるようにしたKubernetesが存在します。
つまりAWSが独自に開発したコンテナオーケストレーションサービスがECSとなります。
ECSの中で、実行環境が2タイプあり、1つがEC2で、もう1つがサーバレスでインスタンスの管理を不要にしたFargateです。Fargateはインスタンスの管理が不要な分、EC2にSSHにログインしてログを取得するみたいなことができないので、ログを外部に出力するなどの対応が必要になります。
また、Amazon Elastic Container Registry (Amazon ECR)は、完全フルマネージドのコンテナレジストリサービスです。
DockerHubのように自分で作成したコンテナイメージを管理することができます。
ECSを構成する3要素
ECSを利用する際に理解しておいたほうが良い概念として、クラスター、タスク、サービスが存在します。
上記の17ページがわかりやすいです。(ページ指定で表示できなくてすみません。)
名称 | 説明 |
---|---|
クラスター | タスクとクラスターを論理的にグルーピングしたもの |
タスク定義 | コンテナイメージの場所、CPU&メモリ、ネットワークモードなどを定義する |
タスク | タスク定義によって起動されるコンテナ群 |
サービス | タスクの実行数を維持する。ELBにはこれをぶら下げる。 |
ハンズオン
ECRの作成 & DockerイメージのPush
まず、自分で作成するDockerイメージを管理しておくためのレポジトリをECR上に作成します。
AWSTemplateFormatVersion: '2010-09-09'
Description: Create ECR
Resources:
ECR:
Type: AWS::ECR::Repository
Properties:
RepositoryName: django_app
上記テンプレートファイルを用いて、CloudFormationスタックを作成します。
作成の仕方は割愛しますが、無事にレポジトリが作成されると以下の画面のようになります。
-
サービス > Elastic Container Service > リポジトリ
-
上記画面の、「プッシュコマンドの表示」を押下します。
-
ECRにコンテナイメージをPushするためのコマンドが表示されます。(macOS/LinuxとWindowsの2つのタブがありますが、macOS/Linuxのほうでいけました。)
-
このコマンド通りにコマンドを叩いていきます。
- 認証トークンを取得し、作成したレジストリに対して Docker クライアントを認証します。
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com
- Dockerイメージを作成します。(Dockerfileが存在するパスへ移動してから)
docker build -t django_app .
なお、Dockerfileには以下を記述しました。
# 1. ベースとなるイメージを指定
FROM python:3.9
# 2. 作業ディレクトリの作成
RUN mkdir /src
# 3. 作業ディレクトリの設定
WORKDIR /src
# 4. カレントディレクトリにあるファイルをコンテナ上の指定のディレクトリにコピーする
ADD . /src
# 5. pipでrequirements.txtに指定されているパッケージを追加する
RUN pip3 install -r requirements.txt
# 6. dbのmigration
RUN python3 ./MyDjangoProject/manage.py migrate
# 7. 起動(コンテナのポート80番で受け付けるように起動する)
CMD python3 ./MyDjangoProject/manage.py runserver 0.0.0.0:80
アプリケーションの作成自体は、こちらをご参照ください。
また、上記の記事と異なる点として、コンテナ側の80番ポートを開放しています。
- リポジトリにPushできるようにタグを付与します。
docker tag django_app:latest XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django_app:latest
- 以下のコマンドでレポジトリにDockerイメージをPushします。
docker push XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django_app:latest
ECRの画面を確認し、「django_app」を押下すると以下の画面に遷移し、イメージがPushできたことが確認できます。
ネットワーク環境の構築
ここから実際にCloudFormationを用いて環境の構築を行います。
- 以下のテンプレートで、VPC, Subnet, Internet Gateway, NAT Gatewayを作成します。
- CloudFormationの構文と、ネットワーク環境の構築は本題とはそれますので説明は割愛させていただきます。
- CloudFormationについてはこちらでまとめておりますのでご参照ください。
- NAT Gatewayの作成についてはこちらを参考にさせていただきました。
AWSTemplateFormatVersion: 2010-09-09
Description: Create Network (VPC, Subnet, Internet Gateway, NAT Gateway)
Resources:
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------#
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
InstanceTenancy: default
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: MyVPC
# ------------------------------------------------------------#
# Internet Gateway
# ------------------------------------------------------------#
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: My-InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref MyVPC
InternetGatewayId: !Ref InternetGateway
# ------------------------------------------------------------#
# Public Subnet Route Table
# ------------------------------------------------------------#
PublicSubnetRouteTable:
Type: AWS::EC2::RouteTable
DependsOn: AttachGateway
Properties:
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: My-RouteTable
Route:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicSubnetRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
# ------------------------------------------------------------#
# Public Sunbet A
# ------------------------------------------------------------#
PublicSubnetA:
Type: AWS::EC2::Subnet
DependsOn: AttachGateway
Properties:
AvailabilityZone: "ap-northeast-1a"
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: 'true'
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: PublicSubnetA
PublicSubetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicSubnetRouteTable
# ------------------------------------------------------------#
# Public Sunbet C
# ------------------------------------------------------------#
PublicSubnetC:
Type: AWS::EC2::Subnet
DependsOn: AttachGateway
Properties:
AvailabilityZone: "ap-northeast-1c"
CidrBlock: 10.0.2.0/24
MapPublicIpOnLaunch: 'true'
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: PublicSubnetC
PublicSubnetCRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetC
RouteTableId: !Ref PublicSubnetRouteTable
# ------------------------------------------------------------#
# NAT Gateway A
# ------------------------------------------------------------#
NatGatewayA:
Type: AWS::EC2::NatGateway
Properties:
SubnetId: !Ref PublicSubnetA
AllocationId: !GetAtt EIPForNATGatewayA.AllocationId
Tags:
- Key: Name
Value: NatGatewayA
EIPForNATGatewayA:
Type: "AWS::EC2::EIP"
Properties:
Domain: vpc
# ------------------------------------------------------------#
# NAT Gateway C
# ------------------------------------------------------------#
NatGatewayC:
Type: AWS::EC2::NatGateway
Properties:
SubnetId: !Ref PublicSubnetC
AllocationId: !GetAtt EIPForNatGatewayC.AllocationId
Tags:
- Key: Name
Value: NatGatewayC
EIPForNatGatewayC:
Type: "AWS::EC2::EIP"
Properties:
Domain: vpc
# ------------------------------------------------------------#
# Private Subnet A Route Table
# ------------------------------------------------------------#
PrivateSubnetARouteTable:
Type: AWS::EC2::RouteTable
DependsOn: NatGatewayA
Properties:
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: PrivateSubnetARouteTable
PrivateSubnetARoute:
Type: AWS::EC2::Route
DependsOn: NatGatewayA
Properties:
RouteTableId: !Ref PrivateSubnetARouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGatewayA
# ------------------------------------------------------------#
# Private Subnet Route Table
# ------------------------------------------------------------#
PrivateSubnetCRouteTable:
Type: AWS::EC2::RouteTable
DependsOn: NatGatewayC
Properties:
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: PrivateSubnetCRouteTable
PrivateSubnetCRoute:
Type: AWS::EC2::Route
DependsOn: NatGatewayC
Properties:
RouteTableId: !Ref PrivateSubnetCRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGatewayC
# ------------------------------------------------------------#
# Private Sunbet A
# ------------------------------------------------------------#
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: "ap-northeast-1a"
VpcId: !Ref MyVPC
CidrBlock: 10.0.3.0/24
Tags:
- Key: Name
Value: PrivateSubnetA
PrivateSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetA
RouteTableId: !Ref PrivateSubnetARouteTable
# ------------------------------------------------------------#
# Private Sunbet C
# ------------------------------------------------------------#
PrivateSubnetC:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: "ap-northeast-1c"
VpcId: !Ref MyVPC
CidrBlock: 10.0.4.0/24
Tags:
- Key: Name
Value: PrivateSubnetC
PrivateSubnetCRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetC
RouteTableId: !Ref PrivateSubnetCRouteTable
Outputs:
VPC:
Value: !Ref MyVPC
Export:
Name: VPCName
PublicSubnetA:
Value: !Ref PublicSubnetA
Export:
Name: PublicSubnetA
PublicSubnetC:
Value: !Ref PublicSubnetC
Export:
Name: PublicSubnetC
PrivateSubnetA:
Value: !Ref PrivateSubnetA
Export:
Name: PrivateSubnetA
PrivateSubnetC:
Value: !Ref PrivateSubnetC
Export:
Name: PrivateSubnetC
- ここまでで以下の図のような環境が作成できました。
セキュリティグループ/ロールの作成
- 以下で、ECSのサービスに割り当てるセキュリティグループ、ALBに割り当てるセキュリティグループ、ECSのタスクに割り当てるロールを作成します。
- ALBにはインターネットからのHTTPのアクセスを受け付けるインバウンドルールを持ったセキュリティグループを割り当てます。
- ECSにはサービスにはALBからのHTTPアクセスを受け付けるインバウンドルールを持ったセキュリティグループを割り当てます。
- ECSのタスクには、ECRからコンテナイメージをPullする、CloudWatchLogsにログを送信するなどの権限を持たせる必要があるため、AmazonECSTaskExecutionRolePolicyなるポリシーを付与したロールを作成しています。
AWSTemplateFormatVersion: 2010-09-09
Description: Create Security Group and Role
Resources:
# ------------------------------------------------------------#
# ALB Security Group
# ------------------------------------------------------------#
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !ImportValue VPCName
GroupDescription: Allow HTTP from Internet
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: ALBSG
# ------------------------------------------------------------#
# ECS Security Group
# ------------------------------------------------------------#
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !ImportValue VPCName
GroupDescription: Allow HTTP from ALB
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: ECSSG
# ------------------------------------------------------------#
# ECS Task Execution Role
# ------------------------------------------------------------#
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: "ECSTaskExecutionRole"
Path: /
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
Outputs:
ECSSecurityGroup:
Value: !Ref ECSSecurityGroup
Export:
Name: ECSSecurityGroupName
ALBSecurityGroup:
Value: !Ref ALBSecurityGroup
Export:
Name: ALBSecurityGroupName
ECSTaskExecutionRole:
Value: !Ref ECSTaskExecutionRole
Export:
Name: ECSTaskExecutionRoleName
Fargate環境の作成
- 以下でALB, ECSのクラスター、サービス、タスク、CloudWatchロググループを作成しています。
- クラスター名、タスク名、サービス名、コンテナ名、コンテナイメージはパラメータとして外だししています。
- クラスターに関して、名前以外にプロパティは指定しませんでした。
- タスクに関しては以下を設定しました。
設定項目 | 設定値 | 説明 |
---|---|---|
Cpu | 256 | 今回は最小値の256 (.25 vCPU)を使用。この時、Memory には512 (0.5 GB), 1024 (1 GB), 2048 (2 GB)の3つの値を設定居できる。 |
Memory | 512 | 最小の512 (.5 vCPU) を使用。この時Cpu には256 (.25 vCPU)の値のみ設定できる。 |
ExecutionRoleArn | security.ymlで作成したECS用のロール | タスク実行時に割り当てるロール |
Family | {StackName}-django-app | タスクに割り当てる名前 |
NetworkMode | awsvpc |
none , bridge , awsvpc ,host の4つの値をとることができる。awsvpc を設定すると、タスクにENIとIPv4 アドレスが割り当てられる。 |
RequiresCompatibilities | FARGATE | タスクが使用している起動タイプ。EC2 とECS の2つのどちらかを設定できる。 |
ContainerDefinitions | テンプレート参照 | 実行するコンテナに関する設定。詳細は割愛するが、CPU, メモリの設定や、イメージの指定、ロググループの指定、ポートマッピングなどを指定している。 |
詳細は以下を参照ください。
- サービスに関しては以下を設定しました。
設定項目 | 設定値 | 説明 |
---|---|---|
Cluster | 作成したクラスターを指定 | ECSクラスター名や、ARNを指定する。 |
DesiredCount | 1 | クラスター内で実行されるタスクの数。 |
LaunchType | FARGATE | サービスの実行タイプ。EC2 ,EXTERNAL ,FARGATE が指定できる。 |
LoadBalancers | テンプレート参照 | LoadBalancerをリストで指定する。LoadBalancerには、ターゲットグループやコンテナ名、ポートなどを指定する。 |
NetworkConfiguration | テンプレート参照 | タスクのNetworkMode がawsvpcのときのみ必須。AwsvpcConfiguration を設定する。ここでは、サービスの起動するサブネットやセキュリティグループを指定する。 |
ServiceName | Paramtersで指定した、サービス名 | ECSのサービス名を指定する。 |
TaskDefinition | 上記で作成したタスク | タスクのFamily かARNを指定する。 |
- ALBは、上記で作成したパブリックサブネットに配置しています。
- ターゲットグループでは、ターゲットタイプにIPを指定することに注意してください。
- CloudWatchのロググループは、Type: AWS::Logs::LogGroupで作成しています。
AWSTemplateFormatVersion: 2010-09-09
Parameters:
ECSClusterName:
Description: ECS Cluster Name
Type: String
Default: django-cluster
ECSTaskName:
Description: ECS Task Name
Type : String
Default: django-task
ECSServiceName:
Description: ECS Service Name
Type : String
Default: django-service
ECSContainerName:
Description: Container Name
Type: String
Default: django-app
ECSContainerImage:
Description: Container Image URL
Type: String
Default: XXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django_app
Resources:
# ------------------------------------------------------------#
# ECS Cluster
# ------------------------------------------------------------#
ECSCluster:
Type: "AWS::ECS::Cluster"
Properties:
ClusterName: !Ref ECSClusterName
# ------------------------------------------------------------#
# ECS TaskDefinition
# ------------------------------------------------------------#
ECSTaskDefinition:
Type: "AWS::ECS::TaskDefinition"
Properties:
Cpu: 256
ExecutionRoleArn: !ImportValue ECSTaskExecutionRoleName
Family: !Join ['', [!Ref 'AWS::StackName', -django-app]]
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: !Ref ECSContainerName
Cpu: 128
Essential: 'true'
Image: !Ref ECSContainerImage
Memory: 128
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref CloudwatchLogsGroup
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: django-app
PortMappings:
- HostPort: 80
Protocol: tcp
ContainerPort: 80
# ------------------------------------------------------------#
# ECS Service
# ------------------------------------------------------------#
ECSService:
Type: AWS::ECS::Service
DependsOn: ALBListener
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LaunchType: FARGATE
LoadBalancers:
-
TargetGroupArn: !Ref TargetGroup
ContainerPort: 80
ContainerName: !Ref ECSContainerName
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !ImportValue ECSSecurityGroupName
Subnets:
- !ImportValue PrivateSubnetA
- !ImportValue PrivateSubnetC
ServiceName: !Ref ECSServiceName
TaskDefinition: !Ref ECSTaskDefinition
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ApplicationLoadBalancer:
Type : AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: ApplicationLoadBalancer
Scheme: "internet-facing"
SecurityGroups:
- !ImportValue ALBSecurityGroupName
Subnets:
- !ImportValue PublicSubnetA
- !ImportValue PublicSubnetC
Tags:
- Key: Name
Value: ApplicationLoadBalancer
# ------------------------------------------------------------#
# Target Group
# ------------------------------------------------------------#
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: TargetGroup
Port: 80
Protocol: HTTP
TargetType: ip
Tags:
- Key: Name
Value: TargetGroup
VpcId: !ImportValue VPCName
# ------------------------------------------------------------#
# ALB Listner
# ------------------------------------------------------------#
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
# ------------------------------------------------------------#
# CloudWatch Log Group
# ------------------------------------------------------------#
CloudwatchLogsGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Join ['-', [ECSLogGroup, !Ref 'AWS::StackName']]
RetentionInDays: 14
動作確認
-
上記のテンプレートを使ってスタックを作成する。
-
スタックが4つ作成された。
-
ここで、サービス > EC2 > ロードバランサーから、上記テンプレートから作成されたALBを確認する。
-
このDNS名をコピーする。
-
これをブラウザに張り付けてパスを指定すると、意図通りの画面が表示された!
-
ちなみに、サービス > CloudWatch > ロググループを参照すると、
/ecs/django-task
なるロググループが作成されており、ログが記録されていた。
最後に
今回は、ECSを使って、自作のDockerコンテナを起動しました。今回は、コンテナを1つしか起動しませんでしたが、ECSのようなコンテナオーケストレーションサービスはコンテナが複数あってこそだと思いますので、そのレベルまで活用できるように頑張っていきたいと思います。
Discussion