こんにちは、貴子です。
dockerを使ったコンテナ化をしたいというプロジェクトがあったので、ECSの導入検討をしました。
その際に作成したcloudformationのtemplateを載せます。
ECSを選んだ理由は?
AWSでdockerのサービスは2つ、AWS Elastic BeanstalkとECSがあります。
何がどう違うの?というところが解らなかったので、AWSのソリューションアーキテクトの方に弊社へ来ていただいてサービスのご紹介を頂きました。
Beanstalkの中でECSが動いているので大きな違いはありませんが、Beanstalkの方は元々PaaSなので、色々元々組み込まれて便利に使え、.ebextensionsというYAMLで管理できるようになっていいます。
ECSはdockerに特化したもので、最新機能はECSの方が組み込まれるのが早いそうです。
.ebextensionsを使っていくと何でもできちゃうので運用めっちゃ大変そうだなーと思って、シンプルにECSを試しました。
ECSについては、公式サイトの紹介の動画 Amazon EC2 Container Service (Docker コンテナ管理) | AWS が解りやすいです。
ECSのtemplate
cloudformation
弊社ではcloudformationは1枚のjsonファイルではなく、base.template・dev.template・dev.elb.template・staging.template…とのように分けて作って管理しているので、ecsのtemplateも同様に管理します。
cloudformationのstack policyは、意図せずに削除とかリプレースがかかると嫌なので、基本的にそれをDenyにするものを設定しています。
全て同じtemplateに書くとdevのインスタンスをリプレースしたいだけなのに、
productionやstagingのstack policyも変わってしまうので、それを避けるためにある程度分けて管理しています。
ECSのtemplate
VPCやSubnet・InternetGateway等は作成済みとします。
1. ecs.cluster.template
ECSのClusterの登録です。今回はdevサーバーを作成し、そこにnginxのコンテナを起動させます。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "ecs cluster stack", "Resources" : { "Dev01": { "Type": "AWS::ECS::Cluster" } } }
2. ecs.task.definitions.template
TaskDefinitions = コンテナ情報を登録します。
docker hubからの取得ではなく、弊社で立てたPrivateのdocker registryから取得します。
ECRも出ましたが、オレゴン・バージニア・アイルランドに繋ぎに行くよりかは
東京リージョン内に自前で立てたほうが早かったので、こちらにしました。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "ecs task stack", "Parameters" : { "NginxContainerName" : { "Type" : "String", "Default": "nginx" }, "NginxImage" : { "Type" : "String", "Default": "【Private docker registryドメイン】/【コンテナ名】:【コンテナバージョン】" }, "Resources" : { "Nginx": { "Type": "AWS::ECS::TaskDefinition", "Properties" : { "ContainerDefinitions" : [ { "Name": { "Ref" : "NginxContainerName" }, "Image": { "Ref" : "NginxImage" }, "PortMappings":[ { "ContainerPort": 80, "HostPort": 80 } ], "Cpu": "1024", "Memory":"500" } ] } } } }
3. ecs.role.template
EC2インスタンスとECSのIAMを登録します。
ECSがEC2インスタンスを利用するので、EC2インスタンスにもIAMが必要と思っていなくて、結構躓きました。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "ecs iam role stack", "Resources" : { "EC2Role": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "ec2.amazonaws.com" ] }, "Action": [ "sts:AssumeRole" ] } ] }, "Path": "/", "Policies": [ { "PolicyName": "ecs-service", "PolicyDocument": { "Statement": [ { "Effect": "Allow", "Action": [ "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", "ecs:Poll", "ecs:RegisterContainerInstance", "ecs:StartTelemetrySession", "ecs:Submit*", "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage" ], "Resource": "*" } ] } } ] } }, "ECSServiceRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "ecs.amazonaws.com" ] }, "Action": [ "sts:AssumeRole" ] } ] }, "Path": "/", "Policies": [ { "PolicyName": "ecs-service", "PolicyDocument": { "Statement": [ { "Effect": "Allow", "Action": [ "ec2:AuthorizeSecurityGroupIngress", "ec2:Describe*", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:Describe*", "elasticloadbalancing:RegisterInstancesWithLoadBalancer" ], "Resource": "*" } ] } } ] } } } }
4. ecs.dev01.template
インスタンスの作成には2つポイントがあります。
(a).まずECS用のAMIを使わなければいけないこと
(b).UserDataで ECS_CLUSTERの名前を設定しなくてはいけないこと
ずっとUserDataをdefaultで作成を試していて、起動してもECSのコンソール画面に表示されなくて、何故なのかを見つけるのが大変でした。。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "dev ecs-ec2 stack", "Parameters" : { "AvailabilityZone" : { "Type" : "AWS::EC2::AvailabilityZone::Name", "Default": "ap-northeast-1c" }, "ImageId" : { "Type" : "AWS::EC2::Image::Id", "Default": "ami-065a6b68" }, "InstanceType" : { "Type" : "String", "Default": "t2.medium" }, "KeyName" : { "Type" : "AWS::EC2::KeyPair::KeyName", "Default": "【key name】" }, "SecurityGroupId" : { "Type" : "CommaDelimitedList", "Default": "【セキュリティグループ id】" }, "SubnetId" : { "Type" : "AWS::EC2::Subnet::Id", "Default": "【サブネット】" }, "ECSCluster" : { "Type" : "String", "Default": "【クラスター名】" }, "EC2Role" : { "Type" : "String", "Default": "【3で作ったEC2 Role】" } }, "Resources" : { "DevInstance" : { "Type" : "AWS::EC2::Instance", "Properties" : { "AvailabilityZone" : { "Ref" : "AvailabilityZone" }, "ImageId" : { "Ref" : "ImageId" }, "InstanceType" : { "Ref" : "InstanceType" }, "KeyName" : { "Ref" : "KeyName" }, "BlockDeviceMappings" : [{ "DeviceName" : "/dev/xvda" , "Ebs" : { "DeleteOnTermination" : "false" , "VolumeSize" : "100" }}], "InstanceInitiatedShutdownBehavior" : "stop", "Monitoring" : "true", "SourceDestCheck" : "true", "UserData" : { "Fn::Base64" : { "Fn::Join": [ "", [ "#!/bin/bash\n", "echo ECS_CLUSTER=", { "Ref": "ECSCluster" }, " >> /etc/ecs/ecs.config" ] ] } }, "NetworkInterfaces": [ { "AssociatePublicIpAddress": "true", "DeviceIndex": "0", "GroupSet": { "Ref" : "SecurityGroupId" }, "SubnetId": { "Ref" : "SubnetId" } } ], "IamInstanceProfile": { "Ref" : "EC2InstanceProfile" }, "Tags" : [ {"Key" : "Name", "Value" : "docker-dev01" } ] } }, "EC2InstanceProfile": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Path": "/", "Roles": [ { "Ref": "EC2Role" } ] } } } }
5. ecs.dev01.elb01.template
コンテナで利用するELBを登録します。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "dev ecs-elb stack", "Parameters" : { "Instance" : { "Type" : "CommaDelimitedList", "Default": "【4で起動したinstance id】" }, "SubnetId" : { "Type" : "CommaDelimitedList", "Default": "【サブネット】" }, "SecurityGroupId" : { "Type" : "CommaDelimitedList", "Default": "【セキュリティグループ】" }, "SSL" : { "Type" : "String", "Default": "arn:aws:iam::【AWS account id】:server-certificate/【登録している証明書名】" } }, "Resources" : { "DevFrontendELB" : { "Type" : "AWS::ElasticLoadBalancing::LoadBalancer", "Properties" : { "Instances" : { "Ref" : "Instance" }, "LoadBalancerName" : "docker-dev-frontend-elb01", "ConnectionSettings" : { "IdleTimeout" : 300 }, "Listeners" : [ { "InstancePort" : 80, "InstanceProtocol" : "HTTP", "LoadBalancerPort" : 443, "Protocol" : "HTTPS", "SSLCertificateId" : { "Ref" : "SSL" } } ], "SecurityGroups" : { "Ref" : "SecurityGroupId" }, "Subnets" : { "Ref" : "SubnetId" } } } } }
6. ecs.service.template
最後に、サービスを登録します。
ここでようやく全てが紐付いて、どのコンテナをどのEC2で起動させて、ELBで外部からそのコンテナにアクセス出来るようにします。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "ecs task stack", "Parameters" : { "Cluster" : { "Type" : "String", "Default": "【クラスター名】" }, "NginxELBName" : { "Type" : "String", "Default": "【5. elb名】" }, "NginxTaskDefinition" : { "Type" : "String", "Default": "【2.Task Definitions名】:【2.で登録したバージョン】" }, "NginxContainerName" : { "Type" : "String", "Default": "nginx" }, "ECSRole" : { "Type" : "String", "Default": "【3.ECS Role 名】" } }, "Resources" : { "Nginx": { "Type" : "AWS::ECS::Service", "Properties" : { "Cluster" : { "Ref" : "Cluster" }, "DesiredCount" : 1, "LoadBalancers" : [ { "ContainerName": { "Ref" : "NginxContainerName" }, "ContainerPort": "80", "LoadBalancerName" : { "Ref" : "NginxELBName" } } ], "Role" : { "Ref" : "ECSRole" }, "TaskDefinition" : { "Ref" : "NginxTaskDefinition" } } } } } }
これらを全て登録して、ドメインにアクセスして、コンテナ化した画面が表示されればOKです。
余談ですが、サービスの登録は1~5までのどれか1つでも失敗しているとサービスのtemplate登録には失敗して、「Did not stabilize」のエラーが出ます。
何かが安定しないんだ?これは、こちら側の問題なのか?思って結構悩んだので、
「リソースの作成に失敗しました。」とか出てくれた方が、自分のtemplateが悪いと判断ついて良いなあと思いました。
ECSの公式のsampleだとAutoScalingでインスタンスを立てていますが、この時は導入検討段階だったので、インスタンスだけで立てました。