CircleCI 2.1 の新機能を使って冗長な config.yml をすっきりさせよう!

こんにちは。スタディサプリ English の開発を担当しているwebフロントエンドエンジニアの福井です。

CircleCI で 2.1 configuration がプレビューとして使えるようになりました。試しに使ってみたところ冗長なconfig.ymlものすごくすっきりした ので簡単な例を交えて紹介します。

参考

2.0 の冗長な config.yml

今回用意したconfig.ymlはwebフロントエンドの典型的な例で、下記のような流れで順次ジョブを実行します。

  1. setup - npmパッケージのインストールとキャッシュ
  2. test - テストを実行
  3. デプロイ
    3.a deploy_dev - developmasterブランチ以外は開発環境(dev)にデプロイ
    3.b deploy_stg - developブランチはステージ環境(stg)にデプロイ
    3.c deploy_prod - masterブランチは本番環境(prod)にデプロイ

setup以降の各ジョブではsetupでキャッシュしたものをレストアしているのですが、それがジョブごとに記載されており、さらに working_directorydockerなどの実行環境もジョブごとに指定されています。やっていることは非常に単純なのですが何も考えずに書くとすごく冗長です…

2.0のconfig.yml(同じ記述だらけ)

version: 2
jobs:
  setup:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Install dependencies
          command: npm install
      - save_cache:
          name: Cache npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
  test:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Run test
          command: npm run test
  deploy_dev:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Deploy to dev environment
          command: npm run deploy:dev
  deploy_stg:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Deploy to stg environment
          command: npm run deploy:dev
  deploy_prod:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Deploy to prod environment
          command: npm run deploy:prod
workflows:
  version: 2
  setup_and_deploy:
    jobs:
      - setup
      - test:
          requires:
            - setup
      - deploy_dev:
          requires:
            - test
          filters:
            branches:
              ignore:
                - develop
                - master
      - deploy_stg:
          requires:
            - test
          filters:
            branches:
              only: develop
      - deploy_prod:
          requires:
            - test
          filters:
            branches:
              only: master

ただし、この例は実はあまりに無工夫です。通常はYAMLのAnchor/Alias、CIRCLE_JOBなどの環境変数を使って冗長な記述をまとめることが多いと思います。しかし2.1の機能を使った方がより自然かつ柔軟な形でconfig.ymlを書けるので、実際に適用するとどうなるか見ていきましょう。

2.1の新機能を使って config.yml を書く

👆のconfig.ymlを2.1の新機能であるexecutorcommandparameterを使ってすっきりさせていきます。

準備

有効化

2018年10月現在、2.1 configuration はプレビューのため、デフォルトでは オフ となっています。そのためconfig.ymlを編集する前に各プロジェクトの Settings -> Advanced SettingsEnable build processing (preview)Onにする必要があります。CircleCI 側で必要な設定はこれだけです。

CLI

CLIを使うとローカルでconfig.ymlのチェックができるので入れておきましょう。

インストール
$ bash -c "$(curl -fSl https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh)"
# Mac だと Homebrew も使える
$ brew install circleci
更新(最新にすると2.1にも対応)

config.ymlのチェック
$ circleci config validate -c .circleci/config.yml
Config file at .circleci/config.yml is valid

executor

executorは実行環境の情報を定義して再利用する機能です。以下のキーを指定できます。

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

今回の例ではnode.jsの実行環境をdefaultとしてexecutorsの中で定義してジョブの中で利用してみます。

version: 2.1
executors:
  # ここに好きな名前で executor を定義できる
  default:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
jobs:
  setup:
    executor: # name で使いたい executor を指定する
      name: default
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Install dependencies
          command: npm install
      - save_cache:
          name: Cache npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
  test:
    executor:
      name: default
    steps:
      - checkout
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Run test
          command: npm run test

command

ジョブの中で再利用したいstepcommandとして定義できるようになりました。この機能のおかげでこれまでYAMLのAnchor/Aliasを使って何とかしていた部分を共通化できるようになります。

今回の例ではnpmパッケージのリストアとキャッシュをrestore_npmsave_npmとしてコマンド化してみます1)save_npmは1箇所でしか利用していませんがセットで定義したほうが綺麗な感じがするのでついでにまとめます

version: 2.1
executors:
  default:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
commands:
  # ここにコマンドを定義する
  restore_npm: # コマンド の名前(任意)
    steps: # 再利用したステップをここに記載
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
  save_npm:
    steps:
      - save_cache:
          name: Cache npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
jobs:
  setup:
    executor:
      name: default
    steps:
      - checkout
      - restore_npm # 使いたいコマンドを指定するとそのコマンドで定義したステップが展開される
      - run:
          name: Install dependencies
          command: npm install
      - save_npm
  test:
    executor:
      name: default
    steps:
      - checkout
      - restore_npm
      - run:
          name: Run test
          command: npm run test

parameter

ワークフローの中でジョブを指定するときに合わせてパラメータも指定することができるようになりました。この機能を使うとほとんど同じ処理だけど一部分だけ変えたい場合にそれらを1つのジョブとして定義できます。 パラメータ定義はジョブ定義時に parametersキーを使って行います。 以下のキーが指定できます。

Key 必須 or 任意 説明
description 任意 パラメータの説明
type 必須 パラメータの型。現時点ではstringbooleanenumstepsが指定可能
default 任意 デフォルト値。指定しないとワークフローの中でジョブを指定する際にパラメータの指定が必須になる
enum enumのとき必須 typeenum のときに列挙するパラメータを書く

使うときはコマンド等の中で<< parameters.パラメータ名 >>と書いてやると渡ってきたパラメータが展開されます。また <<# parameters.パラメータ名 >>hogehoge<</ parameters.パラメータ名 >>と書くとparameters.パラメータ名の場合にブロックの中身(hoghoge)が展開されます。真偽の判定については後述するConditional Stepsを参考にしてください。

今回の例では deploy_devdeploy_stgdeploy_prodの3つのジョブはnpm run deploy:の後に指定するパラメータ以外はすべて同じなのでdeployジョブとしてまとめてみます。

version: 2.1
executors:
  default:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
commands:
  restore_npm:
    steps:
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
jobs:
  deploy:
    parameters:
      # ここにパラメータを定義する
      env: # パラメータの名前(任意)
        type: enum # enum にするとタイポしたときに検知してくれる
        enum: ["dev", "stg", "prod"]
    executor:
      name: default
    steps:
      - checkout
      - restore_npm
      - run:
          name: Deploy to << parameters.env >> environment
          command: npm run deploy:<< parameters.env >>
workflows:
  setup_and_deploy:
    deploy: # ジョブはすべて deploy になる。パラメータを変えることでstep中のコマンドを変える
      name: deploy_dev # name を指定すると UI 上でもこの名前で表示されるので見分けやすくなります
      env: dev # << parameters.env >> が dev として展開されます。 enum なので hoge とか書くとエラー
    deploy:
      name: deploy_stg
      env: stg
    deploy:
      name: deploy_prod
      env: prod

おまけとして、 パラメータはexecutorcommandでも使うことができます 。似たような実行環境や処理をパラメータを使って1つにまとめることもできるので使えそうな場面では積極的に使っていくと良いでしょう。 パラメータはジョブ定義時に指定することもできますし、ワークフローのジョブの中で指定することもできます。後者の場合はジョブ定義時にジョブのパラメータを渡すように明示的に書く必要があります。

ジョブ定義時に指定する場合
version: 2.1
executors:
  default:
    parameters:
      node_version:
        type: enum
        enum: ['8.12.0', '8.11.4']
    working_directory: ~/workspace
    docker:
      - image: circleci/node:<< parameters.node_version >>
commands:
  echo:
    parameters:
      val:
        type: string
    steps:
      - run: echo << parameters.val >>
jobs:
  test:
    executor:
      name: default
      node_version: 8.12.0
    steps:
      - echo:
          val: test value
workflows:
  normal:
    jobs:
      - test
ワークフローのジョブの中で指定する場合
version: 2.1
executors:
  default:
    parameters:
      node_version:
        type: enum
        enum: ['8.12.0', '8.11.4']
    working_directory: ~/workspace
    docker:
      - image: circleci/node:<< parameters.node_version >>
commands:
  echo:
    parameters:
      val:
        type: string
    steps:
      - run: echo << parameters.val >>
jobs:
  test:
    # ジョブ側でも executor, command に渡すパラメータを定義する必要がある
    parameters:
      node_version: # command 側と同じ定義になるので Anchor/Alias を使うこともできる
        type: enum
        enum: ['8.12.0', '8.11.4']
      val:
        type: string
    executor:
      name: default
      node_version: << parameters.node_version >> # executor にパラメータを渡す
    steps:
      - echo:
          val: << parameters.val >> # command にパラメータを渡す
workflows:
  normal:
    jobs:
      - test:
          node_version: 8.11.4
          val: test value 2

最終的な config.yml

executorcommandparameter を使って config.yml を書き直すとこのようになります!
如何でしょう?再利用可能な部分がうまくまとめられてすっきりしたのではないでしょうか。

version: 2.1
executors:
  default:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
commands:
  restore_npm:
    steps:
      - restore_cache:
          name: Restore npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
  save_npm:
    steps:
      - save_cache:
          name: Cache npm dependencies
          key: npm-{{ checksum "package-lock.json" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
jobs:
  setup:
    executor:
      name: default
    steps:
      - checkout
      - restore_npm
      - run:
          name: Install dependencies
          command: npm install
      - save_npm
  test:
    executor:
      name: default
    steps:
      - checkout
      - restore_npm
      - run:
          name: Run test
          command: npm run test
  deploy:
    parameters:
      env:
        type: enum
        enum: ["dev", "stg", "prod"]
    executor:
      name: default
    steps:
      - checkout
      - restore_npm
      - run:
          name: Deploy to << parameters.env >> environment
          command: npm run deploy:<< parameters.env >>
# 2.1では workflows への version 指定は必要ありません
workflows:
  setup_and_deploy:
    jobs:
      - setup
      - test:
          requires:
            - setup
      - deploy:
          name: deploy_dev
          env: dev
          requires:
            - test
          filters:
            branches:
              ignore:
                - develop
                - master
      - deploy:
          name: deploy_stg
          env: stg
          requires:
            - test
          filters:
            branches:
              only: develop
      - deploy:
          name: deploy_prod
          env: prod
          requires:
            - test
          filters:
            branches:
              only: master

その他の新機能

今回の例で使った機能以外にも新機能があるので簡単に使い方を紹介します。

Conditional Step

ジョブの中でパラメータの値によってステップを分岐できるようになりました。whenunlessキーをジョブの中に記載します。whenの場合は指定したパラメータがの場合にwhen以下で定義したステップを実行し、unlessの場合は逆に指定したパラメータがの場合にunless以下で定義したステップを実行します。

どのパラメータを使うかはconditionキーを使って定義します。パラメータのタイプがbooleanの場合はtruefalsestringenumの場合は空文字列が、それ以外はと判定されます。stepsの場合は試してみたところ何かしらのステップが指定されていても空([])でもと判定される結果になりました。

以下の例ではrun_testtrueのときにnpm run testが含まれる一連のステップを実行し、falseのときはskip testと表示されるだけです。

version: 2.1
executors:
  default:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
jobs:
  check:
    parameters:
      run_test:
        type: boolean
    executor:
      name: default
    steps:
      - run: echo begin steps
      - when:
          condition: << parameters.run_test >>
          steps:
            - run: echo begin test
            - run: npm run test
            - run: echo end test
      - unless:
          condition: << parameters.run_test >>
          steps:
            - run: echo skip test
workflows:
  normal:
    jobs:
      - check:
          run_test: true # 'npm run test' を実行する
      - check:
          run_test: false # 'npm run test' を実行しない

ネスト

ネストを表現することもできます。深くなるほど分かりにくくなるので使いすぎには注意したほうがよいでしょう。
以下の例ではrun_testrun_linttrueのときのみnpm run lintを実行します。

version: 2.1
executors:
  default:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:8.11.4
jobs:
  check:
    parameters:
      run_test:
        type: boolean
      run_lint:
        type: boolean
        default: false
    executor:
      name: default
    steps:
      - run: echo begin steps
      - when:
          condition: << parameters.run_test >>
          steps:
            - run: echo begin test
            - run: npm run test
            - run: echo end test
            - when:
                condition: << parameters.run_lint >>
                steps:
                  - run: echo begin lint
                  - run: npm run lint
                  - run: end lint
      - unless:
          condition: << parameters.run_test >>
          steps:
            - run: echo skip test
workflows:
  normal:
    jobs:
      - check:
          run_test: true
          run_lint: false
      - check:
          run_test: false

Orb

Orb はcommandexecutorjobをプロジェクトをまたいで共有するための仕組みで、共有したい部分を各プロジェクトのconfig.ymlとは別に管理できる仕組みです。Orbから別のOrbをインポートしたり、各プロジェクトで直接インポートして利用します。Orbをインポートして利用する際にジョブ側でpre-stepspost-stepsというキーを指定してOrbを利用する環境に合わせた前後の処理を Orbを変更することなく追加する ことができます。Orbはバージョン管理もできるのでバージョンを指定してインポートすることもできます。注意事項として、PublishしたOrbはorganizationのメンバーだけでなく 誰からでもアクセスできる状態になる のでOrbの中に認証情報など機密性の高い情報を含めないようにしなければいけません。詳細な利用方法については以下を参照ください。

まとめ

executorcommandだけを見ると、単なる定義の抜き出しの様でYAMLのAnchor/Aliasと比べてそこまで恩恵を感じることはできません。しかしながらparameterやConditional Stepを使うことで一気に定義の柔軟性が高まり使える機能になったと思います。

みなさんも2.1の便利な新機能を使ってconfig.ymlをガンガンすっきりさせましょう!

脚注

脚注
1 save_npmは1箇所でしか利用していませんがセットで定義したほうが綺麗な感じがするのでついでにまとめます