This page is intended for users in Japan. Go to the page for users in United States.

Docker と Kubernetes を使って『変化に強いインフラ』を作る

WHY

『変化に強いインフラ』を作ることで、技術にこだわり続ける環境ができ、ビジネスの変化にいち早くキャッチアップできます。

そのためにどのようにして、『変化に強いインフラ』を作ることが出来るのか模索したものをまとめます。

WHAT

  • Kubernetes 上にアプリケーションを載せる
  • CI/CD 環境構築
  • GitHub Flow の開発スタイルでを元に QA で自分で書いたコードが確認でき、マージをしてmasterへpushしたら、Produciton へすぐにデプロイする
  • サーバースペックを簡単に変えれる/内部で使われるライブラリ等も変更しやすいようにする
  • Deploy の仕組みを自由に変更できる

ソースコードは以下です。

ref. GitHub Flow

『変化に強いインフラ』を作っていく上での定義とルール

変化に強いインフラは、少なくとも以下の2つが実行出来ている状態と定義します。

  • 継続的にリリース出来ること
  • (利用する言語を含め)サービスの構成を自由に変更出来ること

そのために2つのポイントを抑える必要があると考えました。

  • オペレーションを統一化
  • アプリケーション固有の方言をなくす

※具体的に今回はこの部分をサンプル実装しています。

ref. koudaiii/jjug-ccc2016fall-devops-demo

オペレーションを統一化

アプリケーション作成と同時に、必ずオペレーションが存在します。

  • リリースまでのオペレーションを統一化
  • 保守作業によるオペレーションを統一化

リリースまでのオペレーションを統一化

サンプルのアプリケーションを元にリリースまでのオペレーションを以下のようにしました。

  1. ブランチを切り、プルリクエストを送る
  2. git push する度にテストが実行される
  3. テストが通れば QA に deploy され、ブラウザで確認する
  4. リリース出来るタイミングになったら master にマージする
  5. CI 上でテストが走り、テストが通れば Production にリリースする

実際のフローとなっているコードと実際にリリースするときの画面です。

このように型を決めることで、どんどん継続的にリリースすることが可能になります。

保守作業によるオペレーションを統一化

アプリケーションの特徴を考えてみます。

  • バッチのような一回限りのジョブ
  • 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 のソースに手をいれる必要はありません。

アプリケーションエンジニアは、

  1. Dockerfile を書く
  2. `kubernetes/` 直下にマニフェストファイルを書く
  3. `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 も変更しなければいけません。

そうすると、アーキテクチャ変更 + オペレーション変更 の両方同期をとってマージする必要があり、事故のもとです。

細かい話ではありますがこういったちょっとした手間や事故が変化を鈍くさせます。

そういったものを防ぐために、ルールを決めておくだけでだいぶスムーズになります。以下例です。

このように初めてくるアプリケーションエンジニアも `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

  1. make install => セット・アップ
  2. make test => テストを実行
  3. make => go build で binary file 生成
  4. docker build => binary file をコンテナにあげてビルドする
  5. docker push => DockerHub に push する
  6. 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 にしたい場合


以前ハンズオンで利用したものをがありますので、こちらを参考にして頂ければと思います。例として、マニフェストファイル 簡単な color switch するスクリプト を書きました。

コンテナの入れ替え方法は下で行う Rolling Deploy と同じです。
あとは switch をどのようにして行うかについて、ローカル上で実行できる簡単なスクリプトを書きました。

  • Switch Color Script

script/switch

#!/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 デプロイ


build log

  • 通ったら、実際にブラウザで確認
$ kubectl get deployment -o yaml --namespace=demo-qa
・・・・
LoadBalancer Ingress: your.ap-northeast-1.elb.amazonaws.com
  • ブラウザで見る

(もうちょっと凝ったのを作れば良かった)

  • 問題無ければそのままマージ

マージすると TravisCI から Production 環境へ Deploy します

まとめ

今回サンプルとして Go を利用しましたが基本的にどの言語でも同じことが言えます。
マイクロサービスを行う上で、こういったルールを作ることでバラバラになりにくくなり、統制を保つことができます。

またルールが適用されている事例が社内で増えてくるとそのまま参考にしやすくなるため横展開がさらにしやすくなります。

  • オペレーションの統一化
  • アプリケーション固有の方言をなくす

この他にもちょっとした仕組みで効率化出来る部分はたくさんあると思います。意見交換できればと思います。

Wantedly, Inc.では一緒に働く仲間を募集しています
Anonymous
B6d777a9 1069 4926 a7e8 650fa528d6e6
C295017e a900 4f0e 872a 216348c31683
B9f90031 4063 41df 814c 4b76fcda9553?1500991326
D9ce653d cba0 4e3d b5b7 abad2535e3db
Anonymous
55 いいね!
Anonymous
B6d777a9 1069 4926 a7e8 650fa528d6e6
C295017e a900 4f0e 872a 216348c31683
B9f90031 4063 41df 814c 4b76fcda9553?1500991326
D9ce653d cba0 4e3d b5b7 abad2535e3db
Anonymous
55 いいね!

今週のランキング

ランキングをみる

Page top icon