ども、セプテーニ・オリジナルのさえきと申します。
この記事はContainer with AWS Advent Calendar 2015 15日目の記事です。
弊社ではElastic Beanstalk for Dockerを使い始めました。その時にCloudfomationを使って構築してみたので、サンプルのプロジェクトを例にご紹介できればと思います。
※ Container with AWS だけど、Cloudformationがメインになってしまった感が・・まあいいか
環境
- nginxが稼働するコンテナを動かすためのBeanstalk環境を構築します。

CloudformationでBeanstalk構築
sfnとsparkle_formation
Beanstalk環境をCloudformationで構築します。 ただ、そのままjsonでテンプレート作成するのは大変そうだったので、いろいろ調べて、sfnとsparkle_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 MapReduce、AWS Elastic Beanstalk、 AWS CloudFormation、Amazon Elastic Block Store、Tag Editor、Amazon Redshift など)からのリクエストの場合、 aws:SourceIp は、コンピューターの IP アドレスではなくそのサービスの IP アドレスとして解決されます。 このタイプのサービスでは、aws:SourceIp 条件を使用しないことをお勧めします。
監視はmackerelでやってます。mackerel-agentはホスト側にインストールしてます。その際は.ebextentionsをよく読んでカスタマイズしましょう。
コンテナIDやコンテナのIPアドレスを取得したい場合は、
.ebextentionsのcommandsやcontainer_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
- stackをupdateしたりdeleteする機会があると思いますが、スタックポリシーやDeletionPolicy 属性の設定はしっかりやっておきましょう。
- 何かとドキュメントは英語版を見たほうがいいです。
まとめ
実際にはfrontendにnginx、apiサーバとしてPlayFrameworkをBeanstalk for Dockerで動かしてます。 構築自体はそれほど難しくないですが、カスタマイズするとなると結構大変でした。Beanstalkの仕様をよく理解した上で対応しましょう。 sfnやsparkle_formationはまだ使い始めたばかりなので、基本的な機能しか使えてないですが、もう少し使い方を調べ活用していきたいです。
では