WHY
『変化に強いインフラ』を作ることで、技術にこだわり続ける環境ができ、ビジネスの変化にいち早くキャッチアップできます。
そのためにどのようにして、『変化に強いインフラ』を作ることが出来るのか模索したものをまとめます。
WHAT
- Kubernetes 上にアプリケーションを載せる
- CI/CD 環境構築
- GitHub Flow の開発スタイルでを元に QA で自分で書いたコードが確認でき、マージをしてmasterへpushしたら、Produciton へすぐにデプロイする
- サーバースペックを簡単に変えれる/内部で使われるライブラリ等も変更しやすいようにする
- Deploy の仕組みを自由に変更できる
ソースコードは以下です。
ref. GitHub Flow
『変化に強いインフラ』を作っていく上での定義とルール
変化に強いインフラは、少なくとも以下の2つが実行出来ている状態と定義します。
- 継続的にリリース出来ること
- (利用する言語を含め)サービスの構成を自由に変更出来ること
そのために2つのポイントを抑える必要があると考えました。
- オペレーションを統一化
- アプリケーション固有の方言をなくす
※具体的に今回はこの部分をサンプル実装しています。
ref. koudaiii/jjug-ccc2016fall-devops-demo
オペレーションを統一化
アプリケーション作成と同時に、必ずオペレーションが存在します。
- リリースまでのオペレーションを統一化
- 保守作業によるオペレーションを統一化
リリースまでのオペレーションを統一化
サンプルのアプリケーションを元にリリースまでのオペレーションを以下のようにしました。
- ブランチを切り、プルリクエストを送る
- git push する度にテストが実行される
- テストが通れば QA に deploy され、ブラウザで確認する
- リリース出来るタイミングになったら master にマージする
- CI 上でテストが走り、テストが通れば Production にリリースする
実際のフローとなっているコードと実際にリリースするときの画面です。
- .travis.yml https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/.travis.yml#L33-L42
- build log https://travis-ci.org/koudaiii/jjug-ccc2016fall-devops-demo/builds/183488430
このように型を決めることで、どんどん継続的にリリースすることが可能になります。
保守作業によるオペレーションを統一化
アプリケーションの特徴を考えてみます。
- バッチのような一回限りのジョブ
- Cron で定期的に実行するジョブ
- ジョブキューを常に実行するワーカー(sidekiq, delayed_job)
- ウェブアプリケーション
- 起動時にデータを全てオンメモリーに乗せてから実行するアプリケーション
- etc
このようなものに対して、必ず以下のようなオペレーションがあると思います。
- Release (delete/create)
- Scale (scale-out/scale-in/scale-up/scale-down)
- Run (start/stop/restart)
- Log
- Console (exec)
この中でばらつきが出やすい Release について考えてみたいと思います。
- 「 Blue-Green Deployment で出したい」
- 「 Rolling Deployment で出したい」
- 「一回のバッチ処理のため、ウェブアプリケーションと同じリリース方法で出来ない」
- 「リリース作業自体が Cron を設定するだけ」
このような様々な特徴あるオペレーションを統一化するために Kubernetes を使います。
マニフェストファイルと kubectl
を用いたオペレーション
Kubernetes 上にアプリケーションを実行する場合、 マニフェストファイルというスペック等を記載した yaml(またはjson)ファイルを書きます。
書き終えたら `kubectl create -f ./your_manifest.yaml`
で実行するとリリースされます。
ジョブでもウェブアプリケーションでも同様にマニフェストファイルを書いて kubectl
コマンド実行のみです。
マニフェストファイルに書くオプションを上手く利用することで、ブルーグリーンデプロイやローリングデプロイもすることも可能です。
それ以外に、実行中のログを tail してみることが出来る `kubectl logs -f`
や、アプリケーションに入りたい場合に `kubectl exec`
があります。
この仕組みにより、オペレーションの差異を吸収することができます。
クラスタリング
CoreOS cluster optimized for development and testing
Kubernetes はいくつかのサーバーを組み合わせて、一つのクラスタリングを構築します。そのため、アプリケーションを実行する際にサーバー先を意識する必要はありません。
Rails のデプロイツールとして、よく使われる Capistrano と比較して考えてみます。(fabric なども一緒です)
Capistrnoで新しくサービスを追加する場合、 `config/deploy/`
の下で定義されている stage を追加し、サーバー先を設定する必要があります。
しかし、Kubernetes の場合は、そもそも必要ありません。
kubectl
を実行すると Kubernetes の API Server に指示が飛び、クラスタ内部でよしなに実行してくれるからです。
統一化したオペレーションになるとアプリケーションエンジニアはどう嬉しい?
新しくサービスを作りたい時に、インフラチームに頼んでサーバーを用意してもらったり、Capistrano に stage を追加したり、リリース方法を細かく指定するために既存の Capistrano のソースに手をいれる必要はありません。
アプリケーションエンジニアは、
- Dockerfile を書く
- `
kubernetes/`
直下にマニフェストファイルを書く `kubectl create -f ./your_manifest.yaml`
でアプリケーションをリリース
大きくこの3つだけを行うことで作ったものを簡単に出すことが出来きます。いわば Heroku のような手軽にリリースすることが出来ます。
※話が脱線しますが、 Heroku でいいじゃんと思った方に、少し補足すると Heroku との違いとして以下のような利点があります。
- 欲しいスペックを定義できる
- リリースしてからトラフィックを受付けるタイミング等を決められる
- postStart で事前準備みたいな事ができる
- preStop で事後処理みたいな事ができる
- PodはあくまでもDeployの最小単位であり、「静的な部分を nginx コンテナで返し、動的な部分をアプリケーションコンテナで返す」というセットを作ることができる
- ジョブ等もマニフェストファイルに書くだけでAddonの追加いらず利用できる。
- (社内で Kubernetes を構築した場合は)社内の内部API等のリソースが利用出来る。
- (クラスタリングの使い方次第で)監視やログ等の設定を気にせず、同じDashboradでそのまま利用できる。 ref. Dashboard
ただ、意外にも社内で Kubernetes を使ってみて、
一番反響が大きかったのは システム管理者にいちいち依頼せずに利用できる ことでした。
アプリケーション固有の方言をなくす
Docker とあるルールを決めることで実現します。
Docker を使ってアプリケーションをコンテナに内包する
コンテナは、使用されるライブラリ等や言語などを含めて一つのプロセスに内包します。
そのため、中身が Ruby Go Python Java etc.. と何かしらの言語で作られてたとしても、コンテナの利用者はあくまでも `docker run`
で実行できます。
また、ホストのサーバーや他のアプリケーションとの依存関係をある程度排除するため、自分たちが欲しい環境を手に入れることが出来ます。
詳しくは、Docker を Production で使い続ける理由 で書いたとおりです。
ルールを決める
新しくリポジトリを立ててサービスを作る際や、大きく既存のアーキテクチャを変更したい場合に、チーム連携するのは大変です。またドキュメントを書いたとしても相手に読ませることを強制するのはあまりユーザビリティが良くありません。
(アプリケーションエンジニアが一番モチベーション高い git clone
後で、セット・アップが難しいとびっくりするくらいやる気が無くなります。)
また、server を起動する仕組みを変えてしまうと、オペレーションも変更しないといけないですし、また必要であれば Dockerfile も変更しなければいけません。
そうすると、アーキテクチャ変更 + オペレーション変更 の両方同期をとってマージする必要があり、事故のもとです。
細かい話ではありますがこういったちょっとした手間や事故が変化を鈍くさせます。
そういったものを防ぐために、ルールを決めておくだけでだいぶスムーズになります。以下例です。
- ウェブアプリケーションのヘルスチェックやステータスのURLは統一にする (/healthcheck, /appstatus, /appinfo, /ping)
script/hogehoge
で同じ方法で実行 script/- インストール script/bootstrap
- ビルドステップ script/ci-build
- デプロイステップ script/ci-deploy
- サーバー起動 script/server
- コンソール(one-off container) script/console
- etc..
このように初めてくるアプリケーションエンジニアも `script/hogehoge`
を基本的に確認するだけで済みます。
また README もかなりシンプルに保つことが出来ます。
Getting started
---------------
$ script/bootstrap
Development
-----------
### Run
$ script/server
### Testing
$ script/test
Production
-----------
### Console
$ script/console
今回 Go のアプリケーションを書く際に、 Go でツール書くときの Makefile 晒す を参考にして Makefile を書いてますが、基本的には `script/bootstrap`
と `script/server` を呼び出すだけでセット・アップからローカルで実行できる状態です。
- script/bootstap の例
#!/usr/bin/env bash
set -eu
set -o pipefail
echo "==> Installing glide"
go get -u github.com/Masterminds/glide
echo "==> make install"
make install
echo "==> make"
make
- script/server の例
#!/usr/bin/env bash
set -eu
set -o pipefail
echo "==> Running Server"
make run
CI/CD
Travis CI を使いました。
- setup
ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/.travis.yml#L14-L27
利用する kubectl のバージョン、kubectl 実行の際に必要なパラメータ、DockerHub のレポジトリ先、アカウント情報を設定します。
$ travis enable
$ travis init
$ travis encrypt DOCKER_EMAIL=email
$ travis encrypt DOCKER_USERNAME=name
$ travis encrypt DOCKER_PASSWORD=password
$ travis encrypt K8S_SERVER=https://your-server
$ travis encrypt K8S_TOKEN=your-token
- kubectl のダウンロード
ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/.travis.yml#L29-L31
install:
- curl -o ./kubectl "https://storage.googleapis.com/kubernetes-release/release/${K8S_VERSION}/bin/linux/amd64/kubectl"
- chmod +x ./kubectl
- test -> build -> push
ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/.travis.yml#L33-L34
script:
- script/ci-build
ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/script/ci-build
- make install => セット・アップ
- make test => テストを実行
- make => go build で binary file 生成
- docker build => binary file をコンテナにあげてビルドする
- docker push => DockerHub に push する
- master ブランチであれば、tag を latest に変更して push
#!/usr/bin/env bash
set -eu
set -o pipefail
docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
echo "make install"
make install
echo "Testing..."
make test
echo "Building Binary..."
GOOS=linux GOARCH=amd64 make
echo "docker build -t ${REPO}:${TAG}"
docker build -t $REPO:$TAG .
echo "docker push ${REPO}:${TAG}"
docker push $REPO:$TAG
if [ "$TRAVIS_BRANCH" == "master" ]; then
echo "docker tag ${REPO}:${TAG} ${REPO}:latest"
docker tag $REPO:$TAG $REPO:latest
echo "docker push ${REPO}:latest"
docker push $REPO:latest
fi
全ての push に対して、コンテナに tag を付けて push しています。
その tag には git の hash 値を入れているため、どの pull request の作業で、どの commit かを特定することが出来ます。
from. https://hub.docker.com/r/koudaiii/demo/tags/
- Deploy
ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/.travis.yml#L36-L41
deploy:
skip_cleanup: true
provider: script
script: script/ci-deploy
on:
all_branches: true
すべてのブランチに対して deploy するようにしました。
ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/blob/master/script/ci-deploy
#!/usr/bin/env bash
set -eu
set -o pipefail
if [ "$TRAVIS_BRANCH" == "master" ]; then
echo "Deploy ${REPO}:${TAG} in production"
cat kubernetes/demo.yaml | sed "s,latest,${TAG},g;" | ./kubectl replace -f - --namespace=demo --server=${K8S_SERVER} --token=${K8S_TOKEN} --insecure-skip-tls-verify=true
else
echo "Deploy ${REPO}:${TAG} in qa"
cat kubernetes/demo.yaml | sed "s,latest,${TAG},g;" | ./kubectl replace -f - --namespace=demo-qa --server=${K8S_SERVER} --token=${K8S_TOKEN} --insecure-skip-tls-verify=true
fi
master 以外のブランチの場合は、demo-qa
という場所で deploy するようにしています。
QA環境は、最新の git push された状態になるようにしています。2−3名で開発する分には十分ワークします。
大規模開発になった場合は、ここからさらにブランチ毎に namespace を用意してあげたり、 push したユーザー毎に namespace を作るなど工夫できます。
ローカル上で確認
実際に本番で動いているコンテナは、ローカルに落として確認することが出来ます。
`docker pull koudaiii/demo`
で Download
$ docker pull koudaiii/demo:master-fc598063
master-fc598063: Pulling from koudaiii/demo
3690ec4760f9: Already exists
44c4e991afb3: Pull complete
b3da68369b1a: Pull complete
Digest: sha256:9e82067385bf5bcbfc5f4d2a18eedb22b27db492dfd75cad227b91ce32cb9412
Status: Downloaded newer image for koudaiii/demo:master-fc598063
`docker run -p :8080 koudaiii/demo`
で実行
$ docker run -p :8080 koudaiii/demo:master-fc598063
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] GET /healthcheck --> main.main.func2 (3 handlers)
[GIN-debug] GET /appstatus --> main.main.func3 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2016/12/14 - 06:58:20 | 200 | 21.076µs | 172.17.0.1 | GET /ping```
`docker ps`
で Port を確認
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9da6fe5ab55b koudaiii/demo:master-fc598063 "/demo" 5 seconds ago Up 3 seconds 0.0.0.0:32768->8080/tcp ecstatic_archimedes
- ブラウザで確認
$ open http://localhost:32768/ping
Kubernetes のマニフェストの解説
Namespace
クラスタ内に一つの名前空間を作ります。
repository 毎に namespace を最低限切るようにすると使いやすいです。
また production と qa で分けるとさらに事故が減り安心できます。
- kubernetes/demo-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demo
- kubernetes/demo-qa-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demo-qa
$ kubectl create -f kubernetes/demo-ns.yaml -f kubernetes/demo-qa-ns.yaml
namespace "demo" created
namespace "demo-qa" created
Service
外からのトラフィックを定義します。
apiVersion: v1
kind: Service
metadata:
name: demo
labels:
name: demo
role: web
spec:
ports:
- port: 80 # Service opened port.
protocol: TCP
targetPort: 8080 # Endpoint of containerPort
selector:
name: demo
role: web
type: LoadBalancer
namespace を指定することで、それぞれの名前空間で構築できます。
$ kubectl create -f kubernetes/demo-svc.yaml --namespace=demo
service "demo" created
$ kubectl create -f kubernetes/demo-svc.yaml --namespace=demo-qa
service "demo" created
Deployment
実際にアプリケーションをどう動かすか宣言します。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: demo
labels:
name: demo
role: web
spec:
minReadySeconds: 30
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 100% # maximum number of Pods that can be created above the desired number of Pods
maxUnavailable: 0 # maximum number of Pods that can be unavailable during the update process.
replicas: 1 # template 以下を何個用意するか
template:
metadata:
name: demo
labels:
name: demo
role: web
spec:
containers:
- image: koudaiii/demo:latest # cf. https://hub.docker.com/r/koudaiii/demo
name: demo
ports:
- containerPort: 8080 # Deployment containerPort (Opened Port)
readinessProbe: # traffic を受けるタイミングを指定
httpGet:
path: appstatus # /appstatus で確認
port: 8080
initialDelaySeconds: 5 # 5s後にチェックする
timeoutSeconds: 10 # 10s 以上は timeout
failureThreshold: 20 # 失敗回数
livenessProbe: # healthcheck 用
httpGet:
path: ping # /ping で確認
port: 8080
initialDelaySeconds: 5 # 5s後にチェックする
timeoutSeconds: 10 # 10s 以上は timeout
failureThreshold: 20 # 失敗回数
$ kubectl create -f kubernetes/demo.yaml --namespace=demo-qa
deployment "demo" created
$ kubectl create -f kubernetes/demo.yaml --namespace=demo
deployment "demo" created
Blue-Green Deploy にしたい場合
- ref. http://qiita.com/koudaiii/items/d0b3b0b78dc44d97232a#blue-green-deployent
- ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/pull/15
以前ハンズオンで利用したものをがありますので、こちらを参考にして頂ければと思います。例として、マニフェストファイル と 簡単な color switch するスクリプト を書きました。
コンテナの入れ替え方法は下で行う Rolling Deploy と同じです。
あとは switch をどのようにして行うかについて、ローカル上で実行できる簡単なスクリプトを書きました。
- Switch Color Script
#!/usr/bin/env bash
set -eu
set -o pipefail
echo "==> Getting current color"
CURRENT_COLOR=$(kubectl get svc demo-bg --namespace=demo --template='{{.spec.selector.color}}')
if [ "$CURRENT_COLOR" == "blue" ]; then
NEXT_COLOR=green
else
NEXT_COLOR=blue
fi
echo "Switch from $CURRENT_COLOR to $NEXT_COLOR"
cat kubernetes/bg-deployment/demo-svc.yaml | sed "s,switch,$NEXT_COLOR,g" | kubectl apply -f - --namespace=demo
Rolling Deploy
ref. http://kubernetes.io/docs/user-guide/deployments/
- Max Unavailable 更新処理中に使用できないPodの最大数(default 1)
- Max Surge 作成できるPodの最大数(default 1)
この2つのパラメータを指定することで、どのくらいのペースで Pod を入れ替えるかを指定することができます。30%ずつ変えるといったことも可能です。
この場合、`replicas: 1
` になっているため、この初期設定のままにしてしまうと `Unavailable` の初期値が `1` のため、Pod が Delete -> Create してしまいダウンタイムが発生するので気をつけてください。
- まとめて入れ替える方法 ref. https://github.com/koudaiii/jjug-ccc2016fall-devops-demo/pull/16
- 新旧のコードが混ざッてはいけない場合
- オンメモリ−でデータを固定したい場合(振り分けられる先でレスポンスの内容が変わってしまうのを避ける)
- まとめて入れ替えすることが出来ます
- maxSurge: 100% => replicas の分一気に作る
- maxUnavailable: 0 => 使えない数を0にする
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 100% # maximum number of Pods that can be created above the desired number of Pods
maxUnavailable: 0 # maximum number of Pods that can be unavailable during the update process.
実際にまとめて変わる部分のデモになります。
$ kubectl replace -f kubernetes/demo.yaml --namespace=demo
deployment "demo" replaced
$ kubectl get po -w --namespace=demo
NAME READY STATUS RESTARTS AGE
demo-2332281002-bqgin 1/1 Running 0 4m
demo-3187983409-ztco3 1/1 Running 0 9s
NAME READY STATUS RESTARTS AGE
demo-2332281002-bqgin 1/1 Terminating 0 4m
demo-2332281002-bqgin 0/1 Terminating 0 4m
demo-2332281002-bqgin 0/1 Terminating 0 4m
demo-2332281002-bqgin 0/1 Terminating 0 4m
確実に次のリリースするものが揃ってから古い方を消すようになっています。
QAで確認してProductionにリリースする
QA デプロイ
- 通ったら、実際にブラウザで確認
$ kubectl get deployment -o yaml --namespace=demo-qa
・・・・
LoadBalancer Ingress: your.ap-northeast-1.elb.amazonaws.com
- ブラウザで見る
(もうちょっと凝ったのを作れば良かった)
- 問題無ければそのままマージ
マージすると TravisCI から Production 環境へ Deploy します
まとめ
今回サンプルとして Go を利用しましたが基本的にどの言語でも同じことが言えます。
マイクロサービスを行う上で、こういったルールを作ることでバラバラになりにくくなり、統制を保つことができます。
またルールが適用されている事例が社内で増えてくるとそのまま参考にしやすくなるため横展開がさらにしやすくなります。
- オペレーションの統一化
- アプリケーション固有の方言をなくす
この他にもちょっとした仕組みで効率化出来る部分はたくさんあると思います。意見交換できればと思います。