ども、セプテーニ・オリジナルのさえきと申します。
この記事は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はまだ使い始めたばかりなので、基本的な機能しか使えてないですが、もう少し使い方を調べ活用していきたいです。
では