sgykfjsm.github.com

AWS BeanstalkでDockerをカスタムAMIで使う。

既知の通り、BeanstalkでDockerを使うことができるが、通常の使い方だとインスタンスが配備される度にDocker ImageをPull、BuildしてからRunする。初期配備時は問題ないが、スケールアウトの観点で見た場合、非常にもたつくことがある。また、Docker Imageが大きい(800MB以上ぐらい?)と、devicemapper errorでBuildに失敗することが多い(ような気がする)。単にBuildに失敗しただけであれば切り離せば良いが、複数台のうちいくつかがBuildに失敗しただけでは検知が難しく、そのまま生き残ってしまうとムダなコストが発生する。

上記のような認識でいたため、これまではBeanstalkでDockerを運用することにはあまり乗り気ではなかった。しかし、同僚からの意見で予めデプロイしておいたカスタムAMIを使うのはどうか、という意見があり、検証することにした。

なお、結論から言うと、ここで記した方法では実運用に耐えないと思う。アレコレがんばらないで、素直にAmazon EC2 Container ServiceがGAになるのを待ったほうが良い。

1. カスタムAMIの作成

まず、カスタムAMIを作成する。作成の方法はAWSのドキュメントに記載されている。

上記手順は非常に単純だが、注意点は以下の通り。

  • Beanstalk Dockerで利用可能なAMIでインスタンスを起動する。
  • インスタンスを起動するリージョンはBeanstalkと同じリージョンでなければならない。
  • Beanstalkで使われているカスタムAMI IDを事前に調べておくこと。
  • インスタンス起動時にUser dataに以下を埋め込むこと。
1
2
3
#cloud-config
repo_releasever: <repository version number>
repo_upgrade: none

User dataに埋め込むコードについての詳細は割愛するが、repository version numberSupported Platformsに記載されている各Solution StackのAMI列を記載すること。この設定により、lock-on-launch 機能が設定され、セキュリティ更新の自動インストールの無効化がなされる。上記設定はBeanstalkでカスタムAMIを使うために必須である。また、VPC環境下でインスタンスを起動する場合はAuto-assign Public IPをenabledにすることを忘れないこと。

上記を踏まえてCloudFormation用テンプレートを作成した。

こいつを流し込んでやれば、まずはベースとなるAMIを持つインスタンスができる。

2. AMIの初期設定

実際の運用次第だと思うが、最低限必要であろう設定をここで行っておく。個別アプリケーションの設定を一部含んでいるが、不要な場合は設定しなくて良い。

あとはtd-agentのインストールや設定も行っておきたいが、設定のことを考えると長くなりそうなので、今回は割愛する。 ちょっと長いけど、この辺りをやっておくと後々の運用が楽になるんじゃないかと思う。

上記設定後、一旦インスタンスを再起動する。再起動後は以下の様にして、これまでの設定が反映されていることを確認する。

1
2
3
4
5
6
$ ulimit -n
65536
# pgrepの対象はmonitでなくても良い。
$ cat /proc/$(pgrep monit)/limits | grep "Max open files"
Max open files            65536                65536                files
$ sudo monit status

3. Dockerイメージのビルドとコンテナの起動

次に、Dockerコンテナのデプロイを行なう。これは単純にDeckerfileを用意すれば良いが、どのようなファイルを用意すべきかはデプロイ内容による。
今回はすでに必要な設定は済んでおり、アプリケーションを取り込み済みのDockerイメージを用いるので、以下の様な内容になる。

1
FROM sgykfjsm/play-scala-intro:latest

これを元にコンテナの起動を行う。

1
2
3
$ sudo docker run -i -t -d -p 12812:2812 -p 80:9000 --name play-scala-intro -v /etc/localtime:/etc/localtime:ro sgykfjsm/play-scala-intro
Unable to find image 'sgykfjsm/play-scala-intro' locally
Pulling repository sgykfjsm/play-scala-intro

実際の運用では起動後に受け入れテストのようなもので簡単な動作確認を行なうべきだろう。

1
2
$ curl --silent localhost:80 -o - | grep "<title>Welcome to Play</title>"
        <title>Welcome to Play</title>

4. 起動スクリプトの用意と再起動処理の上書き

インスタンス起動時にカスタムAMIでビルドしたDockerコンテナが起動するようにupstartスクリプトを用意する。これは後述するダミー用のデプロイアプリケーションで配布するため、.ebextensionsの書式に従って以下の様なものを用意する。

インスタンス初期配備時のための設定。

03-run-dummy-app.config / Docker用のカスタムAMIを使うためにダミーのアプリケーションに仕込んでおく.ebextensionslink
1
2
3
4
5
6
7
8
9
10
11
12
13
---
commands:

    01-run-dummy-app:
        command: |
            /usr/bin/docker ps -a | grep -q 'play-scala-intro' || \
            /usr/bin/docker run -d \
                -p 22812:2812 \
                --name play-scala-intro \
                -v /etc/localtime:/etc/localtime:ro \
                sgykfjsm/play-scala-intro
        cwd: /home/ec2-user
        ignoreErrors: false

initctlに登録するための起動スクリプトの設定。

01-make-upstart.config / Docker用のカスタムAMIを使うためにダミーのアプリケーションに仕込んでおく.ebextensionslink
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
---
files:

    "/etc/init/sample-docker-app.conf" :
        mode: 755
        owner: root
        group: root
        content: |
            description "Elastic Beanstalk Docker Container sample-docker-app"

            start on started docker
            stop on stopping docker

            respawn

            script
                # "play-scala-intro" is *exmaple*. This must be replaced with real container name.
                CONTAINER_NAME="play-scala-intro"

                # Wait for docker to finish starting up first.
                FILE=/var/run/docker.sock
                while [ ! -e ${FILE} ]; do
                    sleep 2
                done

                DOCKER_APP_FILE=/etc/elasticbeanstalk/.aws_beanstalk.current-container-id
                CONTAINER_ID=$(docker ps --no-trunc -a| grep ${CONTAINER_NAME} | cut -d" " -f1)
                if ! docker ps | grep -q ${CONTAINER_ID} ; then
                    docker start ${CONTAINER_ID} > ${DOCKER_APP_FILE}
                fi

                NGINX_UPSTREAM_IP=$(docker inspect ${CONTAINER_ID} | jq ".[0].NetworkSettings.IPAddress" --raw-output)
                # "9000" is *example*. This must be replaced with real application settings.
                NGINX_UPSTREAM_PORT=9000

                DOCKER_PORT_FILE=/etc/elasticbeanstalk/.aws_beanstalk.container-port
                if ! cat /etc/nginx/conf.d/elasticbeanstalk-nginx-docker-upstream.conf | grep -q $NGINX_UPSTREAM_IP; then
                    sed -i.$(date '+%Y%m%d%H%M%S.%Z')  \
                        "s/server.*;/server ${NGINX_UPSTREAM_IP}:${NGINX_UPSTREAM_PORT};/" \
                        /etc/nginx/conf.d/elasticbeanstalk-nginx-docker-upstream.conf
                    service nginx restart
                fi
                echo ${NGINX_UPSTREAM_PORT} > ${DOCKER_PORT_FILE}

                mkdir -p /var/log/eb-docker/containers/eb-current-app
                docker logs -f ${CONTAINER_ID} > /var/log/eb-docker/containers/eb-current-app/${CONTAINER_ID:0:12}-stdouterr.log 2>&1

                exec docker wait ${CONTAINER_ID}
            end script

            post-stop script
                CONTAINER_ID=$(docker ps --no-trunc | grep 'play-scala-intro' | cut -f1 -d' ')

                if [ -n "${CONTAINER_ID}" ] ; then
                    docker stop ${CONTAINER_ID}
                fi
            end script

Beanstalkではインスタンスの起動やアプリケーションのデプロイなどをフックにして様々なスクリプトが起動する。それらのうち、今回はAWSの管理コンソールからアプリケーションの再起動ができるように、再起動処理のスクリプトを上書きをする。これも.ebextensionsに仕込んでおく。

その他の.ebextensionsについてはgistを参照すること。

5. Beanstalkインスタンス用のダミーアプリケーションを用意する。

Beanstalkのインスタンス配備時にはデプロイアプリケーションが必要だ。上記で設定した.ebextensionsたちも同梱してデプロイしたいので、適当なアプリケーションを用意する。

Dockerfile / BeanstalkのDockerプラットフォーム向けのダミーアプリケーションlink
1
2
3
4
5
6
7
FROM dockerfile/python

ADD application.py /usr/local/share/application.py

EXPOSE 8000

ENTRYPOINT ["python", "/usr/local/share/application.py"]
Dockerrun.aws.json / BeanstalkのDockerプラットフォーム向けのダミーアプリケーションlink
1
2
3
4
5
6
7
{
  "AWSEBDockerrunVersion": "1",
  "Logging": "/var/log/dummy-application",
  "Image": {
      "Update": "false"
  }
}
application.py / BeanstalkのDockerプラットフォーム向けのダミーアプリケーションlink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# vim:fenc=utf-8
from wsgiref.simple_server import make_server, WSGIServer
from SocketServer import ThreadingMixIn

def application(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/html')])
    return ['ok']

class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
    pass

if __name__ == '__main__':
    httpd = make_server('', 8000, application, ThreadingWSGIServer)
    httpd.serve_forever()

6. カスタムAMIを使ったBeanstalk環境を構築する。

管理コンソールからBeanstalk環境を構築すると、カスタムAMIを指定することができないため、環境を構築した後にConfigurationからカスタムAMIを変更する必要がある。これだと1時間分のムダな課金が発生するし、自動化が面倒である。なので、ここでもCloudFormationを使ってBeanstalk環境を構築する。

sample-docker-with-custom-ami.template / BeanstalkでカスタムAMIを使ったDocker環境を構築するCloudFormationテンプレートlink
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "ApparelCloud MediaAPI Template on elastic beanstalk for Non-VPC",
    "Parameters": {
        "ApplicationName": {
            "Description": "Beanstalk Application Name",
            "Type": "String"
        },
        "BeanstalkInstanceType": {
            "AllowedValues": [
                "t2.micro",
                "t2.small",
                "t2.medium",
                "m3.medium",
                "m3.large",
                "m3.xlarge",
                "m3.2xlarge",
                "c3.large",
                "c3.xlarge",
                "c3.2xlarge",
                "c3.4xlarge",
                "c3.8xlarge",
                "r3.large",
                "r3.xlarge",
                "r3.2xlarge",
                "r3.4xlarge",
                "r3.8xlarge"
            ],
            "ConstraintDescription": "must be a valid EC2 instance type.",
            "Description": "Bastion Host EC2 instance type",
            "Type": "String"
        },
        "BeanstalkSecurityGroup": {
            "Description": "Security Group Id for BeanstalkInstace",
            "Type": "String"
        },
        "CustomAmiId": {
            "Description": "You can override the default Amazon Machine Image (AMI) by specifying your own custom AMI ID.",
            "Type": "String"
        },
        "CNAMEPrefix": {
            "Description": "Endpoint prefix for environment",
            "Type": "String"
        },
        "EnvironmentName": {
            "Description": "Environment Name on This Application",
            "Type": "String"
        },
        "HostName": {
            "Description": "The URL name for bastion instance",
            "Type": "String"
        },
        "HostedZone": {
            "Description": "The DNS name of an existing Amazon Route 53 hosted zone",
            "Type": "String"
        },
        "KeyName": {
            "AllowedPattern": "[\\x20-\\x7E]*",
            "ConstraintDescription": "can contain only ASCII characters.",
            "Description": "Name of an existing EC2 KeyPair to enable SSH access to the Elastic Beanstalk hosts",
            "MaxLength": "255",
            "MinLength": "1",
            "Type": "String"
        },
        "PrivateSubnet": {
            "Description": "logical private subnet id existing vpc",
            "Type": "String"
        },
        "SSHSecurityGroup": {
            "Description": "Bastion Security Group Id",
            "Type": "String"
        },
        "SolutionStackName": {
            "Description": "Application Platform(http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/concepts.platforms.html)",
            "Type": "String"
        },
        "VPC": {
            "Description": "logical VPC id",
            "Type": "String"
        },
        "VPCDefaultSecurityGroup": {
            "Description": "VPC Default Security Group Id",
            "Type": "String"
        }
    },
    "Resources": {
        "SampleDocker2Environment": {
            "Properties": {
                "ApplicationName": {
                    "Ref": "ApplicationName"
                },
                "CNAMEPrefix": {
                    "Ref": "CNAMEPrefix"
                },
                "Description": "",
                "EnvironmentName": {
                    "Ref": "EnvironmentName"
                },
                "OptionSettings": [
                    {
                        "Namespace": "aws:ec2:vpc",
                        "OptionName": "AssociatePublicIpAddress",
                        "Value": "true"
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "ImageId",
                        "Value": {"Ref": "CustomAmiId"}
                    },
                    {
                        "Namespace": "aws:elasticbeanstalk:environment",
                        "OptionName": "EnvironmentType",
                        "Value": "SingleInstance"
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "SSHSourceRestriction",
                        "Value": {
                            "Fn::Join": [
                                "",
                                [
                                    "tcp,22,22,",
                                    {
                                        "Ref": "SSHSecurityGroup"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "IamInstanceProfile",
                        "Value": "aws-elasticbeanstalk-ec2-role"
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "SecurityGroups",
                        "Value": {
                            "Ref": "BeanstalkSecurityGroup"
                        }
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "SecurityGroups",
                        "Value": {
                            "Ref": "VPCDefaultSecurityGroup"
                        }
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "EC2KeyName",
                        "Value": {
                            "Ref": "KeyName"
                        }
                    },
                    {
                        "Namespace": "aws:autoscaling:launchconfiguration",
                        "OptionName": "InstanceType",
                        "Value": {
                            "Ref": "BeanstalkInstanceType"
                        }
                    },
                    {
                        "Namespace": "aws:ec2:vpc",
                        "OptionName": "VPCId",
                        "Value": {
                            "Ref": "VPC"
                        }
                    },
                    {
                        "Namespace": "aws:ec2:vpc",
                        "OptionName": "Subnets",
                        "Value": {
                            "Ref": "PrivateSubnet"
                        }
                    }
                ],
                "SolutionStackName": {
                    "Ref": "SolutionStackName"
                },
                "Tier": {
                    "Name": "WebServer",
                    "Type": "Standard",
                    "Version": "1.0"
                }
            },
            "Type": "AWS::ElasticBeanstalk::Environment"
        },
        "SampleDocker2InstanceDNSRecord": {
            "Properties": {
                "Comment": "A record for ac-media instance.",
                "HostedZoneName": {
                    "Fn::Join": [
                        "",
                        [
                            {
                                "Ref": "HostedZone"
                            },
                            "."
                        ]
                    ]
                },
                "Name": {
                    "Fn::Join": [
                        "",
                        [
                            {
                                "Ref": "HostName"
                            },
                            ".",
                            {
                                "Ref": "HostedZone"
                            },
                            "."
                        ]
                    ]
                },
                "ResourceRecords": [
                    {
                        "Fn::Join": [
                            "",
                            [
                                {
                                    "Ref": "CNAMEPrefix"
                                },
                                ".",
                                "elasticbeanstalk.com",
                                "."
                            ]
                        ]
                    }
                ],
                "TTL": "300",
                "Type": "CNAME"
            },
            "Type": "AWS::Route53::RecordSet"
        }
    }
}

まとめ

ここまで長々と書いたが、見ての通り、それなりに独自スクリプトを用意したり既存のhooksスクリプトを上書きするなどの対応が必要である。また、上記ではConfig Deployへの対応をしていないし、スケールアウトの安定性の検証は未確認である(ちょっと試した感じだと早いが、実業務で使うような巨大イメージではまだ試していない)。管理コンソールのEventsに表示されるコンテナIDはeb-dockerのままなので、書き換えたスクリプトやnginxの設定ファイルの監視も追加したほうが良いだろう。

結局のところ、既存の設定書き換えやコンテナの停止は割とリスキーである。なぜならAMI(Platform)のバージョン間で同じ設定ないしは同じスクリプトが使われているとは限らないし、以前のバージョンには無かったスクリプトの追加、あるいはスクリプトの仕様変更があるかもしれないからだ。そのため、AMIのバージョンを変える時は/opt/elasticbeanstalk配下のスクリプトを充分に把握しておく必要がある。

敢えてポジティブなことを言えば、カスタムAMIを作りこむことでDocker Wayに則ったシングルインスタンス・マルチサービスが可能になるし、かつ運用に堪えるような設定を施すことができれば、Beanstalkの便利機能(柔軟なスケールアウトやRolling Update、メトリクス管理)を使えるので、かなり嬉しいと思う。とは言え、やはりリスキーには変わりないので、今のところだと、このようなやり方では実運用に耐えないと思う。


色々試行錯誤した割に、こう言う結果になって残念…