Wercker を使ってみた感想

Oracle Container Pipelines として取り込まれた CI・CD ツールである Wercker を使ってみたので紹介。

Wercker ってどんなもの?僕が感じた特徴まとめ

Wercker とはどんなものなのか。Jenkins を知っていればイメージはしやすいと思う。

その他特徴をトピックごとに

実行環境がコンテナになる点、Step という概念が難しい。また、Workflow と環境変数が wercker.yml と別れてしまうので、それぞれの関連性が把握しづらい感じがある。下手すると「作った人しか理解できない」Workflow になりそうなので、小さく作ろう。

Pipeline の名称は任意に決められるが、Wercker CLI は builddeploy という名前の Pipeline だけ特別に実行できたりするので、メインはこの2つ、ビルドしてデプロイする、という CD (Continuous Delivery) を意識したツールになっている。ココでいう「デプロイ」は、Docker コンテナイメージを成果物として Docker リポジトリに Push するまでにしたり、もしくは Kubernetes クラスタへのデプロイまでを行わせたりもできる (kubectl コマンドを実行するための Step もある)。

各 Pipeline をまたいだ時、成果物ファイルはどうなるかというと、Workflow 全体でボリュームを共用しているので、「手前の build Pipeline でビルドしたファイルを使って deploy Pipeline を実行する」といった処理が可能。一つの Pipeline で全部をやる必要はない。しかし、npm install のようにモジュールをインストールするような Pipeline の場合、次の Pipeline に移行した時にシンボリックリンクが破損することがあるようなので、Pipeline をまたいで使用するファイルは静的なファイルである方が安全。

あと、Wercker サービス全体としてはユーザ管理が微妙だ。Jenkins の場合、ジョブ定義は全メンバで共用でき、権限さえあれば誰でも編集できる関係だった。しかし Wercker は、Git リポジトリと Wercker アカウントが1対1で紐付いたような関係になってしまっている。

プライベートな Git リポジトリを紐付けたい時、ユーザ名とパスワードをアカウント設定に記述するため、そのユーザがパスワード変更を行うと認証が通らなくなり Pipeline が実行できなくなったりする。

Organization というグループを作る機能があるものの、コレに参加していない人は、wercker.yml は変更できるのに、Workflows は変更できない、という中途半端な状態になる。Jenkinsfile だったらこうはならないのだが…。

いずれにしても、「人」が作ったアカウントに紐付くため、「Jenkins 用アカウント」みたいなシステム利用するアカウントで逃げる方法が取れず、同じ考え方では不便するところが出てくると思う。

自分が作ってみた wercker.yml

感想はココまで。以降は、最初とっつきづらい wercker.yml のイメージを知ってもらうため、自分が作ってみた wercker.yml を紹介する。

以下の例は、

の2つから構成されている。

box:
  id: node:10-stretch

build:
  steps:
    - npm-install
    - script:
        name: lint
        code: npm run lint
    - npm-test
    - script:
        name: build
        code: npm run build

deploy:
  steps:
    - internal/docker-build:
        name: build image
        dockerfile: ./Dockerfile
        image-name: my-container
    - internal/docker-push:
        name: push image
        image-name: my-container
        username: $REGISTRY_USERNAME
        password: $REGISTRY_TOKEN
        repository: iad.ocir.io/my-tenancy/my-ocir/my-project
        tag: v1

各行にコメントを足してみた詳説版。

# Box
# 
# 各 Pipeline の実行環境となる Docker イメージ。DockerHub から自動的に取得してくれる
box:
  # ココでは Node.js v10 系が動作するイメージを選択した
  id: node:10-stretch

# Build
# 
# - Lint・UT を実施する
# - 成果物ファイルをビルドする
build:
  steps:
    # この「npm-install」Step は Wercker 公式が提供する Step
    # - 参考 : https://app.wercker.com/steps/wercker/npm-install
    # - 参考 : https://github.com/wercker/step-npm-install
    - npm-install
    # 「script」Step は、シェルスクリプトで任意のコマンドを実行できる
    # npm コマンドが使える Box を指定しているので、以下のように「npm run」が叩ける
    # package.json の scripts で定義した「lint」という npm-run-scripts を実行して Lint している
    - script:
        name: lint
        code: npm run lint
    # 「npm run test」「npm test」と同義
    - npm-test
    # ソースファイルのビルド。「dist」ディレクトリにでも成果物が吐かれているテイ
    - script:
        name: build
        code: npm run build

# Deploy
# 
# - Docker イメージをビルドする
# - プライベート Docker レジストリに Push する
deploy:
  steps:
    # Docker ビルドを行う
    - internal/docker-build:
        name: build image
        dockerfile: ./Dockerfile
        # 「image-name」は、次の「internal/docker-push」Step で対象イメージを紐付けのための一時的な名前
        image-name: my-container
    # Docker Push を行う
    - internal/docker-push:
        name: push image
        # 前述の「image-name」を指定する
        image-name: my-container
        # 以下の環境変数は管理画面で設定しておく
        username: $REGISTRY_USERNAME
        password: $REGISTRY_TOKEN
        # Docker レジストリの URL を指定する。以下は Oracle Cloud Infrastructure Registry を使うテイ
        repository: iad.ocir.io/my-tenancy/my-ocir/my-project
        # Docker イメージのタグ名
        tag: v1

実装してみたのはこんな感じ。

Docker タグ名を変更したい時のアイデア

上述の wercker.yml の最終行を見ると、tag: v1 と、Push する Docker イメージのタグ名がベタ書きになっている。このままだと、イメージ名を変更したい場合はこの wercker.yml のベタ書きな行を変更して git push しないといけなくなってしまう。少々面倒で、忘れてしまいやすい。

そこでアイデアとして考えられるのは、Git のブランチ名からバージョン番号を抜き出して利用する、という方法。例えば release/v1release/v2 といったブランチを必ず作るルールにしていれば、この release/ 以降の文字列を抜き出して使用できれば良いワケだ。

Wercker の実行環境には、Wercker が自動的に設定してくれる環境変数がいくつかあり、トリガーとなった Git ブランチ名は $WERCKER_GIT_BRANCH という環境変数で読み取れる。コレで取得したブランチ名を整形してタグ名を作り、export コマンドで環境変数として残してやれば、次の Step で参照できる。

以下にサンプルを掲載する。

deploy:
  steps:
    # 環境変数 $WERCKER_GIT_BRANCH から「release/」部分を除去し、Docker タグ名として使えるように環境変数 $TAG_NAME に設定する
    # ブランチ名が「release/」始まりでないブランチはそもそもこの Step を実行しなくて良いので異常終了とする
    - script:
        name: create image tag name
        # コードを複数行に渡って書きたい場合は、以下のようにパイプ記号を入れてから改行していけば書ける
        code: |
          if $(echo "${WERCKER_GIT_BRANCH}" | grep '^release\/*' > /dev/null) ; then
            echo "This is release branch ... ${WERCKER_GIT_BRANCH}"
          else
            echo "This is not release branch. Abort ... ${WERCKER_GIT_BRANCH}"
            exit 1
          fi
          export TAG_NAME=$(echo "${WERCKER_GIT_BRANCH}" | sed -e 's:^release/::')
          echo "Tag name ... ${TAG_NAME}"
    - internal/docker-build:
        # Docker Build : 中略
    - internal/docker-push:
        # 他の項目は中略
        # Docker イメージのタグ名として、先ほど export した環境変数 $TAG_NAME を使用する
        tag: $TAG_NAME

この deploy Pipeline は release/ で始まるブランチに対する Push 時のみ実行できれば良いので、Wercker 管理画面の Workflows 定義にて、実行するブランチの条件を設定しておく。シェルスクリプトでの異常終了処理はあくまで安全のため。

なお、GitHub 以外の Git リポジトリと接続している場合、ブランチ名に含まれるスラッシュ以前の文字列が欠落する不具合を確認しているので、Wercker の挙動に合わせて、スラッシュではなくハイフンで繋ぐなど、ブランチ命名規則は要検討。

Kubernetes クラスタにデプロイするには

wercker.yml から Kubernetes クラスタにデプロイするにはどうしたらいいかというと、以下のページに掲載されているコードが参考になるだろう。

deploy:
  steps:
    - kubectl:
        name: apply deployment
        server: $KUBERNETES_MASTER
        token: $KUBERNETES_TOKEN
        insecure-skip-tls-verify: true
        command: apply -f $WERCKER_ROOT/kubernetes/deployment.yaml

こんなノリで kubectl コマンドが叩ける。ココで使っている kubectl Step も Wercker 公式。

server プロパティに指定するのは kubeconfig ファイル中の clusters.cluster.server の値 (URL)、token プロパティに指定するのは kubeconfig 中の users.user.token の値。単純に kubectl コマンドが叩けるユーザのクレデンシャル情報を引っ張ってきているだけ。これらの値は環境変数として退避しておく。

deployment.yaml の中には、デプロイしたい Docker イメージ名を書くことになる。予め書いておいて git push するでも良いが、ベタ書きだと wercker.yml の中の値と一致させないといけないので、オペミスの危険がある。そこで、先程環境変数 $TAG_NAME を編み出したコードを活用して、deploy Pipeline 中で deployment.yaml を置換してしまうという方法が取れるだろう。

deployment.yaml の記述は、以下の状態で固定しておく。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  selector:
    matchLabels:
      app: my-app
  replicas: 3
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-container
          # ダミーのタグ名を記述しておく
          image: iad.ocir.io/my-tenancy/my-ocir/my-project:__TAG_NAME__
# 以下略……

タグ名の部分は後で置換するので、ダミー文字列 __TAG_NAME__ と書いておく。

続いて wercker.yml

deploy:
  steps:
    # 環境変数 $TAG_NAME を作っておく
    - script:
        name: create image tag name
        code: |
          if $(echo "${WERCKER_GIT_BRANCH}" | grep '^release\/*' > /dev/null) ; then
            echo "This is release branch ... ${WERCKER_GIT_BRANCH}"
          else
            echo "This is not release branch. Abort ... ${WERCKER_GIT_BRANCH}"
            exit 1
          fi
          export TAG_NAME=$(echo "${WERCKER_GIT_BRANCH}" | sed -e 's:^release/::')
          echo "Tag name ... ${TAG_NAME}"
    - internal/docker-build:
        # Docker Build : 中略
    - internal/docker-push:
        # Docker Push : 中略
    # 環境変数 $TAG_NAME を利用して deployment.yaml を置換・上書き保存する
    - script:
        name: edit deployment file
        code: |
          sed -i -e "s/__TAG_NAME__/$TAG_NAME/" $WERCKER_ROOT/kubernetes/deployment.yaml
    # 置換したファイルを使って Kubernetes クラスタにデプロイする
    - kubectl:
        name: apply deployment
        server: $KUBERNETES_MASTER
        token: $KUBERNETES_TOKEN
        insecure-skip-tls-verify: true
        command: apply -f $WERCKER_ROOT/kubernetes/deployment.yaml

こんな感じで、deployment.yamlsed で置換し、それを使って kubectl apply を叩けば良いだろう。

じゃあブルー・グリーン・デプロイをするには?service.yaml も置換して kubectl apply しようか。その後残った古い Deployment をどう消そうか?…考え出すとつらい。そもそも「時代は CI・CD だ!」と言いながら「リリース予定日」を決めないと動けなかったりするのが実際のところなので、kubectl を叩くような wercker.yml は書かないことにした。手動でやろう。

Docker ビルドと Push だけにするにしても、コレも高頻度でやられてしまうと困る、という場合は、「Docker ビルドを行うためのブランチ名規則」を決めておいて、ビルドしたい時だけそのブランチ名で git push してやる、という方法が取れるだろう。Werkcer は git tag の Push にも対応しているのだが、現状は連携先が GitHub リポジトリの場合しか使えないので、仕方なく「ビルド用ブランチ」という考え方を取ることにした。

# こんな風にリモートに Push するブランチ名を変える
$ git push origin master:build-v1.2

# リモートブランチが要らなくなったら以下のようにすれば消せる
$ git push origin :build-v1.2

以上

Wercker にしても Jenkins にしても、「自動化できる」といえばできるんだけど、それは「技術的には出来る」までの意味でしかない。自動化するための手順を作っていくにはやっぱり人が作る必要があって、何度実行してもおかしくならないような冪等性を担保したり、特定のエラーをハンドリングしたり、といったことを始めると物凄く大変になる。そうして作り上げた「匠の技盛り合わせスクリプト」は、Jenkinsfile だろうと YAML だろうとシェルスクリプトだろうと、言語に限らず肥大化・複雑化していき、管理しきれなくなる。

チーム開発では特に問題になりがちなので、「まずどんな業務があるか」を洗い出し、できればその時点で不要な業務を「やらない」選択を取れるよう、業務を整理した方が良い。その上で、「実装コスト・その後のメンテナンスのコストを加味しても自動化するに値する業務」のみ自動化した方が良いだろう。条件が定量化できなくて自動化しづらいような業務は、潔く「自動化しない」という選択を取ることも重要。

自動化した後に本当に「楽」を感じられるように、小さく CI・CD 環境を作ろう。

参考文献