DockerとECSでInfrastructure as codeを体感する

f:id:watass:20151223222329p:plain

Infrastructure as codeの思想に感動し、AWSを使い始めて早1年になりますが、なんだかんだで最近はコードを書いてばかりでAWSを触っていませんでした。貴重な無料期間も終了し、非常にもったいないことをしたなと反省中です。

さて気を取り直して、ついに先日 EC2 Container Registry (ECR)がアナウンスされましたね!

まだUSリージョンのみの公開で、相変わらず東京リージョンはもう少しかかりそうですが、これでDockerイメージの管理までAWS上で行えるようになるわけです。

ところで、インフラ構築の自動化といえば、Elastic BeanstalkやOpsWorks、CloudFormationなどなどAWS上でもサービスが乱立しており、少し外に目を向ければ、Chef、Puppet、Ansible、Terraformなどなど、もう困っちゃうぐらい選択肢にあふれている昨今です。
ただ、どうにもこれらの選択肢はアプリケーションのデプロイに難があったり、やたらと複雑だったりでイマイチピンと来ない部分があって、インフラのコード化という視点から見るとDockerが一番しっくり来る感じでしたので、今回はECRの東京リージョン公開を期待しつつ、ECSでInfrastructure as code的なものをやってみたいと思います。

Dockerとは

Dockerとは、2014年辺りから注目され始めたコンテナ型仮想化におけるコンテナ管理ソフトウェアです。詳細な説明はもっと専門的なページに任せるとして、注目され始めた背景としては、コンテナ型仮想化が従来の仮想化に比べて効率がよかったことや、Infrastructure as code、Immutable Infrastructureの思想の高まりがあると個人的には考えています。
DockerではDockerfileと呼ばれるファイルを元にDockerイメージを作成し、それを元にコンテナを立ち上げることができます。Dockerfileはシェルスクリプトを書くようなシンプルさで記述できるので、少ない学習コストでインフラをコードに落としこむことができます。また、作成したDockerイメージはDocker Hubや冒頭に紹介したECRにpushすることで、他のユーザもそのイメージを元にコンテナを立ち上げることができます。

Dockerで立ち上がるものはコンテナなので、簡単に破棄して、立ち上げ直すことができます。Dockerfileを変更することでインフラの変更も管理できるので、Infrastructure as codeとImmutable Infrastructureにはまさにぴったりというわけです。

ECSとは

正式名称はEC2 Container Service、クラスタとして構築されたEC2インスタンス上にDockerでコンテナを立ち上げ、コンテナ群を管理することができます。
個人的に大きなメリットだと思っているのは、ELBが内部的にサポートされていて、コンテナはEC2のAvailability Zoneを意識しながら冗長化するように配置されるというポイントです。また、EC2のAuto Scalingのように、内部のコンテナもコンテナ数が常に一定数起動しているように管理されます。

versions

Dockerイメージを作成するローカルマシンにはMac OS X Yosemiteを使用します。

名前 バージョン
docker 1.9.1
docker-compose 1.5.2
docker-machine 0.5.4
VirtualBox 5.0.12

Dockerのインストール

Docker toolboxを使用してインストールします。

f:id:watass:20151223231349p:plain

Mac版のpkgをダウンロードして、展開するとインストーラーが起動しますので、後は案内通りに進めていくだけです。インストールが終わると、QuickstartをTerminal.appまたは類似のコマンドラインツールで実行します。Quickstartが終了すると、ターミナル上にクジラさんのAAが表示されます。愛らしいですね。

準備ができたら、以下のコマンドでコンテナを立ち上げてみます。

$ docker run hello-world

Hello World!を表示するコンテナが立ち上がってすぐに終了します。エラーが特に発生しなければOKです。実行する際に、存在しないhello-worldという名前のDockerイメージをDocker Hubに取得しにいき、取得されたイメージが保存されます。以下のコマンドで登録されているイメージの一覧を確認することができます。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
hello-world         latest              0a6ba66e537a        10 weeks ago        960 B

ちなみに、もうhello-worldのイメージは使用しないので、削除しておきましょう。

$ docker rmi 0a6ba66e537a
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE

Dockerfileを作成

さて、Dockerイメージからコンテナが立ち上がることが確認できたので、Dockerイメージを自作してみます。適当なディレクトリにDockerfileという名前でファイルを作成します。今回はGitHubにおいている自作のFuelPHPサンプルコードをデプロイし、Apacheを起動するようなDockerfileを作成します。

できあがったものがこちら。

Dockerfile

FROM centos:centos6
MAINTAINER wata <wata@example.com>
RUN yum -y update
RUN yum -y install httpd
RUN yum -y install php php-devel
RUN yum -y install git
RUN chkconfig httpd on
EXPOSE 80

WORKDIR /var/www/html
RUN git clone https://github.com/wata727/twitterloginsample.git
WORKDIR twitterloginsample
RUN php composer.phar self-update
RUN php composer.phar update

CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]

使っているDockerfileのコマンドはたった6種類です。

コマンド名 説明
FROM 元となるDockerイメージを指定します。今回はCentOSの6を使用します。
MAINTAINER Dockerfileの作成者です。記載することが望ましいようです。
RUN ビルド時に実行されるコマンドです。普通にシェルスクリプトを書く感覚でスラスラ書けます。
WORKDIR ビルド時の作業ディレクトリです。移動することができます。
CMD コンテナ起動時に実行されるコマンドです。今回はhttpdを実行するように指定しています。

もちろん、他にもDockerfileのコマンドはあるので、必要に応じて様々な操作をすることができますが、今回のような簡単なサンプルであれば6種類もあれば十分に記述できるわけです。
作成できたらDockerfileのある場所でビルドを実行します。

$ docker build -t wata727/fuel-example:0.1 .

このとき、Dockerイメージの名前の構成は {作者名}/{イメージ名}:{タグ} のようになります。この作者名が後述のDocker Hubでのユーザ名と一致しない場合、Dockerイメージの登録に失敗するので注意してください。
ビルドが完了したらイメージを確認。

$ docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
wata727/fuel-example      0.1                 00c9e0908686        About a minute ago   609.6 MB
centos                           centos6             1a895dd3954a        10 weeks ago         190.6 MB

DockerfileのFROM句にcentos6のイメージを指定したので、存在しないイメージをDocker Hubからpullしてきています。それはともかく、肝心のDockerイメージも無事ビルドできているようですね。さっそくコンテナを立ち上げてみます。

$ docker run -d -p 80:80 wata727/fuel-example:0.1
$ docker ps
CONTAINER ID              IMAGE                   COMMAND                  CREATED             STATUS              PORTS                NAMES
53bef123ad58        wata727/fuel-example:0.1   "/usr/sbin/httpd -D F"   31 seconds ago      Up 30 seconds       0.0.0.0:80->80/tcp   berserk_cori

docker psで現在立ち上がっているコンテナについて確認することができます。DockerfileのCMDで指定したhttpdの起動が実行されているようですね。docker runのオプション-pでコンテナ側とホスト側のポート番号のマッピングをしています。この例では双方のポート80番がマッピングされているので、ホストの80番に接続すると、コンテナの80番が対応します。つまり、ホストIPにアクセスすれば、Dockerfileでデプロイしたアプリケーションを確認することができます。

Docker toolboxでインストールした場合、docker-machineを使用しているので、以下のコマンドでホストのIPアドレスを確認できます。

$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER   ERRORS
default   *        virtualbox   Running   tcp://192.168.99.100:2376           v1.9.1

これで、192.168.99.100/twitterloginsample/publicにアクセスすると、いつものFuelPHPのWelcome画面が表示されます。
コンテナの動作が確認できたら、コンテナは終了しておきましょう。以下のコマンドで実行中のすべてのコンテナを終了させられます。

$ docker rm -f $(docker ps -a -q)
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Docker HubへDockerイメージを登録

Dockerイメージの確認ができましたので、ECSでこのDockerイメージを使用できるようにするために、Docker HubにDockerイメージを登録します。まずはユーザ登録をしましょう。

f:id:watass:20151223235940p:plain

Docker HubはDocker社が公開しているDockerイメージのためのレジストリサービスです。ここに登録されているイメージはdockerコマンドで参照することができます。ECSもデフォルトでDocker Hubを参照するようになっています。

ユーザ登録ができたら、コマンドラインからログインし、先ほど作成したDockerイメージを指定してpushします。

$ docker login
$ docker push wata727/fuel-example:0.1

これでしばらくすれば、Docker Hubにイメージが公開されます!

ECSの設定

Dockerイメージの作成および公開ができましたので、後はこれを使ってECS側の設定を行います。ECSはタスク、サービス、クラスタといった独自の概念を持っておりますので、ひとつずつ説明していきます。

タスク定義

f:id:watass:20151224000526p:plain

タスクはDockerのコンテナ起動のために行う設定です。使用するDockerイメージ、コンテナに割り当てるメモリ量、ホストとコンテナのポート番号のマッピングなどが該当します。今回はDocker Hubに公開したDockerイメージを使用するので、そのままpushしたときと同様のフォーマットで指定します。

サービスの設定

f:id:watass:20151224001143p:plain

サービスはタスクを実行するためのスケジューラーです。これがタスクと別に分離しているのがなぜなのかよくわからないのですが、ELBによる冗長化のオプションはここで設定できます。今回はELBは使用しません。また、サービスの数についてはよくわかりませんが、おそらくタスクの数と同様になるのでしょうか・・・とりあえず今回は1インスタンス、1コンテナの構成なのでサービスも1つにしておきます。

クラスタの構築

f:id:watass:20151224001429p:plain

クラスタはコンテナを動かすEC2インスタンス群です。どのインスタンスタイプを使うか、いくつインスタンスを起動するか、セキュリティグループはどうするか、IAMロールはどうするかなどの設定ができます。今回はシンプルに1インスタンスにします。また、SSHのポートを開けていないので、鍵の設定はなしにします。

動作確認

以上で設定完了になりますので、ECSを起動します。すると、EC2とECSのステータスが進行中になりますので、それらがすべて完了するのを待ちます。

f:id:watass:20151224001759p:plain

上記のようにECSのステータスがオールグリーンになったら、デプロイ完了です。クラスタ一覧から起動しているインスタンスの状態を確認できるので、ちょっと見てみましょう。

f:id:watass:20151224002052p:plain

ここにあるIPアドレスで公開されているので、ローカルでの動作確認時同様、{ホスト名}/twitterloginsample/publicでFuelPHPのWelcome画面を見ることができます。

まとめ

  • Dockerfileでインフラをコードとして記述できる
  • DockerイメージはDocker Hubなどで公開すればECSからも参照できる
  • ECSはEC2インスタンス上にコンテナを立ち上げて、ELBなどで冗長化できる

DockerとECSの今後の展開に期待

ECSを触ってみた感じ、簡単かつ違和感なくローカル同様にコンテナを起動できたのはよかったのですが、セキュリティグループやVPCが自動でECS用に作成されてしまうようでした。できれば既存のセキュリティグループやVPCを使えるようになってくれると、部分的なコンテナ化と柔軟な構成ができていいなーと思います。

他のInfrastructure as codeの技術としては、OpsWorksやTerraformに注目していたのですが、OpsWorksはひとつのコードですっきり管理!という感じではなくて結構がっちりした大規模なアプリケーションを管理するためのもので、小規模なインフラにはあまり向かないかなと感じました。Terraformはもともとインフラ構築のみで、サーバの中身そのものはPackerなどを使ってAMIを作って、それをTerraformでデプロイするという役割分担をしていたため、2つのツールを使うのではなく、できれば1つのツールで完結したいなぁと考えた結果、Dockerに今回行き着きました。

とはいえ、実はECSもそれなりに規模の大きなアプリケーションを対象にしていたりするのでは?とやっているうちに感じることがあったので、Packer + Terraformの構成も少し試してみたいですね。

Dockerのコンテナによるアプリケーションの実運用事例はあまり多く聞かないので、今後過酷な本番環境でも問題なく運用できる事例が増えて普及が進んでくれたらいいなと思います。ただ、Dockerには色々な批判もあるので、最終的にはDockerである必要はないかもしれませんが、アプリケーションの開発者もインフラの担当者も幸せになれるようなツールが出てきてくれることを望んでいます。

インフラがコードでプログラミングできるようになったこの時代、プログラミングが好きでプログラマになったバックエンドエンジニアの人たちもインフラは担当領域に入ってきてもいいのではないかなと最近感じています。俺は私はプログラマだから〜と言わず、AWSでInfrastructure as codeの楽しさを体験してみては。