Septeni Engineer's Blog

セプテーニエンジニアが綴る技術ブログ

CloudformationでElastic Beanstalk for Dockerの構築

ども、セプテーニ・オリジナルのさえきと申します。

この記事はContainer with AWS Advent Calendar 2015 15日目の記事です。

弊社ではElastic Beanstalk for Dockerを使い始めました。その時にCloudfomationを使って構築してみたので、サンプルのプロジェクトを例にご紹介できればと思います。

※ Container with AWS だけど、Cloudformationがメインになってしまった感が・・まあいいか

環境

  • nginxが稼働するコンテナを動かすためのBeanstalk環境を構築します。 f:id:t_saeki_sep:20151214140201p:plain

CloudformationでBeanstalk構築

sfnとsparkle_formation

Beanstalk環境をCloudformationで構築します。 ただ、そのままjsonでテンプレート作成するのは大変そうだったので、いろいろ調べて、sfnsparkle_formationを使ってRubyで作成してみました。 詳細は公式サイトをご確認ください。

sparkleformationで作成したrubyのテンプレートをsfnコマンドツールで実行する、といった感じです。 sfnは実行中のイベントが表示されたり、diffで現在のstackの状態とテンプレートの差異を確認できたりと便利なのでおすすめです。

sparkle_formationを利用したbeanstalk用テンプレート例

# Example templates/sample-project.rb
# 事前にsample_projectというKeyPairを作成しておく

# 以下のリソースが構築されます
# VPC
# InternetGateway
# RouteTable
# Subnet
# SecurityGroup
# Beanstalk for Docker

SparkleFormation.new(:SampleProjectTemplate) do
  set!('AWSTemplateFormatVersion', '2010-09-09')
  description "Sample Project"

  parameters do
    project do
      description 'Project Name'
      type 'String'
      default 'sample-project'
    end
  end
  #########################
  # VPC
  #########################
  resources(:Vpc) do
    type 'AWS::EC2::VPC'
    properties do
      cidr_block '10.0.0.0/16'
    end
  end

  #########################
  # Internet Gateway
  #########################
  resources(:InternetGateway) do
    type 'AWS::EC2::InternetGateway'
    properties do
      tags _array(
        -> {
          key 'Project'
          value ref!(:Project)
        }
      )
    end
  end

  resources(:AttachGateway) do
    type 'AWS::EC2::VPCGatewayAttachment'
    properties do
      internet_gateway_id ref!(:internet_gateway)
      vpc_id ref!(:vpc)
    end
  end

  #########################
  # RouteTable
  #########################
  resources(:PublicRouteTable) do
    type 'AWS::EC2::RouteTable'
    properties do
      vpc_id ref!(:Vpc)
    end
  end

  resources(:PublicRoute) do
    type 'AWS::EC2::Route'
    properties do
      destination_cidr_block '0.0.0.0/0'
      gateway_id ref!(:InternetGateway)
      route_table_id ref!(:PublicRouteTable)
    end
  end

  #########################
  # Subnet
  #########################
  resources(:PublicSubnet) do
    type 'AWS::EC2::Subnet'
    properties do
      availability_zone 'ap-northeast-1b'
      cidr_block '10.0.0.0/24'
      map_public_ip_on_launch 'true'
      vpc_id ref!(:Vpc)
    end
  end

  resources(:PublicSubnetRouteTableAssociation) do
    type 'AWS::EC2::SubnetRouteTableAssociation'
    properties do
      route_table_id ref!(:PublicRouteTable)
      subnet_id ref!(:PublicSubnet)
    end
  end

  #########################
  # Security Group
  #########################
  resources(:PublicSecurityGroup) do
    type 'AWS::EC2::SecurityGroup'
    properties do
      group_description 'Public Security Group'
      vpc_id ref!(:Vpc)
    end
  end

  resources(:PublicSecurityGroupIngress1) do
    type 'AWS::EC2::SecurityGroupIngress'
    properties do
      source_security_group_id ref!(:PublicSecurityGroup)
      from_port 0
      to_port 65535
      ip_protocol -1
      group_id ref!(:PublicSecurityGroup)
    end
  end

  resources(:PublicSecurityGroupEgress1) do
    type 'AWS::EC2::SecurityGroupEgress'
    properties do
      cidr_ip '0.0.0.0/0'
      from_port 0
      to_port 65535
      ip_protocol -1
      group_id ref!(:PublicSecurityGroup)
    end
  end

  #########################
  # Instance Profileの作成
  #########################
  resources(:ServerRole) do
    type 'AWS::IAM::Role'
    properties do
      assume_role_policy_document do
        statement _array(
          -> {
            effect 'Allow'
            principal do
              service _array(
                'ec2.amazonaws.com'
              )
            end
            action ['sts:AssumeRole']
          }
        )
      end
      path '/'
    end
  end

  resources(:ServerPolicy) do
    type 'AWS::IAM::Policy'
    depends_on "ServerRole"
    properties do
      policy_name 'ServerRole'
      policy_document do
        statement _array(
          -> {
            effect 'Allow'
            not_action 'iam:*'
            resource '*'
          }
        )
      end
      roles _array(
        ref!(:ServerRole)
      )
    end
  end
  resources(:ServerInstanceProfile) do
    type 'AWS::IAM::InstanceProfile'
    depends_on "ServerRole"
    properties do
      path '/'
      roles _array(
        ref!(:ServerRole)
      )
    end
  end

  #########################
  # BeanstalkのApplication作成
  #########################
  resources(:SampleProjectApplicatioin) do
    type 'AWS::ElasticBeanstalk::Application'
    properties do
      description 'Sample Project Application'
      application_name 'SampleProject'
    end
  end

  ###########################
  # ApplicationのEnvironment作成
  ###########################
  resources(:SampleProjectApplicatioinEnvironment) do
    type 'AWS::ElasticBeanstalk::Environment'
    properties do
      application_name ref!(:SampleProjectApplicatioin)
      description "Sample Project for Production"
      solution_stack_name '64bit Amazon Linux 2015.03 v2.0.2 running Docker 1.7.1'
      environment_name 'SampleProjectProduction'
      CNAMEPrefix 'sample-projet-production'
      tier do
        name 'WebServer'
        type 'Standard'
      end
      option_settings _array(
        -> {
          namespace 'aws:autoscaling:launchconfiguration'
          option_name 'SSHSourceRestriction'
          value "tcp,22,22,xxx.xxx.xxx.xxx/32"
        },
        -> {
          namespace 'aws:autoscaling:launchconfiguration'
          option_name 'SecurityGroups'
          value ref!(:PublicSecurityGroup)
        },
        -> {
          namespace 'aws:autoscaling:launchconfiguration'
          option_name 'EC2KeyName'
          value 'sample_project'
        },
        -> {
          namespace 'aws:ec2:vpc'
          option_name 'VPCId'
          value ref!(:Vpc)
        },
        -> {
          namespace 'aws:ec2:vpc'
          option_name 'AssociatePublicIpAddress'
          value "true"
        },
        -> {
          namespace 'aws:ec2:vpc'
          option_name 'Subnets'
          value ref!(:PublicSubnet)
        },
        -> {
          namespace 'aws:ec2:vpc'
          option_name 'ELBSubnets'
          value ref!(:PublicSubnet)
        },
        -> {
          namespace 'aws:autoscaling:launchconfiguration'
          option_name 'InstanceType'
          value 't2.micro'
        }
      )
    end
  end
end

sfnによるstack作成

#  sfnとsparkle_formationをインストール
$ gem install sfn
$ gen install sparkle_formation

# .sfnファイルを作成
$ vim .sfn
{
  "credentials": {
    "aws_access_key_id": "<aws access key>",
    "aws_secret_access_key": "<aws secret key>",
    "aws_region": "ap-northeast-1"
  },
  "options": {
    "disable_rollback": true,
    "capabilities": ['CAPABILITY_IAM']
  }
}

# validateとstack作成
$ sfn validate -b templates/ -f templates/sample_project.rb -P
[Sfn]: Template Validation (aws):  templates/sample_project.rb
[Sfn]: Validating: SampleProjectTemplate
[Sfn]:   -> VALID

$ sfn create sample-project -b templates/ -f templates/sample_project.rb -P
[Sfn]: SparkleFormation: create
[Sfn]:   -> Name: sample-project
[Sfn]: Stack runtime parameters:
[Sfn]: Project [sample-project]:
[Sfn]: Events for Stack: sample-project
Time                      Resource Logical Id   Resource Status      Resource Status Reason
2015-12-14 05:21:22 UTC   sample-project        CREATE_IN_PROGRESS   User Initiated
2015-12-14 05:21:31 UTC   InternetGateway             CREATE_IN_PROGRESS
2015-12-14 05:21:31 UTC   Vpc                         CREATE_IN_PROGRESS
2015-12-14 05:21:31 UTC   ServerRole                  CREATE_IN_PROGRESS
2015-12-14 05:21:31 UTC   SampleProjectApplicatioin   CREATE_IN_PROGRESS
2015-12-14 05:21:32 UTC   InternetGateway             CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:32 UTC   Vpc                         CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:33 UTC   SampleProjectApplicatioin   CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:34 UTC   SampleProjectApplicatioin   CREATE_COMPLETE
2015-12-14 05:21:42 UTC   ServerRole                  CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:48 UTC   InternetGateway             CREATE_COMPLETE
2015-12-14 05:21:49 UTC   Vpc                         CREATE_COMPLETE
2015-12-14 05:21:51 UTC   PublicSubnet                CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:51 UTC   PublicRouteTable            CREATE_IN_PROGRESS
2015-12-14 05:21:51 UTC   PublicSecurityGroup         CREATE_IN_PROGRESS
2015-12-14 05:21:51 UTC   AttachGateway               CREATE_IN_PROGRESS
2015-12-14 05:21:51 UTC   AttachGateway               CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:51 UTC   PublicRouteTable            CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:51 UTC   PublicSubnet                CREATE_IN_PROGRESS
2015-12-14 05:21:52 UTC   PublicRouteTable            CREATE_COMPLETE
2015-12-14 05:21:53 UTC   ServerRole                  CREATE_COMPLETE
2015-12-14 05:21:54 UTC   PublicRoute                 CREATE_IN_PROGRESS
2015-12-14 05:21:54 UTC   ServerPolicy                CREATE_IN_PROGRESS
2015-12-14 05:21:54 UTC   ServerInstanceProfile       CREATE_IN_PROGRESS
2015-12-14 05:21:55 UTC   PublicRoute                 CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:55 UTC   ServerInstanceProfile       CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:55 UTC   ServerPolicy                CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:21:56 UTC   ServerPolicy                CREATE_COMPLETE
2015-12-14 05:22:07 UTC   PublicSecurityGroup         CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:22:07 UTC   AttachGateway               CREATE_COMPLETE
2015-12-14 05:22:08 UTC   PublicSubnet                         CREATE_COMPLETE
2015-12-14 05:22:08 UTC   PublicSecurityGroup                  CREATE_COMPLETE
2015-12-14 05:22:09 UTC   PublicSubnetRouteTableAssociation    CREATE_IN_PROGRESS
2015-12-14 05:22:09 UTC   SampleProjectEnvironmentProduction   CREATE_IN_PROGRESS
2015-12-14 05:22:09 UTC   PublicSecurityGroupEgress1           CREATE_IN_PROGRESS
2015-12-14 05:22:09 UTC   PublicSecurityGroupIngress1          CREATE_IN_PROGRESS
2015-12-14 05:22:10 UTC   PublicSecurityGroupIngress1          CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:22:10 UTC   PublicSecurityGroupEgress1           CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:22:10 UTC   PublicSecurityGroupIngress1          CREATE_COMPLETE
2015-12-14 05:22:10 UTC   PublicSubnetRouteTableAssociation    CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:22:11 UTC   PublicSecurityGroupEgress1           CREATE_COMPLETE
2015-12-14 05:22:11 UTC   PublicRoute                          CREATE_COMPLETE
2015-12-14 05:22:13 UTC   SampleProjectEnvironmentProduction   CREATE_IN_PROGRESS   Resource creation Initiated
2015-12-14 05:22:26 UTC   PublicSubnetRouteTableAssociation    CREATE_COMPLETE
2015-12-14 05:23:56 UTC   ServerInstanceProfile                CREATE_COMPLETE
2015-12-14 05:29:24 UTC   SampleProjectEnvironmentProduction   CREATE_COMPLETE
2015-12-14 05:29:27 UTC   sample-project                       CREATE_COMPLETE
[Sfn]: Stack create complete: SUCCESS
[Sfn]: Stack description of sample-project:
[Sfn]: Outputs for stack: sample-project
[Sfn]:    Sample Project Environment Production Url: http://awseb-e-b-xxxxx.ap-northeast-1.elb.amazonaws.com

デプロイ

実際にデプロイしてみます。

DockerfileとDockerrun.aws.jsonの準備

DockerfileとDockerrun.aws.jsonを準備します。

# Example Dockerfile

FROM nginx

COPY nginx/conf.d/default.conf /etc/nginx/conf.d/
COPY index.html /usr/share/nginx/html/index.html

# JSTに変更
RUN echo "Asia/Tokyo" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata

EXPOSE 8080
# Example Dockerrun.aws.json

{
  "AWSEBDockerrunVersion": "1",
  "Ports": [
    {
      "ContainerPort": "8080"
    }
  ],
  "Volumes": [],
  "Logging": "/var/log/nginx"
}

nginxのconfigとコンテンツを準備

Dockerコンテナ作成のnginx configファイルとコンテンツ(index.html)を準備します。

# nginx/conf.d/default.conf 

server {
    listen       8080;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html;
    }

}
# index.html
# とりあえず簡単なページ

<h1>Sample Project</h1>

デプロイしてみる

実際にデプロイしてみます。※ eb init済みと仮定

# Environment list確認
$ eb list
* SampleProjectProduction # 作成したEnvironmentが表示される

# デプロイ実行
$ eb deploy SampleProjectProduction
Creating application version archive "app-151214_143818".
Uploading SampleProject/app-151214_143818.zip to S3. This may take a while.
Upload Complete.
INFO: Environment update is starting.
INFO: Deploying new version to instance(s).
INFO: Successfully pulled nginx:latest
INFO: Successfully built aws_beanstalk/staging-app
INFO: Docker container 52682f92d24c is running aws_beanstalk/current-app.
INFO: New application version was deployed to running EC2 instances.
INFO: Environment update completed successfully.

コンテンツ確認

デプロイが成功したら、http://sample-projet-production.elasticbeanstalk.com/ を確認して、先ほど作成したコンテンツが表示されるか確認します。

Cloudformationで構築する際に気をつけておきたいこと

  • OptionSettingsは全項目確認して、必要なところはちゃんと設定しましょう。全項目はこちらにあります。推奨設定もあるので参考にしてください。
  • IAMのConditionにaws:SourceIp条件は設定しないほうがいいようです。

    http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_elements.html

    ユーザーに代わって AWS への呼び出しを実行した AWS サービス(Amazon Elastic MapReduceAWS Elastic Beanstalk、 AWS CloudFormation、Amazon Elastic Block Store、Tag Editor、Amazon Redshift など)からのリクエストの場合、 aws:SourceIp は、コンピューターの IP アドレスではなくそのサービスの IP アドレスとして解決されます。 このタイプのサービスでは、aws:SourceIp 条件を使用しないことをお勧めします。

  • 監視はmackerelでやってます。mackerel-agentはホスト側にインストールしてます。その際は.ebextentionsをよく読んでカスタマイズしましょう。

  • コンテナIDやコンテナのIPアドレスを取得したい場合は、.ebextentionscommandscontainer_commandsではなく/opt/elasticbeanstalk/hooks/appdeploy/post/スクリプトを実行させましょう。

# .extentions/00_check_container.confg
# Test Check Container
#
# どの時点で最新のコンテナ情報が取得できるかテストする
# 結果は以下の通りになり、/opt/elasticbeanstalk/hooks/appdeployの時点で
# 最新のコンテナ情報が取得できることがわかる
#
# Result
#
# $ cat /tmp/test.txt
# commands "fb798b372b48" "172.17.0.13" # デプロイ前のコンテナ情報
# container_commands "fb798b372b48" "172.17.0.13" # デプロイ前のコンテナ情報
# hook appdeploy post 7ad5f7c16c2b 172.17.0.14 # デプロイ後のコンテナ情報

files:
  /opt/elasticbeanstalk/hooks/appdeploy/post/99_test.sh:
    mode: "00755"
    owner: root
    group: root
    encoding: plain
    content: |
      #!/bin/sh
      DOCKER_CONTAINER_ID=$(docker ps -l -q)
      DOCKER_IP_ADDRESS=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' ${DOCKER_CONTAINER_ID})
      echo "hook appdeploy post ${DOCKER_CONTAINER_ID} ${DOCKER_IP_ADDRESS}" >> /tmp/test.txt
commands:
  check_container:
    command: echo "commands \"$(echo $(docker ps -l -q))\" \"$(echo $(docker inspect --format='{{.NetworkSettings.IPAddress}}' $(docker ps -l -q)))\"" >> /tmp/test.txt
container_commands:
  check_container:
    command: echo "container_commands \"$(echo $(docker ps -l -q))\" \"$(echo $(docker inspect --format='{{.NetworkSettings.IPAddress}}' $(docker ps -l -q)))\"" >> /tmp/test.txt

まとめ

実際にはfrontendにnginx、apiサーバとしてPlayFrameworkをBeanstalk for Dockerで動かしてます。 構築自体はそれほど難しくないですが、カスタマイズするとなると結構大変でした。Beanstalkの仕様をよく理解した上で対応しましょう。 sfnやsparkle_formationはまだ使い始めたばかりなので、基本的な機能しか使えてないですが、もう少し使い方を調べ活用していきたいです。

では