Kekeの日記

エンジニア、読書なんでも

CircleCIでSpinnakerのデプロイメントパイプラインをデプロイする

f:id:bobchan1915:20181206211505p:plain

どうも、もうすぐ大学卒業の@timtimtim1026です。

もしコメントなどがございましたらDMなり、リプライしてください。

本記事はCircleCI Advent Calendarの14日目の記事になっています。

本記事

この記事では、CircleCIを使ってSpinnakerのデプロイメントパイプラインをデプロイする方法を解説します。

twitter.com

今回の目的としては、CircleCIを使いながら継続的デリバリー(以下、CD)を構築する一例を知ってもらうことです。また、そのなかで紹介するCircleCIの知識が何かしら役に立てられればいいなと思います

考え方としては、今回使うCircleCIやSpinnakerに限らずあらゆるCDにも、CIにも言えることなのでSpinnakerだけに限った話ではありません。

実は、以前(2018年4月ごろ)にSpinnakerを用いて宣言的継続的デリバリーというものを提案しました。

継続的デリバリーのためのデプロイメントパイプラインも宣言的に、かつ、イミュータブルにしよう

というアイデアでした。

www.1915keke.com

この記事はデプロイメントパイプラインをCircleCIを使ってデプロイするための記事なので注意してください。

例えばコードの更新からデプロイまで以下のような流れがあるフローだと本記事では赤枠の箇所に対応しています。

f:id:bobchan1915:20181206204226p:plain

コンテンツ

本記事では以下のセクションで構成されています。


Spinnakerとは

Spinnakerとは

オープンソースな高速に安全に継続的デリバリーをするプラットフォーム

です。公式サイトは以下のサイトにあるのでご覧ください。

www.spinnaker.io

Spinnakerには大きな概念として、PipelineとPipeline Templateというものがあります。

Pipeline Templateという再利用可能なモジュールのようなものに、設定値を渡すとPipelineを生成することができます(もちろんいきなりPipelineを作ることもできます)。

f:id:bobchan1915:20181210210706p:plain

Pipeline Templateによって生成されたPipelineはImmutableであり、宣言的なデプロイメントパイプラインを作ることができます。

f:id:bobchan1915:20181210211000p:plain

さらにCircleCIを使うことによって自動化をすることができます

GitOpsのような考えに近いのですが、設定ファイルをリポジトリに書いて、CIによって適宜Pipelineをデプロイします。

f:id:bobchan1915:20181210211234p:plain

ブランチごとにfilterしたりできますが、宣言的に継続的にデプロイメントパイプラインを配置して、それに沿ってデプロイすることができるわけです。

デプロイメントパイプラインとは

少し深くデプロイメントパイプラインに注目します。

デプロイメントパイプラインとは要求された環境へ移行する一連の過程のことです。

例えば

  • v1.0からv1.1にDocker RegistryにあるアプリケーションのDocker Imageを更新
  • Docker RegistryからトリガーをうけたCDがデプロイメントパイプラインを実装する
    • カナリアリリース
    • Slack通知
    • 待機
    • 古いバージョンのアプリケーションへトラフィックを制限する  - 再度、Slack通知

などデプロイといってもいろんな過程があります。

Spinnakerでは宣言的に書いて、容易にカナリアデプロイやBlue/Greenデプロイ、Red/Blackデプロイをすることができます。

継続的デリバリーの観点だと、デプロイまで携わってないチームメンバーがいるなら継続的デリバリーはできていないです。

すべての更新がすぐさまデリバリーする必要があります。

以下の画像は実際にSpinnakerで定義してみたパイプラインです。

https://qiita-image-store.s3.amazonaws.com/0/153320/be80fb22-d497-de97-916a-651cb422abfd.png

このデプロイメントパイプラインでは

  1. Configuration: Google Cloud RegistryへのPushでトリガー
  2. Deploy Canaryでカナリアをデプロイ、Webhookで「カナリアがデプロイされる」ことをSlackに通知
  3. カナリアが「デプロイされたこと」をSlackに通知
  4. 古いサーバーを削除して、「カナリアデプロイが成功したこと」をSlackに通知

で構成されています。

仮にカナリアで何かしらのバグがあって、システムが落ちるようなことがあっても、トラフィックを切って元の安全なバージョンを使い続けることができ、逆にうまくデプロイできたら古いバージョンを削除して、安全にバージョンアップをすることができます。

またSpinnakerはPipeline Templateのテンプレート機能を用いるとPipeline自体がImmutableになるので安全で運用がしやすいです。


SpinnakerのCLIツール

CIからデプロイをするので今回はroerというSpinnaker公式CLIツールを使います。

実はCLIツールには二つ種類があって

  • roer
  • spin

があります。

もちろんGUIからでもデプロイメントパイプラインを定義することはできます。

このCLIツールを少し紹介してみます。

Roer

github.com

現在はメンテナンスモードで開発は行われていません

安定性を求めるならば、このCLIツールを選定してもいいかもしれません。

サブコマンドもpipeline、pipeline-templateなど、やりたいことと機能が一致しており、使いやすい点もポイントかなと思います。

この記事ではroerを使います。

Spin

次世代のCLIツールはspinというものがあるのですが、

This is under active development, and not yet intended for production use.

と書いてあり、まだプロダクション環境で使うのは控えた方がいいでしょう。

CLIツールといえどSpinnaker自体の機能はroerを使っても変わらないため、サポートの状況を見る必要があります。

github.com

しかしながら、こちらをフィードしておくといいかもしれません。


roerからデプロイする

ここからはValueを渡して、実際にSpinnakerのデプロイメントパイプラインを構築するための方法を解説します。

ローカル環境から試す

まず、最初にCIではなくてローカル環境からデプロイする方法を解説します。CIで実践する前にローカルで実行できると、あとからわかりやすくなると思います。

f:id:bobchan1915:20181210214404p:plain

roerでは以下のようなYAMLファイルを書きます。

今回は.spinnaker/config.ymlに保存していますがファイル名と保存先は任意です。

あらかじめSpinnakerのインスタンスなりVMからポート転送してlocalhost:8084でアクセスできるものとします。

schema: "1"
pipeline:
  application: testappfromgui
  name: Atomic Canary Deployment
  template:
    source: spinnaker://atomicCanaryDeployWebhookTrigger
  variables:
    replicas: 5
    loadbalancer: nginx-keke
    slackWebhookURL: https://hooks.slack.com/services/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

.pipeline.template.sourceであらかじめ用意したatomicCanaryDeployWebhookTriggerというPipeline Templateを用意します。

URLスキーマがspinnakerなのでSpinnaker本体のストレージにあるPipeline Templateを参照しています。

深くはSpinnakerの機能なので解説しません。

また、ローカル環境(Spinnakerへポート転送しているとき)ではroerのpipeline saveコマンドを使って以下のようにデプロイすることができます。

env SPINNAKER_API=http://localhost:8084 go run cmd/roer/main.go pipeline save /go/src/github.com/KeisukeYamashita/advent-article/.spinnaker/config.yml

環境変数SPINNAKER_APIでリクエスト先を指定することになっていて、設定されていないとエラーになります。

どのようにCircleCIでやるのか

手順としては簡単です。ローカル環境でやらせているものをCircleCIにやらせたらいいのです。

またGithubのPushがトリガーになります。

f:id:bobchan1915:20181210215126p:plain

手順をおって説明します。

1. SpinnakerのAPIサーバーにCircleCIからアクセスをする

f:id:bobchan1915:20181210215844p:plain

まず、sshでCircleCIからGCPにアクセスをするのでサービスアカウントが必要になりますのでGCPのコンソールで作成してください。

f:id:bobchan1915:20181210220126p:plain

サービスアカウント秘密鍵(json)を作成してクリップボードにコピーします。

cat /path/to/service/key/hoge.key | pbcopy

そしてCircleCIのSecretにGCLOUD_SERVICE_ACCOUNTに保存します。

CircleCIではContextsで設定をすることができます。

f:id:bobchan1915:20181206205615p:plain

詳しいドキュメントは以下のリンクで確認してください。

circleci.com

簡単にいうと環境変数などをKey-Valueで渡せるものです。

Contexts名はgcloud-contextと指定します。これがKeyとなります。

ここで追加でzone情報も追加しました。

次にCircleCIのJobとして定義します。アンカーで定義しているので抜粋しました。

アンカー名はgcloud_authです。

  gcloud_auth: &gcloud_auth
    run:
      name: Gcloud auth
      command: |
        echo ${GCLOUD_SERVICE_ACCOUNT} > ${HOME}/gcloud-service-key.json
        gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json
        gcloud config set project --quiet ${GCLOUD_PROJECT_ID}
        ssh-keygen -t rsa -C s02294 -f ${HOME}/.ssh/id_gcloud -N ''
        chmod 400 ${HOME}/.ssh/id_gcloud
        publickey=$(cat ${HOME}/.ssh/id_gcloud.pub)
        gcloud compute instances add-metadata keke-spinnaker --zone=asia-northeast1-a --metadata "^#&&#^ssh-keys=s02294:$publickey"
        ssh -i ${HOME}/.ssh/id_gcloud -o "StrictHostKeyChecking no" s02294@35.200.80.114 -L 9000:localhost:9000  -L 8084:localhost:8084 -fN

簡単に説明すると

  • echoでファイルにサービスアカウントキーを書き出す
  • gcloudをアクティベート
  • sshで接続し、ポート転送をしている

サービスアカウントは最低限の権限だけ渡すようにしてください。

2. デプロイメントパイプラインをデプロイする

f:id:bobchan1915:20181210230602p:plain

以下のようにデプロイします。アンカー部分を取り出したものです。deploy_pipelineとアンカーを名付けました。

deploy_pipeline: &deploy_pipeline
    run:
      name: Deploy pipeline by roer
      command: |
        go get github.com/spinnaker/roer
        cd /go/src/github.com/spinnaker/roer
        env SPINNAKER_API=http://localhost:8084 go run cmd/roer/main.go pipeline save /go/src/github.com/KeisukeYamashita/advent-article/.spinnaker/config.yml

ここでは先ほどのローカル環境の時に説明したPipelineを作るコマンドpipeline saveをしています。

3. Jobを定義、Workflowにまとめる

CircleCIでは一つのタスクのまとまりとしてJobとして定義します。

f:id:bobchan1915:20181210232220p:plain

Jobを以下のように定義しました。deploy_spinnaker_pipelineが一つのJobです。

jobs:
  deploy_spinnaker_pipeline:
    <<: *docker_image_config_with_golang
    steps:
      - checkout
      - *gcloud_auth
      - *deploy_pipeline
      - *kill_gcloud_ssh

ここでは勝手にroerのテストを挟んでいます。Testはroerのtestコマンドを叩いて実行しています。

f:id:bobchan1915:20181210232125p:plain

以下のようにWorkflowにまとめます。

workflows: 
  version: 2
  build_and_test_and_deploy:
    jobs:
      - test
      - deploy_spinnaker_pipeline:
          context: gcloud-context
          requires:
            - test
          filters:
            branches:
              only:
                - master
                - develop

今のところ、簡単ですが流れとしてはこのようになっています。

f:id:bobchan1915:20181211012141p:plain

ここではfiltersによってブランチでフィルタリングをかけています。

またrequiresによって並列ジョブと逐次ジョブを指定することができ、スケジューリングすることができます。順序性がなければ並列化してワークフローを速く終えることができます。

circleci.com

また、build_and_test_and_deploytestが成功したときに走るように設定しています。

4. Container RegistryにPushしたり、、、

Docker環境で運用していると、例えばGCPなんかはContainer RegistryにPushすることを追加してもいいかもしれません。

f:id:bobchan1915:20181211012518p:plain

SpinnakerのProvider(トリガーをもらう先)にContainer Registryを設定することもできるので、トリガーを発せられるようになります。

      - send_webhook_to_slack:
          context: gcloud-context
          requires: 
            - deploy_spinnaker_pipeline
          filters:
            branches:
              only:
                - master
                - develop
      - build_image_and_push_gcr:
          context: gcloud-context
          requires:
            - deploy_spinnaker_pipeline
          filters:
            branches:
              only:
                - master
                - develop

またSlackに通知を送るwebhookも指定します。 SlackのIncoming Webhookを使っていて、URLをさらにContextで渡しています。

f:id:bobchan1915:20181211012728p:plain

用途としては、ワークフローの途中経過をフィードバックしたいからです。

5. 最終的なWorkflow

GithubにデプロイメントパイプラインのリポジトリをPushすると以下のようにWorkflowがはしります。

f:id:bobchan1915:20181206210532p:plain

またbuild_image_and_push_gcrによってデプロイしたパイプラインでのトリガーとなって、Spinnakerによってデプロイされるようになっています。

最終的な流れとしては以下のようになっています。

f:id:bobchan1915:20181211012922p:plain

そのあとの話

Mergeによるデプロイメントパイプラインの更新

デプロイメントパイプラインはブランチごとなど、必要に応じて定義できて、実行できます。

f:id:bobchan1915:20181214021417p:plain

例えば今回だと.spinaker/config.ymlのように設定ファイルでしかないので、ブランチ間の差分を読むことによってデプロイメントパイプラインの差分がわかります。

f:id:bobchan1915:20181214021226p:plain

つまりconfig.ymlファイルがデプロイメントパイプラインを宣言的にすべてを記述しているのでGitOpsということができます。

f:id:bobchan1915:20181214021959p:plain

  • configを読めばデプロイメントパイプラインがわかる
  • configを書けばデプロイメントパイプラインを宣言できる

その仕組みを今回、CircleCIにやってもらったわけです。

f:id:bobchan1915:20181214022258p:plain

アプリケーションが運ばれる流れ

デプロイメントパイプラインとアプリケーションの更新が同時である必要はないですが、今回は説明のために同時に説明します。

例えばエンジニアがアプリケーションに対して差分をPushしたとします。

f:id:bobchan1915:20181214023543p:plain

この差分には

  • デプロイメントパイプラインの更新
  • アプリケーションの更新

があったとします。

次にその差分がMasterブランチにマージされてCIがトリガーされたとします。

f:id:bobchan1915:20181214023757p:plain

ここからが醍醐味なのですが、まずデプロイメントパイプラインの更新をします。

差分が追加されました。

f:id:bobchan1915:20181214024053p:plain

これによってデプロイメントパイプラインは最新の差分を含んでいることになります。

次にGCRに新しいアプリケーションのイメージをPushします。

f:id:bobchan1915:20181214024427p:plain

すると今回だとGCRの更新によってSpinnakerがトリガーされるので、トリガーされてアプリケーションはデプロイメントパイプラインを流れていきます。

f:id:bobchan1915:20181214024857p:plain

するとあとは自らが定義したデプロイメントパイプラインの出力先(例えばKubernetesクラスタ)などにデプロイされるだけです。

この一連の流れをCircleCIとCDツールをもってして実現することができます。

CI/CDの恩恵

例えばインフラもサーバーのこともよくわからない新米デザイナーがいたとしましょう。

f:id:bobchan1915:20181214025600p:plain

HTMLやCSSを軽くいじってPushして、チームリーダーにマージしてもらいました。

f:id:bobchan1915:20181214030317p:plain

CI/CDを敷いているおかげで、何もインフラなどは知らないのにデプロイをする(=開発した成果物を届ける)ことができます。

f:id:bobchan1915:20181214030726p:plain

もちろんSlack通知などの通知によるフィードバックはもちろんのことながら

f:id:bobchan1915:20181214031316p:plain

Spinnaker自体のGUIでもフィードバックを得られます。

f:id:bobchan1915:20181214031539p:plain

ただ知らないというだけでなくて、安全に、チームだれもが、フィードバックを得ながらデプロイしていく、価値をユーザーに届けられるプラットフォームが実現したというわけです。

まとめ

今回はSpinnakerでやりましたが、CDのデプロイメントパイプライン自体をデプロイするためにCIを使う方法を紹介しました。

明日はKubernetes Advent CalendarにArgo CDやGitOpsについて記事をアップします。CDについて興味が出てきた人はぜひ見てみてください。

ありがとうございました。