Kesinの知見置き場

知見を共有していきたいじゃないですか

CircleCI Orbsの美味しいところだけを使おう 〜例えばrestore_cacheを撲滅する〜

はじめに

CircleCI 2.1になってOrbsという新機能が登場しましたが、みなさん活用しているでしょうか? 先駆者の方々がOrbsについてのブログを早速書いてくれていますが、その多くは自分でOrbsを作る記事でした。

Orbsは自分で作成するだけでなく、CircleCI公式や誰かが作成して公開したOrbsも使うことが可能ですが、そのようなサンプルコードや解説記事がまだまだ少ない状況です。実際に自分がCircleCI公式のOrbsを使用しようとしたときにサンプルが少なくて困ったので、自分でOrbsを作らなくても使うだけで十分という人向けに解説をします。

Before & After

まずはサンプルを紹介します。BabelかTypeScriptでnpm run buildを実行するというjs開発では日常的に見られるconfig.ymlです。

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:latest
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          - v1-dependencies-
      - run: npm install
      - run:
          name: build
          command: npm run build
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
workflows:
  version: 2
  build:
    jobs:
      - build

これが、Orbsを使うとこのように書けます。

version: 2.1
orbs:
  # https://circleci.com/orbs/registry/orb/circleci/node
  node: circleci/node@0.0.8 # circleci/nodeのOrbsを`node`という名前で参照する
jobs:
  build:
    executor:
        # nodeのOrbsによってimage: 'circleci/node:latest'と同じになる
        name: node/node
        tag: "latest"
    working_directory: ~/repo

    steps:
      - checkout
      - node/with-cache:
          # デフォルトは~/project/node_modulesをキャッシュするのでworking_directoryを変更した場合はそれに合わせる
          dir: ~/repo/node_modules
          cache-version: v1
          steps:
          - run: npm install
          - run:
              name: build
              command: npm run build
workflows:
  version: 2
  build:
    jobs:
      - build

Orbsによってrestore_cache, save_cacheが隠蔽されているのが分かるでしょうか。
キャッシュ周りについてはCircleCIのサンプルコードをコピペしてるだけの人が大半ではないでしょうか。本当は暗黙的にキャッシュして欲しいぐらいですが、CircleCI 2.0からはキャッシュコントロールも含めて全てユーザが面倒を見る仕様となったため、みんな面倒くさいと思いつつ毎回コピペするしかありませんでした。

Orbsは再利用可能なパーツを提供できる仕組みであり、自分で作らずとも誰かが作ったOrbsを使わせてもらうことでコピペコードを減らすことができます。

Orbsを知る

npm installのキャッシュ周りだけをOrbsで解決する方法を検索して記事に辿り着いた方は先程のサンプルコードだけで十分かもしれません。
ここからはOrbsを使うために知っておくべき要素を解説します。

OrbsはCommands, Executors, Jobsで構成されています。3つを全て含む必要はなく、Commandsだけで構成されているOrbsもたくさんあります。では実際にドキュメントを見ながらそれぞれについて理解していきましょう。
https://circleci.com/docs/2.0/using-orbs/

Commands

Commandはstepsの集まりです。そしてstepsはrunとかsave_cacheなどのことです。
workflowsで複数のjobに分割していると同じコマンドのコピペだらけになることが多いですが、同じコマンドを繰り返している場合は一連の流れがCommandとして登録されているOrbsを使うことでコピペコードを撲滅できます。

Orbsの中にはパラメータを受け取り、挙動を変えることができるものも存在します。例えば、先程のサンプルのwith-cacheではdir, cache-version, stepsがパラメータです。
ちなみにCommandの中にstepsを入れ子にすることも可能で、with-cacheはそれをうまく活用したものとなっています。詳しくは後述します。

Jobs

CommandとExecutorが一緒になったものがJobです。実はこれは見慣れたものであり、2.0から意識せずとも定義しているjobsの中で実行環境のdockerイメージとstepsを書いているはずです。

workflowsでjobsに定義したものを使用しているのと同じようにして、Orbsで定義されたjobsもまたworkflowsの中で使うことが可能です。

Executors

見慣れない単語ですがこれも実は新しい概念というわけではありません。今までjobsの中でdockerイメージを指定していたものがExecutorです。

個人的にはExecutorは実はOrbsの中で結構重要なものではないかと思います。基本的コピペコードを撲滅するためにOrbsを使うことが多いと思いますが、Orbsの中で定義されているCommandを実行できるかどうかは実行環境に依存しています。

例えば、npmはnodejsが動く環境でしか使えませんし、bundlerrubyが動く環境でしか使えません。npmbundlerであればDocker Hubから公式のイメージを探せばOKでしょうが、例としてHerokuに使用するherokuコマンドを扱うOrbsを見つけたとして、その実行環境にはどのdockerイメージを使うべきでしょうか?

このように、実はCommandだけでは片手落ちなのです。Executorが用意されているOrbsでは作者がCommandの実行に必要な環境を指定しているはずですので、Orbsが提供しているExecutorを指定すれば実行環境について考える必要はなくなるでしょう。

また、Orbsが対応していればExecutorにもパラメータを渡すことが可能です。パラメータをdockerイメージのタグ指定に使うことで、言語やOSのバージョンをある程度コントロール可能にしているものが多いようです。

実際に外部製のOrbsのドキュメントを読んでみよう

では実際にcircleci/node使い方をドキュメントから読み解いてみましょう。

Commands

執筆時点でのバージョン0.0.8ではCommandsが3つありますが、サンプルでも使用したwith-cacheを例に解説します。

f:id:Kesin:20190226003809p:plain
https://circleci.com/orbs/registry/orb/circleci/node

まず、stepsがREQUIREDになっているため、このパラメータは必須であることが分かります。そして、DESCRIPTIONによると、Nodeのキャッシュを使いながら実行するステップを指定するようです。

次にdir, cache-key, cache-versionを見ると、デフォルト値がそれぞれ以下のようになっています。

PARAMETER DEFAULT
dir ~/project/node_modules
cache-key package.json
cache-version v1

ちなみに、Orbsを使う前のconfig.ymlではsave_cacheステップはこのようにしていましたね。これを見ると、これらのパラメータはおそらくsave_cacheのステップで使用する変数をコントロールするものだと推測ができます。

- save_cache:
  paths:
    - node_modules
   key: v1-dependencies-{{ checksum "package.json" }}

では本当に想定した挙動かどうかを確認します。"Show Command Source"と書かれているリンクをクリックするとOrbsの実際のyamlが表示されるので見てみましょう。
まず、細かいパラメータを省略すると全体の流れはこのようになっています。

steps:
  - when:
      condition: << parameters.use-strict-cache >>
      steps:
        - restore_cache:
  - unless:
      condition: << parameters.use-strict-cache >>
      steps:
        - restore_cache:
  - steps: << parameters.steps >>
  - save_cache:

use-strict-cacheのwhen-unlessを一旦横においておくと、restore_cache -> 任意のsteps -> save_cacheという流れになっていることが分かります。つまり、stepsパラメータにキャッシュ周り以外のnpm installnpm run buildといったstepを渡せばいい感じにキャッシュを扱ってくれそうな感じがします。

次にsave_cacheの定義を見てみましょう。keysの文字列を読み解くと、cache-version, ブランチ, cache-keyチェックサムの3つの組み合わせでキャッシュを判定し、dirディレクトリをキャッシュ対象とすることが分かります。

- save_cache:
    key: >-
      node-deps-<< parameters.cache-version >>-{{ .Branch }}-{{ checksum "<<
      parameters.cache-key >>" }}
    paths:
      - << parameters.dir >>

どうでしょうか、ざっと見ただけでもwith-cacheがどのような挙動をするのか大体つかめたのではないかと思います。

Executors

バージョン0.0.8にはExecutorが4種類あります。おそらくdefaultが標準として用意されているものなのでこちらを解説といきたいところですが、実際にコードを見てみるとnodeと全く同一なのでnodeを例にします。

"Show Executor Source"をクリックすると以下の実際のコードが表示されます。非常にシンプルになっており、dockerイメージにはcircleci/nodeを使用し、タグの指定がtagパラメータで可能なことが分かります。 デフォルト値はlatestです。つまり、自分のconfig.yml上でこのOrbsのExecutorを使用すると、実際にはcircleci/nodeのdockerイメージが使用されるということが分かります。

parameters:
  tag:
    type: string
    description: Pick a specific circleci/node image variant.
    default: latest
docker:
  - image: 'circleci/node:<< parameters.tag >>

ここまでドキュメントから読み解いた挙動を元に、再び最初に示したOrbsを使用したサンプルコードを見直してみてください。Orbsの概念、そして使い方もそれほど難しいものではないことが分かるかと思います。

玉石混交のOrbsの中から玉を探す方法

ここまでの解説によって自分でOrbsを作らずとも、外部製のOrbsを活用する方法が理解できたと思います。
次は実際に必要なOrbsを探すことになるのですが、今はOrbsが公開されたばかりということもあって少し見ただけでも以下のように玉石混交の状況です。

  • CircleCI謹製
  • かなり本気で作成したコミュニティ製
  • とりあえずやってみましたレベル

自分が使いたいコマンドで検索して候補が複数存在した場合、あくまで私見ですが以下の点で優れているものを選ぶと良いと思います。

指定可能なパラメータが多いもの

パラメータが多い = 挙動を細かくコントロールできるということ。
自分が使いたいコマンドのオプションをコントロールできるパラメータが存在しない場合、Orbsを使えないことになってしまいます。最初から望む細かい挙動などがあるのならばちゃんとOrbsのコードを確認しておきましょう。

例えば、with-cacheはパラメータの数が豊富だったのでキャッシュ周りで困ることはなさそうですね。

Orbsを利用することでコード量の減少が見込めるもの

例として紹介したwith-cacheはCommandにstepsを渡せるという仕様をうまく活用することでrestore_cache, save_cacheのコードを減らしています。さらに、キャッシュという本来あまり気にしたくはない工程を隠蔽してくれる点で非常に優れていると思います。

他には、パラメータのデフォルト値として妥当なものが設定されているかどうかも重要でしょう。パラメータの数は多いほうが良いと先述しましたが、デフォルト値が微妙だと自分で全て指定することになってしまいます。

executorとなるdockerイメージのタグを柔軟に選択可能なもの

例えば、executorで指定されたdockerイメージがcircleci/node:10とタグが固定されてしまっていた場合、どうなるでしょうか?
その場合、新しいNode.jsのバージョンが将来公開されたとしても、circleci/node:11を指定できないため古いNode.jsを使用し続けるしかなくなってしまいます。

そのOrbsのバージョンが上がることでExecutorsが更新されて新しいNode.jsに対応する可能性もありますが、その場合はExecutors以外にJobsやCommandsに非互換で破壊的な変更が入る可能性も有り得るでしょう。

Executorのパラメータを使ってある程度柔軟にdockerイメージを指定できるOrbsであれば、Orbsのバージョンを固定してもパラメータを更新するだけで新しいdockerイメージを使うことができます。

まとめ

CircleCI 2.1から登場したOrbsはまだサンプルコードもあまりない状況です。そのような状況ですのでドキュメントを見るだけで使い方を読み解けるようにOrbsの使い方を解説しました。

この記事でOrbsの概念とドキュメントの読み方は理解できたと思いますので、ぜひOrbsのドキュメントやコードを読み解きながら活用してみてください。