アドテクスタジオのオペレーションテクノロジー事業部(通称オペテク)の杉澤です。
前回の記事「Elastic Beanstalk の Multi-Container Docker Environment は便利①」の続きです。
概要
今回は Elastic Beanstalk のソースコードのフォルダ構成の話や、設定ファイルから構築する際の書き方などの
実際にどのように設定しているのかを、書いていきたいと思います。
また、Elastic Beanstalk によって得られるデプロイのメリットが具体的にどんなオペレーション、どんな動きで行われるのか、構築する場合に気をつけたほうが良い事などを紹介します。
本記事は要所を切り出して記事にしているため、不明な点は Elastic Beanstalk の公式ドキュメントを参考にして頂ければと思います。
Elastic Beanstalk のフォルダ構成と実際の設定内容について
Elastic Beanstalk は awsコマンド
ではなく、ebコマンド
を使用して環境の構築やデプロイを行います。
eb
コマンドは .elasticbeanstalk/config.yml
の内容を読み取ってアプリケーションへの操作を行うため
webアプリケーションの操作は cd
で web に移動してから、batch アプリケーションの操作は batch に移動してから行います。
※ebコマンドで環境を構築するにあたって、アクセス許可の設定などが必要になります。ドキュメントはこちらhttp://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/concepts-roles.html
私の担当するプロダクトでは以下のようなフォルダ構成となっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
infra-beanstalk/ |- web/ |- Dockerrun.aws.json |- .elasticbeanstalk/ |- config.yml |- .ebextensions |- 00.option.config |- 01.endpoint.config |- 02.docker.config |- 03.application.config |- 04.ldap.config |- 05.autoscaling.config |- batch/ |- webとほぼ同じ |
Elastic Beanstalk では eb コマンドで create
や deploy
を使うと git commit
された状態の
カレントディレクトリにある.ebextensions
の中にある config ファイルを名前順に読み込み、適用を始めます。
そのため config ファイルのプレフィックスに数字で連番が振られています。
設定ファイルとしては、この config ファイル郡 + Dockerrun.aws.json というファイルでひとつのアプリケーションを動かす環境構築が可能です。
次に、Web のアプリケーションを例にして、それぞれのファイルの内容を実際に見ていきます。
また、以降プロダクト固有の部分については伏せてあります。ご了承ください。
00.option.config
- このファイルでは config の中で指定できる
option_settings
というものを設定しています。 - ドキュメントはこちら http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/command-options-general.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
option_settings: "aws:autoscaling:launchconfiguration" : IamInstanceProfile : "aws-elasticbeanstalk-ec2-role" EC2KeyName : "ec2-key-name" InstanceType : "t2.medium" SecurityGroups : "sg-0000000a" RootVolumeSize : "100" RootVolumeType : "gp2" "aws:ec2:vpc" : VPCId : "vpc-00000a00" Subnets: "subnet-00000000, subnet-00000000" ELBSubnets: "subnet-00000000, subnet-00000000" ELBScheme: "internal" "aws:elasticbeanstalk:environment" : EnvironmentType : "LoadBalanced" ServiceRole : "aws-elasticbeanstalk-service-role" "aws:autoscaling:asg" : MinSize : 4 MaxSize : 10 "aws:elasticbeanstalk:command" : DeploymentPolicy : "Rolling" BatchSize : 50 "aws:autoscaling:trigger" : BreachDuration : 5 MeasureName : CPUUtilization Statistic : Average Unit : Percent UpperThreshold : 60 |
01.endpoint.config
このファイルでは環境のエンドポイントに関係する以下2つを作成するように設定しています。
- AWSEBLoadBalancer : 443 to 443 のロードバランサーを作成しています。
- AssetsInternalDNSRecord: インターナルの DNS レコードを登録しています。 内容は、ドメインが
eb-{環境名}.example.com
でAWSEBLoadBalancerで作成したロードバランサーのエイリアスを作成しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Resources: AWSEBLoadBalancer: Type: "AWS::ElasticLoadBalancing::LoadBalancer" Properties: LoadBalancerName: { "Ref":"AWSEBEnvironmentName" } CrossZone: true Listeners: - {LoadBalancerPort: 443, InstancePort: 443, Protocol: "HTTPS", InstanceProtocol: "HTTPS", SSLCertificateId: "arn:aws:iam::000000000000:ssl-key/ssl-key"} HealthCheck: HealthyThreshold: "3" Target: "HTTPS:443/health_check" Interval: "30" Timeout: "5" UnhealthyThreshold: "5" AssetsInternalDNSRecord: Type: "AWS::Route53::RecordSet" DependsOn: "AWSEBLoadBalancer" Properties: HostedZoneId: XXXXXXXXXXXX0 Comment: {"Fn::Join":["-", [{ "Ref":"AWSEBEnvironmentName" }, "record"]]} Name: { "Fn::Join" : ["", [ "eb-", { "Ref":"AWSEBEnvironmentName" }, ".", "example.com", "." ]]} # eb-{環境名}.example.comのドメインをRoute53に登録 Type: A AliasTarget: HostedZoneId: { "Fn::GetAtt" : [ "AWSEBLoadBalancer", "CanonicalHostedZoneNameID" ] } DNSName: { "Fn::GetAtt" : ["AWSEBLoadBalancer","DNSName"] } |
AssetsInternalDNSRecord は、Elastic Beanstalk の CNAME を使いたくなかったため自前でレコードを用意しています。
理由としては、Elastic Beanstalk が提供する URL 内に Elastic Beanstalk の ID があってわかりにくく使いたくなかったのと、
提供された URL を使うと CNAME を使う事になるのですが、ルートドメインを使いたかったため、CNAME は使っていません。
(ルートドメインを使う場合、 DNS の決まり事で CNAME が使えなかったと思うので、このようにしています)
02.docker.config
社内で用意されている、JFrogArtifactory の DockerRegistry にログインしています。
1 2 3 4 |
commands: 01-login-artifactory: command: docker login --email=foo@example.com --username=username --password=password docker.example.com |
03.application.config
ログをマウントするディレクトリを作成します。
1 2 3 4 5 6 |
commands: 01mkdir_frontend_logdir: command: "mkdir -p /var/log/frontend" 02mkdir_backend_logdir: command: "mkdir -p /var/log/backend" |
04.ldap.config
内容は表示できませんが、EC2インスタンスにコマンドで hosts を追加して社内 ldap サーバーとの連携をさせる config ファイルです。
1 2 3 4 5 6 7 8 9 10 |
container_commands: 00add_hosts_init: command: "非公開" 01add_hosts_init: command: "非公開" 02add_hosts_init: command: "非公開" 03add_ldap: command: "非公開" |
05.autoscaling.config
オートスケーリンググループに対しての設定を書いています。
今回作成する環境は夜間はアクセス頻度が落ちるため以下のようなスケジュールのスケーリングを行わせます。
- 21時 : 最低2台稼働している状態に
- 09時 : 最低4台稼働している状態に
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
2100scalingsetting: Type: AWS::AutoScaling::ScheduledAction Properties: MinSize: 2 MaxSize: 10 Recurrence: 0 12 * * * # タイムゾーンUTCで書いてあります AutoScalingGroupName: Ref: AWSEBAutoScalingGroup 0900scalingsetting: Type: AWS::AutoScaling::ScheduledAction Properties: MinSize: 4 MaxSize: 10 Recurrence: 0 0 * * * # タイムゾーンUTCで書いてあります AutoScalingGroupName: Ref: AWSEBAutoScalingGroup |
Dockerrun.aws.json
- インスタンス内で稼働させる Docker コンテナの設定を書いています。
- ドキュメントはこちら http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_v2config.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
<span class="p">{</span> <span class="nt">"AWSEBDockerrunVersion"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nt">"Authentication"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"Bucket"</span><span class="p">:</span> <span class="s2">"elasticbeanstalk-ap-northeast-1"</span><span class="p">,</span> <span class="nt">"Key"</span><span class="p">:</span> <span class="s2">"docker-config/artifactory_config.json"</span> <span class="p">},</span> <span class="nt">"volumes"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"name"</span><span class="p">:</span> <span class="s2">"frontend-log"</span><span class="p">,</span> <span class="nt">"host"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"sourcePath"</span><span class="p">:</span> <span class="s2">"/var/log/frontend"</span> <span class="p">}</span> <span class="p">},</span> <span class="p">{</span> <span class="nt">"name"</span><span class="p">:</span> <span class="s2">"backend-log"</span><span class="p">,</span> <span class="nt">"host"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"sourcePath"</span><span class="p">:</span> <span class="s2">"/var/log/backend"</span> <span class="p">}</span> <span class="p">},</span> <span class="p">{</span> <span class="nt">"name"</span><span class="p">:</span> <span class="s2">"varlog"</span><span class="p">,</span> <span class="nt">"host"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"sourcePath"</span><span class="p">:</span> <span class="s2">"/var/log"</span> <span class="p">}</span> <span class="p">}</span> <span class="p">],</span> <span class="nt">"containerDefinitions"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"name"</span><span class="p">:</span> <span class="s2">"frontend"</span><span class="p">,</span> <span class="nt">"image"</span><span class="p">:</span> <span class="s2">"registry.example.com/frontend:latest"</span><span class="p">,</span> <span class="nt">"memory"</span><span class="p">:</span> <span class="mi">512</span><span class="p">,</span> <span class="nt">"cpu"</span><span class="p">:</span> <span class="mi">256</span><span class="p">,</span> <span class="nt">"portMappings"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"hostPort"</span><span class="p">:</span> <span class="mi">443</span><span class="p">,</span> <span class="nt">"containerPort"</span><span class="p">:</span> <span class="mi">443</span> <span class="p">}</span> <span class="p">],</span> <span class="nt">"links"</span><span class="p">:</span> <span class="p">[</span> <span class="s2">"backend"</span> <span class="p">],</span> <span class="nt">"mountPoints"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"sourceVolume"</span><span class="p">:</span> <span class="s2">"frontend-log"</span><span class="p">,</span> <span class="nt">"containerPath"</span><span class="p">:</span> <span class="s2">"/var/log/nginx"</span><span class="p">,</span> <span class="nt">"readOnly"</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="p">{</span> <span class="nt">"name"</span><span class="p">:</span> <span class="s2">"backend"</span><span class="p">,</span> <span class="nt">"image"</span><span class="p">:</span> <span class="s2">"registry.example.com/backend:latest"</span><span class="p">,</span> <span class="nt">"memory"</span><span class="p">:</span> <span class="mi">2048</span><span class="p">,</span> <span class="nt">"cpu"</span><span class="p">:</span> <span class="mi">768</span><span class="p">,</span> <span class="nt">"portMappings"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"hostPort"</span><span class="p">:</span> <span class="mi">8080</span><span class="p">,</span> <span class="nt">"containerPort"</span><span class="p">:</span> <span class="mi">8080</span> <span class="p">}</span> <span class="p">],</span> <span class="nt">"mountPoints"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"sourceVolume"</span><span class="p">:</span> <span class="s2">"backend-log"</span><span class="p">,</span> <span class="nt">"containerPath"</span><span class="p">:</span> <span class="s2">"/var/log/backend"</span><span class="p">,</span> <span class="nt">"readOnly"</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="p">{</span> <span class="nt">"name"</span><span class="p">:</span> <span class="s2">"logging"</span><span class="p">,</span> <span class="nt">"image"</span><span class="p">:</span> <span class="s2">"registry.example.com/logging:latest"</span><span class="p">,</span> <span class="nt">"memory"</span><span class="p">:</span> <span class="mi">640</span><span class="p">,</span> <span class="nt">"cpu"</span><span class="p">:</span> <span class="mi">512</span><span class="p">,</span> <span class="nt">"mountPoints"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">"sourceVolume"</span><span class="p">:</span> <span class="s2">"frontend-log"</span><span class="p">,</span> <span class="nt">"containerPath"</span><span class="p">:</span> <span class="s2">"/fluentd/log/frontend"</span><span class="p">,</span> <span class="nt">"readOnly"</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">{</span> <span class="nt">"sourceVolume"</span><span class="p">:</span> <span class="s2">"backend-log"</span><span class="p">,</span> <span class="nt">"containerPath"</span><span class="p">:</span> <span class="s2">"/fluentd/log/backend"</span><span class="p">,</span> <span class="nt">"readOnly"</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">{</span> <span class="nt">"sourceVolume"</span><span class="p">:</span> <span class="s2">"varlog"</span><span class="p">,</span> <span class="nt">"containerPath"</span><span class="p">:</span> <span class="s2">"/fluentd/log/docker"</span><span class="p">,</span> <span class="nt">"readOnly"</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> |
どうやって作成されていくのか?
infra-beanstalk/web
に移動して eb create
コマンドで実際に環境を作成すると、以下A〜Eのように環境が構築されます。
やり直したい場合は?
Elastic Beanstalk には、環境を消すという機能も備わっています。
この操作を行うと、 eb create
コマンドで作成した時に作られたものが消えます。
(2016年8月現在、ECS の一部(task definition)で残骸が残ってしまいますが、基本的には殆どの設定が消えます)
消す命令をすると作成したものだけが消えるため、環境の作り直しが非常に簡単に行えます。
具体的にどんなデプロイができるのか
- プロダクトで採用しているデプロイ方法について書いています。
- Elastic Beanstalk がサポートしているデプロイの一覧はこちら(http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/using-features.deploy-existing-version.html)
- Multi-Container Docker Environment なので、デプロイの内容は Dockerrun.aws.json による各インスタンスでの新しい ECS タスクのスタートと各 config ファイルの適用を指します。
ローリングデプロイ
- Elastic Beanstalk のデプロイ機能としてサポートされているもので、一度に全体の何%のインスタンスにデプロイするかなど細かい設定が可能です。
- インスタンスを1台づつ ELB から切離してデプロイ、ヘルスチェックに合格したら ELB に紐付け…を繰り返して全台デプロイします。
- プロダクション(本番)環境でのみ、このデプロイ方法でデプロイしています。
option_settingsに以下を設定することで有効になる
1 2 3 4 5 6 |
option_settings: "aws:elasticbeanstalk:command" : DeploymentPolicy : "Rolling" # 他にもいくつかある。デプロイポリシーについてはこちら http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/using-features.rolling-version-deploy.html BatchSize : 50 # 一度に どのくらいデプロイするか BatchSizeType: Percentage # Fixidの場合、パーセント指定ではなく台数指定となる |
- メリット
ダウンタイムなしデプロイを行うことが可能です。
- デメリット
デプロイに時間がかかります。
50%で実施したとして、AllAtOnce と比較してローリングデプロイは約5倍くらい時間がかかりました。
(Rolling : 約10分、 AllAtOnce : 約2分)
AllAtOnce
- Elastic Beanstalk のデプロイ機能としてサポートされているものです。
- 一度に全てのサーバーにデプロイする方式です。
- 私の担当するプロダクトでは、開発環境、確認環境(dev、stg)のデプロイで使用しています。
- メリット
デプロイがとにかく速いです。
Elastic Beanstalk が提供しているデプロイ方法の中では一番速いです。
- デメリット
一度に全てのインスタンスにデプロイするため、ダウンタイムが発生します。
ブルーグリーンデプロイ
- Elastic Beanstalk の「CNAMEのスワップ」とは別のものです。自分で DNS レコードの付け替えを行います。新しい Elastic Beanstalk の環境を作成して、エイリアスをその環境に向けます。
- サーバー構成に大きく変更があった場合(インスタンスタイプの調整、Docker コンテナの追加)にこのデプロイ方法を実施しています。
- 切り替わりのイメージとしては、以下のように切り替わります。
①まず eb-prd1.example.com
のエイリアスとして example.com
が存在するとします。
②エイリアスを eb-prd1.example.com
から eb-prd2.example.com
へと切り替えます。
各ユーザーの参照する DNS が更新され次第、新しいエイリアス先 (eb-prd2.example.com)
へアクセスが来ます。
③2つの環境 eb-prd1.example.com
と eb-prd2.example.com
が同時に存在する状態となります。
④eb-prd1.example.com
の環境のアクセスログに暫く書き込まれていない事を確認して、 eb-prd1.example.com
の環境を削除します。
- メリット
本番に切り替える前に確認が行える事から、非常に安全にデプロイできます。
また、切替後に異常があった場合でも、切替前の環境を消さない間は切り戻しが簡単に行えます。
CNAME ではサポートされていないルートドメインも対応しています。
また、少し無理矢理ですが以下の手順で事前確認も可能です。