生産性向上ブログ

継続的な生産性向上を目指すエンジニアのためのブログ

CircleCI 2.1 の新機能で yaml の記述を DRY にする方法

github.com

まだプレビューですが CircleCI の新機能が 2.1 として使えるようになっているという話と、2.1 の新機能で yaml の設定を DRY にする方法を紹介します。

2.0 までの DRY な yaml を書く方法

2.0 までは CircleCI の yaml の設定を DRY にしようとすると yaml のエイリアスを使う必要がありました。

例えば、node.js のプロジェクトで node の複数バージョンでビルドする設定をエイリアスで DRY にしようとすると次のようになります。

node-base: &node-base
  steps:
    - checkout
    - restore_cache:
        keys:
          - v1-npm-deps-{{ checksum "package-lock.json" }}
          - v1-npm-deps-
    - run: npm install
    - run: npm test
    - save_cache:
        key: v1-npm-deps-{{ checksum "package-lock.json" }}
        paths:
          - node_modules

version: 2
jobs:
  test-v8:
    docker:
      - image: circleci/node:8
    <<: *node-base
  test-v10:
    docker:
      - image: circleci/node:10
    <<: *node-base

workflows:
  version: 2
  build:
    jobs:
      - test-v8
      - test-v10

ジョブの内容について詳細な説明は省きますが、&node-base の部分でアンカーを作成し、<<: *node-base の部分でオブジェクト(正確にはマッピング)のキーとしてマージすることで、共通部分の steps を DRY にしています。

エイリアスでも結構 DRY に書けるのですが、yaml の仕様上、配列(正確にはシーケンス)のマージができない、共通部分でも部分的に変数化してパラメータとして受け取るようなことができないなどがあり、共通化したい内容によっては困ることがありました。(正直 yaml の仕様にそこまで詳しくないので、全然違ってたら教えてください)

このあたりの問題が 2.1 では解消されています。

2.1 を有効にする方法

2.1 の新機能は現時点でオプトインのプレビュー版となっています。有効にするには、プロジェクトごとに設定を変更する必要があります。

  1. Project Settings → Advanced Settings を開く
  2. Enable build processing (preview) を On にする

f:id:miya-jan:20181001033922p:plain

この記事では触れませんが、この設定は 2.1 の新機能だけでなく、新しいビルドトリガー API冗長なビルドの自動キャンセル機能のワークフロー対応も有効化されます。

有効にする前に知っておくべき注意点としては、次があります。

  • プレビュー版なのでまだ安定していないかもしれない
  • ほぼ後方互換性を保っているが、一部変更点がある
    • << が特殊なキーワードになるので、これまでのシェルなどの記述で '<<' を書いてた部分は \ でエスケープする必要があるっぽい
    • CLI でローカルビルドがサポートされてない
      • 回避策はある(後述)
    • すでにドキュメントに記載されていない古いシンタックスが使えなくなる
      • shellsetup_docker_engine など

2.1 の新機能

ここからは 2.1 の新機能を紹介していきます。全体に共通することとして、.circleci/config.yml の先頭に書いていた version: 2 を、version: 2.1 にしないと新機能が使えないので注意してください。あと、workflows 下の version はいらなくなったみたいです。

Commands

commands を使うと独自のステップを定義できます。例えば、先程の例の restore_cacherun: npm install をまとめて 1 つのステップにしてみます。

version: 2.1

commands:
  npm_install_with_cache:
    steps:
      - restore_cache:
          keys:
            - v1-npm-deps-{{ checksum "package-lock.json" }}
            - v1-npm-deps-
      - run: npm install

jobs:
  test-v8:
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - npm_install_with_cache
      - run: npm test
      - save_cache:
          key: v1-npm-deps-{{ checksum "package-lock.json" }}
          paths:
            - node_modules
(省略)

npm_install_with_cache という独自のコマンドを定義して、ジョブの steps 内で呼び出しています。コマンドの必須キーは steps だけですが、オプショナルなキーとして description でコマンドの説明を、parameters でパラメータを定義できます。パラメータについては後述します。

Executors

executors を使うとビルドの実行環境を定義できます。今回の例では node という executor を定義して、ジョブから呼び出してみます。

version: 2.1

executors:
  node:
    parameters:
      tag:
        type: string
    docker:
      - image: circleci/node:<< parameters.tag >>

jobs:
  test-v8:
    executor:
      name: node
      tag: "8"
    steps:
(省略)
  test-v10:
    executor:
      name: node
      tag: "10"
    steps:
(省略)

例だと docker だけ定義しているので元の例と比べてあまり共通化してる感じがしないかもですが、executor には次のキーを組み合わせて使えます。

  • docker or machine or macos
  • environment
  • working_directory
  • shell
  • resource_class

また、コマンドと同じように parameters キーも定義できるため、今回の例でイメージのタグを tag というパラメータで切り替えられるようにしています。

parameters では type のみが必須キーで、string,boolean,enum,steps が値としてサポートされています。オプショナルキーとしては description でパラメータの説明を、default でパラメータのデフォルト値を指定できます。

ジョブのパラメータ化&ワークフロー内での複数回実行

ジョブの定義にも parameters が設定できるようになり、さらに 1 つのワークフローの中で同じジョブを複数回実行できるようになっています。今回の例だと、ジョブの定義を node のバージョンごとに分けて用意する必要がなくなります。

version: 2.1

executors:
  node:
    parameters:
      tag:
        type: string
    docker:
      - image: circleci/node:<< parameters.tag >>

jobs:
  test:
    parameters:
       node-version:
         type: string
    executor:
      name: node
      tag: << parameters.node-version >>
    steps:
(省略)

workflows:
  build:
    jobs:
      - test:
          name: "test-v8"
          node-version: "8"
      - test:
          name: "test-v10"
          node-version: "10"

ジョブの定義は test だけになり、workflows 内で test ジョブをパラメータを変えて複数回実行しています。workflowstest ジョブを呼び出すときに name キーでジョブ名を指定することによって、ブラウザ上の表示で見やすくなりますし、今回の例では使ってないですが requires で依存関係を指定するときにも便利です。

Conditional Steps

同じジョブでも boolean のパラメータによってステップを実行するかどうか分岐させることができるようになっています。今回の例だとちょっと説明が難しいですが、例えば node のバージョンが 10 のときだけカバレッジを取得してテスト実行するようにしてみます。

version: 2.1

jobs:
  test:
    parameters:
      node-version:
        type: string
      measure-coverage:
        type: boolean
        default: false
    executor:
      name: node
      tag: << parameters.node-version >>
    steps:
      - checkout
      - npm_install_with_cache
      - when:
          condition: << parameters.measure-coverage >>
          steps:
            - run: npm run test:coverage
      - unless:
          condition: << parameters.measure-coverage >>
          steps:
            - run: npm test
      - save_cache:
          key: v1-npm-deps-{{ checksum "package-lock.json" }}
          paths:
            - node_modules

workflows:
  build:
    jobs:
      - test:
          name: "test-v8"
          node-version: "8"
      - test:
          name: "test-v10"
          node-version: "10"
          measure-coverage: true

ジョブの steps 内で when キーと unless キーを使って measure-coverage というパラメータが true のときは npm run test:coverage、false のときは npm test が実行されるようにしています。

Orb

Orb は、これまで紹介したコマンドや実行環境やジョブをパッケージングして共通化するための仕組みです。ここまでの説明は 1 リポジトリの設定内での共通化を例にしましたが、実際には複数プロジェクトで設定を共通化したい場合もあり、そういうときに Orb という形でパッケージングして共有するようです。

ただし、現時点では個人で Orb の公開をすることなどがまだできないようなので、この記事ではまだ詳細について触れるのをやめておきます。

CLI の変更

2.1 に合わせて CircleCI の CLI ツールが新しくなっています。

github.com

これまでの CLI を使っている人は、以下のコマンドを実行する必要があります。

$ circleci update
$ circleci switch

circleci config validate はこれまで通り実行できます。

ローカルでビルドを実行する方法が少し変わっていて、circleci build だったのが circleci local execute に変わっています。(実はヘルプに表示されないだけで circleci build も今は実行できるようですが)

さらに、現状だと 2.1 の設定ファイルだと circleci local execute がエラーになります。

$ circleci local execute
Error:
You attempted to run a local build with version '2.1' of configuration.
Local builds do not support that version at this time.
You can use 'circleci config process' to pre-process your config into a version that local builds can run (see 'circleci help config process' for more information)

エラーメッセージにある通り、circleci config process で前処理して 2.0 形式に変換するとローカルビルドが実行できます。

$ circleci config process .circleci/config.yml > .circleci/config-2.0.yml
$ circleci local execute -c .circleci/config-2.0.yml --job test-v8

まとめ

新機能が多いので全部は拾いきれてないですが、CircleCI 2.1 について簡単にまとめました。

例だとちょっとありがたみが実感しづらいかもしれませんが、これまでよりとても柔軟にジョブ設定の共通化ができるようになっています。複雑で大規模なビルドパイプラインを構築しようとすると yaml が肥大化しがちだったので、これはとてもありがたい新機能追加だと感じています。実際、会社のあるチームは 2.1 の新機能のおかげで config.yml を 500 行ぐらい削減してました。

読みやすさという面でも、これまでのエイリアス方式と比較して意味単位がわかりやすいと感じています。CI 沼にハマると無限に時間が過ぎていってしまうので、設定が理解しやすいというのはとても大事です。

2.1 はまだプレビューなので不安定な部分もあるかもしれませんが、とても便利かつ大きな変更なので、今のうちに試して気づいたことがあれば中の人にフィードバックするといいと思います。(CircleCI Japan の twitter アカウントはレスポンスがとてもよくていつも助かっています、ありがとうございます!)