今年の初頭に「Phoenixアプリのホットデプロイ完全自動化」の記事を書いてから一年が過ぎようとしている。この自動化は Elixir/Erlang の Hot swapping 機能を利用していて、git push
から10分以内でデプロイが完了するという、当時としてはそこそこ満足のいく達成だったのだが、こんな不具合や、exrm (Elixir Release Manager) 作者の「hot upgrades はあんまりオススメ出来ない発言」などを見るにつけ、これを本番で使うのはちょっと辛いかもしれないと思うようになった。
今回、Cotoami プロジェクト を始めるに当たって、前々から気になっていた Google の Kubernetes(クバネテス)を試してみようと思い立った。そして実際に自動化の仕組みを構築してみて、その簡単さと仕組みの先進さに驚いた。言語に依存しないマイクロサービスのパッケージングと、それらを組み合わせて簡単にスケーラブルなWebサービスを構築できる仮想環境。これで本格的にコンテナの時代が来るんだなという新しい時代の訪れを感じずにはいられない。
というわけで、以下では Kubernetes を使った自動化の詳細について紹介したいと思う。この仕組みの全貌は Cotoami プロジェクトの一部として公開しているので、興味のある方は以下の GitHub プロジェクトを覗いて頂ければと思う。
- Cotoami https://github.com/cotoami
- 自動化実現直後のバージョン – https://github.com/cotoami/cotoami/tree/auto-deployment
- 目次
Kubernetes とは何か?
Kubernetes が提供する仕組みは Container Orchestration と呼ばれている。Container Orchestration とは、Docker のようなコンテナ(アプリケーションを実行環境ごとパッケージングする仕組み)で実現されている小さなサービス(マイクロサービス)を組み合わせて、より大きなサービスを作るための仕組みである。
今では、Webサービスを複数のサービス(プロセス)の連携として実現することが当たり前になって来ている。次第に細かくなりつつあるこれらのサービスを扱う時の最大の障害が従来型の重い仮想化だ。例えば、Amazon Machine Images (AMI) のような従来型の仮想化技術を使ってサービスを更新する場合、イメージをビルドするのに20分から30分程度、更にそれを環境にデプロイするのに10分以上かかってしまう。自動化も容易ではない。サービスの数が多くなるほどに時間的なペナルティが積み重なってしまい、マイクロサービスのメリットを享受するのは難しくなる。なので、実際はマシンイメージをデプロイの単位にすることはせずに、言語やフレームワーク固有のパッケージに頼ったデプロイを行っている現場が多いのではないだろうか。
これらの問題を一挙に解決しようとするのが、Docker のような軽い仮想化と、それらをまるでソフトウェアモジュールのように組み合わせることを可能にする Container Orchestration 技術である。
Kubernetes を最短で試す
複数サービスの連携を、ローカルマシンで簡単に試せるというのも Kubernetes のようなツールの魅力だ。Kubernetes には Minikube というスグレモノのツールが用意されていて、ローカルマシン上に、お手軽に Kubernetes 環境を立ち上げることが出来る。
以下では、Mac OS X での手順を紹介する。
1. VirtualBox をインストールする
- 以下からパッケージをダウンロードしてインストール
筆者の環境:
$ vboxmanage --version 5.1.8r111374
2. Minikube をインストールする
$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.12.2/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/ $ minikube version minikube version: v0.12.2
3. Kubernetes を操作するためのコマンドツール kubectl をインストールする
$ curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.3.0/bin/darwin/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/
4. Minikube を起動する
$ minikube start Starting local Kubernetes cluster... Kubectl is now configured to use the cluster.
以下のような情報を見れれば、準備は完了。
$ kubectl cluster-info Kubernetes master is running at https://192.168.99.101:8443 KubeDNS is running at https://192.168.99.101:8443/api/v1/proxy/namespaces/kube-system/services/kube-dns kubernetes-dashboard is running at https://192.168.99.101:8443/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard $ kubectl get nodes NAME STATUS AGE minikube Ready 5d
5. サンプルプロジェクトをデプロイしてみる
Kubernetes には色んなサンプルプロジェクトが用意されているが、ここでは Guestbook という簡単なアプリを試してみる。
以下のファイル(guestbook-all-in-one.yaml
)を適当な場所に保存して、
以下のコマンドを実行してデプロイする。
$ kubectl create -f guestbook-all-in-one.yaml service "redis-master" created deployment "redis-master" created service "redis-slave" created deployment "redis-slave" created service "frontend" created deployment "frontend" created
これによって、以下の3つの Deployments と、
$ kubectl get deployments NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE frontend 3 3 3 3 5m redis-master 1 1 1 1 5m redis-slave 2 2 2 2 5m
それぞれの Deployments に対応する3つの Services(kubernetes
はシステムのサービスなので除く)が出来上がっていることが分かる。
$ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE frontend 10.0.0.118 <none> 80/TCP 7m kubernetes 10.0.0.1 <none> 443/TCP 6d redis-master 10.0.0.215 <none> 6379/TCP 7m redis-slave 10.0.0.202 <none> 6379/TCP 7m
簡単に説明すると、Deployment は一つのマイクロサービスのクラスタに対応し、Service はそのクラスタへのアクセス手段を提供する。
たったこれだけの手順で、冗長化された Redis をバックエンドにした、アプリケーションの環境が出来上がってしまった。構成の全ては guestbook-all-in-one.yaml
というテキストファイルに定義されている。
早速ブラウザでアクセスして試してみたいところだが、デフォルトの設定だとサービスが Kubernetes の外部には公開されていないので、frontend
サービスの設定をちょっと書き換えて(guestbook-all-in-one.yaml
に以下のような感じで type: NodePort
の行を追加する)、
apiVersion: v1 kind: Service metadata: name: frontend labels: app: guestbook tier: frontend spec: ports: - port: 80 type: NodePort selector: app: guestbook tier: frontend
以下のコマンドを実行して設定ファイルの更新を環境に適用する。
$ kubectl apply -f guestbook-all-in-one.yaml
更新が完了したら、以下のコマンドでアプリケーションのURLを知ることが出来る。
$ minikube service frontend --url http://192.168.99.101:31749
以下のようなページが表示されただろうか?
6. お片づけ
先ほどのサンプルプロジェクトで作ったリソースは、以下のコマンドで全部削除出来る。
$ kubectl delete -f guestbook-all-in-one.yaml
Minikube の停止は以下。
$ minikube stop
Phoenix/Elm アプリの Docker イメージを作る
さて、Cotoami の話に戻ろう。Cotoami では、以下のような構成で自動化を実現しようとしている。
CircleCI 上のビルドで Docker イメージをビルドして Docker Hub にリリース。その後、AWS上に構築した Kubernetes に更新の命令を出して、新しいイメージでアプリケーションの Rolling Update(無停止デプロイ)を行う。
この仕組みを構築するためには、まず Phoenix/Elm アプリケーションを Docker でパッケージングするための Dockerfile
を用意する必要がある。しかし、ここで気をつけなければならないのは、パッケージングそのものよりも、CircleCI 上でどうやって Phoenix/Elm アプリケーションをビルドするかという問題である。
Elixirアプリケーションは、クロスコンパイル・ビルドが出来るという説明もあるが、実行環境とビルド環境は合わせておいた方が良いというアドバイスもよく見かけるので、Cotoami ではよりトラブルが少なそうな、環境を合わせるアプローチを取ることにした。
今回の例では、実行環境も Docker 上になるので、まずビルド用の Docker イメージを用意しておき、それを使ってアプリケーションのコンパイルとテストを行い、その後、そのイメージをベースにしてアプリケーションをパッケージングするという、docker build
の二段構え方式でビルドを実施する。
まずは、以下の Dockerfile
で Phoenix/Elm アプリのビルド環境を作る。
- Phoenix/Elm アプリのビルド環境
Dockerfile
一度作ったイメージは、CircleCI のキャッシュディレクトリに入れておき、後々のビルドで使い回せるようにしておく。この辺の設定は全て circle.yml
に書く。
- CircleCI のビルド設定
circle.yml
アプリケーションのコンパイルとテストが終わったら、ビルド用のイメージをベースにして、アプリケーションのパッケージングを行う。そのための Dockerfile
が以下である。
- Phoenix/Elm アプリのパッケージング
Dockerfile
これらの組み合わせで、git push
する度に、Docker Hub にアプリケーションのイメージがリリースされるようになる(Docker Hub に docker push
するために、CircleCI に 認証用の環境変数を設定しておくこと: DOCKER_EMAIL
, DOCKER_USER
, DOCKER_PASS
)。
参考: Continuous Integration and Delivery with Docker – CircleCI
AWS上に Kubernetes 環境を作る
アプリケーションの Docker イメージが用意出来たら、それを動かすための Kubernetes 環境を作る。今回は AWS 上に Kubernetes 環境を構築することにした。
Kubernetes から kops という、これまた便利なツールが提供されていて、これを使うと簡単に環境を構築出来る。
1. kops のインストール
Mac OS の場合:
$ wget https://github.com/kubernetes/kops/releases/download/v1.4.1/kops-darwin-amd64 $ chmod +x kops-darwin-amd64 $ mv kops-darwin-amd64 /usr/local/bin/kops
2. Kubernetes 用のドメイン名を用意する
ここが比較的厄介なステップなのだが、kops による Kubernetes 環境はドメイン名を名前空間として利用する仕組みになっている。具体的には、Route 53 内に Kubernetes 環境用の Hosted zone を作る必要がある。
例えば、立ち上げようとしているWebサービスのドメインが example.com
だとすれば、k8s.example.com
のような専用の Hosted zone を用意する(k8s
は Kubernetes の略称)。
Cotoami の場合、AWS のリソースは出来るだけ Terraform を利用して管理することにしているので、Terraform で Hosted zone を設定する際の例を以下に置いておく。
resource "aws_route53_zone" "main" { name = "example.com" } resource "aws_route53_zone" "k8s" { name = "k8s.example.com" } resource "aws_route53_record" "main_k8s_ns" { zone_id = "${aws_route53_zone.main.zone_id}" name = "k8s.example.com" type = "NS" ttl = "30" records = [ "${aws_route53_zone.k8s.name_servers.0}", "${aws_route53_zone.k8s.name_servers.1}", "${aws_route53_zone.k8s.name_servers.2}", "${aws_route53_zone.k8s.name_servers.3}" ] }
主ドメインとなる example.com
の Hosted zone について、サブドメイン k8s
の問い合わせを委譲するような NS レコードを登録しておくのが味噌。
以下のコマンドを叩いて、DNSの設定がうまく行っているかを確認する。
$ dig NS k8s.example.com
上で設定した4つの NS レコードが見えれば OK。
3. kops の設定を保存するための S3 bucket を作る
kops は、Amazon S3 上に保存された構成情報に基づいて環境の構築・更新などを行う。というわけで、予めそのための S3 bucket を作っておき、その場所を環境変数 KOPS_STATE_STORE
に設定する。
$ aws s3 mb s3://kops-state.example.com $ export KOPS_STATE_STORE=s3://kops-state.example.com
これで、準備は完了。いよいよ Kubernetes の環境を立ち上げる。
4. Kubernetes の設定を生成する
新しい環境の名前を staging.k8s.example.com
として、以下のコマンドで新規環境の設定を生成する。生成された設定は先ほどの S3 bucket に保存される。
$ kops create cluster --ssh-public-key=/path/to/your-ssh-key.pub --zones=ap-northeast-1a,ap-northeast-1c staging.k8s.example.com
Kubernetes ノードにログインするための ssh キーや、ノードを展開する Availability Zone などを指定する。細かいオプションについては、以下を参照のこと。
- kops/kops_create_cluster.md at master · kubernetes/kops
デフォルトでは、以下のような構成の環境が立ち上がるようになっている。
- master (
m3.medium
) - node (
t2.medium
* 2)
5. Kubernetes 環境を立ち上げる
Kubernetes 環境を AWS 上に立ち上げる。単純に以下のコマンドを実行すれば良いのだが、
$ kops update cluster staging.k8s.example.com --yes
Terraform の設定ファイルを生成するオプションもあるので、Cotoami ではその方法を取ることにした。
$ kops update cluster staging.k8s.example.com --target=terraform $ cd out/terraform $ terraform plan $ terraform apply
生成されたデフォルトの構成から、セキュリティグループなどをより安全な設定にカスタマイズすることもあると思われるが、これらのファイルは自動生成によって更新される可能性があることに注意する必要がある。ファイルを直接編集すると、新しく生成したファイルに同じ変更を施すのを忘れてしまう可能性が高い。なので、AWS のコンソール上で直接カスタマイズした方が良いかもしれない(新しい設定ファイルとの齟齬は terraform plan
の時に気づける)。
どのようなファイルが生成されるか興味のある方は、Cotoami のリポジトリを覗いてみて欲しい。
環境を立ち上げる過程で、kop によって kubectl の設定も自動的に追加されている。以下のコマンドを実行すれば、AWS上の環境に接続していることが確認できるはずだ。
$ kubectl cluster-info Kubernetes master is running at https://api.staging.k8s.example.com KubeDNS is running at https://api.staging.k8s.example.com/api/v1/proxy/namespaces/kube-system/services/kube-dns
Kubernetes 上にアプリケーションをデプロイする
Kubernetes の準備は整ったので、後はアプリケーションをデプロイするだけである。Minikube のところでサンプルアプリをデプロイしたのと同じように、サービスの構成情報を YAML ファイルに定義しておき、kubectl create
コマンドでデプロイを行う。
Cotoami の構成ファイルは以下に置いてある。
$ kubectl create -f deployment.yaml $ kubectl create -f service.yaml
設定ファイルの仕様については Kubernetes のサイトを参照して頂くとして、内容自体は単純だということはお分かり頂けると思う。deployment.yaml
では、アプリケーションの Docker イメージ名やクラスタを構成するレプリカの数、ポート番号などが指定されている。service.yaml
では、そのサービスを外部にどのように公開するかという設定がされており、面白いのは type: LoadBalancer
と書いておくと、AWS の ELB が自動的に作成されてアプリケーションのエンドポイントになるところだろうか。
デプロイ自動化をビルド設定に組み込む
最初のデプロイが無事に成功すれば、無停止更新の仕組みは Kubernetes 上に用意されている。後はそれを利用するだけである。
CircleCI から Kubernetes にアクセスするためには、以下のような準備が必要になる。
- kubectl のインストール
- CircleCI には用意されていないのでビルド中にインストールする必要がある。
- Cotoami では以下のシェルスクリプトでインストールと設定を行っている。
- kubectl の設定
ensure-kubectl.sh
では、環境変数S3_KUBE_CONF
に設定された Amazon S3 のパスから kubectl の設定ファイルをビルド環境にコピーする。- Kubernetes on AWS を構築する過程でローカルに出来上がった設定ファイル
~/.kube/config
を S3 にコピーして、その場所を CircleCI の環境変数S3_KUBE_CONF
に設定する。- この設定ファイルには、Kubernetes にアクセスするための credential など、重要な情報が含まれているので、取り扱いには注意すること!
- CircleCI 側から S3 にアクセスするためのユーザーを IAM で作成して最低限の権限を与え、その credential を CircleCI の AWS Permissions に設定する。
これらの設定が完了すれば、ビルド中に kubectl コマンドを呼び出せるようになる。Cotoami の場合は、circle.yml
の deployment
セクションに、以下の二行を追加するだけで自動デプロイが行われるようになった。
https://github.com/cotoami/cotoami/blob/auto-deployment/circle.yml
- ~/.kube/kubectl config use-context tokyo.k8s.cotoa.me - ~/.kube/kubectl set image deployment/cotoami cotoami=cotoami/cotoami:$CIRCLE_SHA1
長くなってしまったが、以上が自動化の全貌である。