こんにちは、貴子です。
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でインスタンスを立てていますが、この時は導入検討段階だったので、インスタンスだけで立てました。