FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

ECSのcloudformationのtemplateを作成

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