はじめに
本連載は、WANTEDLY TECH BOOK 2から抜粋し、再編集したものになります。第1回に引き続き、第2回もインフラチームの坂部(@koudaiii)が担当します。前回は「変化に強いインフラはWantedlyにとってなぜ必要か」「これまでどのように変化に強いインフラに取り組んできたのか」といったことを中心に紹介しました。今回は、架空のサービスをサンプルとして、Kubernetesの活用方法を具体的に紹介します。
実現すること
- Ruby on RailsのサイトをKubernetes上に構築する
-
CI/CD環境を構築する
- GitHub Flowの開発スタイルを元に、自分が書いたコードをQA環境で確認できる
- masterにマージしたら、Production環境へデプロイする
- サーバースペックを簡単に変えられる
- ライブラリを簡単に変えられる
- ローカルPC上でMinikubeを使って確認する
なお、ソースコードは本記事からダウンロードできるサンプルファイルの他、koudaiii/myapp(GitHub)にも置いています。
定義とルール
変化に強いインフラは、少なくとも以下の2つが実行できている状態と定義します。
- 継続的にリリースできること
- サービスの構成を自由に変更できること
そのために、2つのポイントを抑える必要があると考えました。
- オペレーションの統一化
- アプリケーション固有の方言をなくす
これらの実現方法について解説していきます。
オペレーションの統一化について
どんなアプリケーションを作る際にも必ずオペレーションが発生します。エンジニアがなるべくコードに集中できるように、オペレーション部分は統一していく必要があります。
具体的には、以下の2点のオペレーションを統一します。
- リリースまでのオペレーションを統一化
- 保守作業によるオペレーションを統一化
リリースまでのオペレーションを統一化(1)
サンプルのアプリケーションを元に、リリースまでのオペレーションを以下の通り定めました。
- masterブランチに直接コミット、プッシュを行わない。
- ブランチを切ってそこで開発を実施し、Pull Requestを送る。
-
git push
する度にテストが行われ、問題があればすぐに把握できる。 - テストが通ればQA環境にリリースされ、ブラウザで確認する。
- リリースできるタイミングになったらmasterにマージする。
- CI上でテストが走り、テストが通ればProductionにリリースする。
WANTEDLY TECK BOOK 2ではGoを用いて紹介いたしましたが、今回はReact with Webpacker and Ruby on Rails(以下Railsと表記)の構成で進めます。
実際のフローとなっているコードと実際にリリースする流れです。
deploy: skip_cleanup: true provider: script script: "./script/ci-deploy" on: all_branches: true
#!/usr/bin/env bash set -eu set -o pipefail if [ "$TRAVIS_BRANCH" == "master" ]; then echo "Deploy ${REPO}:${TAG_COMMIT} in production" ./kops export kubecfg --name $K8S_PROD_CLUSTER else echo "Deploy ${REPO}:${TAG_COMMIT} in qa" ./kops export kubecfg --name $K8S_QA_CLUSTER fi ./kubectl set image deployment/${DEPLOYMENT_NAME} rails=${REPO}:${TAG_COMMIT} --namespace=${NAMESPACE} --record
Travis CIのビルドログはこちらから確認できます。
リリースの流れを図にしました。順を追って紹介します。
CI/CDのセットアップ
CI/CD環境として、今回はTravis CIを利用しています。
$ travis enable $ travis init
次に環境変数の設定を行います。秘密の情報をリポジトリに保存させないように、travis encrypt
で登録します。
$ travis encrypt DOCKER_USERNAME=xxx # docker login するための情報 $ travis encrypt DOCKER_PASSWORD=xxx # docker login するための情報
実際の設定は以下の通りです。
- REPO=quay.io/koudaiii/myapp # docker registry の Path - DEPLOYMENT_NAME=${REPO##*/} # Path から myapp を抽出 - NAMESPACE=$DEPLOYMENT_NAME # リポジトリとNAMESPACEを同じにする - KUBECTL_VERSION=1.6.6 # kubectl の利用するバージョン - KOPS_VERSION=1.6.2 # kops の利用するバージョン(k8s on AWS で利用) - K8S_PROD_CLUSTER=prod.cluster.example.com # Production 環境のクラスタ名 - K8S_QA_CLUSTER=qa.cluster.example.com # QA 環境のクラスタ名 - TAG_COMMIT=$TRAVIS_COMMIT # git の commit hash 値 - TAG_COMMIT_SHORT=${TRAVIS_COMMIT::7} # git の commit hash 値の先頭7文字 - TAG_BRANCH=$(echo $TRAVIS_BRANCH | sed -e 's/[^a-zA-Z0-9-]/_/g') # koudaiii/test => koudaiii_test という普通に dockerのimage tagとして利用できるように置換 # travis encrypt DOCKER_USERNAME=xxx # docker login するための情報 - secure: "xxxxxxxxxxxxxxxxxxxxxxxxxx" # travis encrypt DOCKER_PASSWORD=xxx # docker login するための情報 - secure: "xxxxxxxxxxxxxxxxxxxxxxxxxx" - YARN_VERSION=1.0.1 # 利用する yarn のバージョン
before_install:とbefore_script:でアプリケーションのテストを実行するためのセットアップを行っています。
before_install: - npm install -g yarn@$YARN_VERSION before_script: - bundle exec rails db:create RAILS_ENV=test
script:でテストを行い、基本的にテストが通ったらDockerのコンテナイメージを作成し、そのコンテナイメージを保存するためのレジストリ先として、Quayに保存するようにします。
script: - ./script/ci-test # アプリケーションのテスト - ./script/ci-assets-build # 静的ファイルの生成 - ./script/ci-build # コンテナイメージ作成 - ./script/ci-push # コンテナイメージをQuayに保存
before_deploy:はデプロイするためのツールのインストールを行っています。
before_deploy: - curl -SL -o ./kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl" - chmod +x ./kubectl - curl -SL -o ./kops "https://github.com/kubernetes/kops/releases/download/${KOPS_VERSION}/kops-linux-amd64" - chmod +x ./kops
deploy:で実際にリリースします。
deploy: skip_cleanup: true provider: script script: "./script/ci-deploy" on: all_branches: true
- script/ci-test⇒アプリケーションのテスト
- script/ci-assets-build⇒静的ファイルの生成
- script/ci-build⇒コンテナイメージ作成
- script/ci-push⇒コンテナイメージをQuayに保存
- script/ci-deploy⇒kubectlでリリース作業を行う
Railsのテストをここで行います。今回はRails標準のテストフレームワークを利用しました。
#!/usr/bin/env bash set -eu set -o pipefail cd "$(dirname $0)/.." echo "test" bundle exec rails test
静的ファイルの生成をCI上で行っています。Amazon S3配信の場合は、静的ファイルを生成時にアップロードしますが、今回は自前でnginxを使って静的ファイルを配信します。そのため静的ファイルの生成後、Railsコンテナに含めてイメージを作成します。コンテナイメージの作成は、次のステップであるscript/ci-buildで行います。
#!/usr/bin/env bash set -eu set -o pipefail if [ $TRAVIS_PULL_REQUEST != "false" ]; then echo "Skip build because this build is a pull request." exit 0 fi cd "$(dirname $0)/.." echo "build assets" RAILS_ENV=production bundle exec rails assets:precompile
すべてのgit push
に対して、コンテナにタグを付けてビルドしています。そのタグにはgitのハッシュ値」を入れているため、どのPull Requestの作業で、どのコミットかを特定することができます。
また、latestタグやブランチ名タグなども合わせて作成し、その都度上書きをしています。これは、そのブランチ名を指定したタグが常に最新になることを担保しています。
#!/usr/bin/env bash set -eu set -o pipefail if [ $TRAVIS_PULL_REQUEST != "false" ]; then echo "Skip build because this build is a pull request." exit 0 fi cd "$(dirname $0)/.." docker build -t $REPO:$TAG_COMMIT . docker tag $REPO:$TAG_COMMIT $REPO:$TAG_COMMIT_SHORT docker tag $REPO:$TAG_COMMIT $REPO:$TAG_BRANCH if [ $TRAVIS_BRANCH = "master" ]; then docker tag $REPO:$TAG_COMMIT $REPO:latest fi
保存先のレジストリサービス(ここではQuay)にログインし、ビルドしたコンテナのイメージをpush
します。
#!/usr/bin/env bash set -eu set -o pipefail if [ $TRAVIS_PULL_REQUEST != "false" ]; then echo "Skip build because this build is a pull request." exit 0 fi cd "$(dirname $0)/.." echo "docker login" docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" quay.io docker push $REPO:$TAG_COMMIT # git commit hash値 docker push $REPO:$TAG_COMMIT_SHORT # git commit hash値の先頭7文字 docker push $REPO:$TAG_BRANCH # gitのブランチ名 if [ $TRAVIS_BRANCH = "master" ]; then docker push $REPO:latest # gitのブランチ名がmasterだった場合は、latestにpush fi
masterの場合は、Productionのクラスタに対してリリースします。master以外のブランチの場合は、QAのクラスタに対してリリースするようにしています。リリース方法は、kubectl set image
のコマンドを使い、ローリングデプロイで反映されます。
#!/usr/bin/env bash set -eu set -o pipefail if [ "$TRAVIS_BRANCH" == "master" ]; then echo "Deploy ${REPO}:${TAG_COMMIT} in production" ./kops export kubecfg --name $K8S_PROD_CLUSTER else echo "Deploy ${REPO}:${TAG_COMMIT} in qa" ./kops export kubecfg --name $K8S_QA_CLUSTER fi ./kubectl set image deployment/${DEPLOYMENT_NAME} rails=${REPO}:${TAG_COMMIT} --namespace=${NAMESPACE} --record