CircleCIを2.0から2.1にアップグレードした

CircleCIログ

CircleCIのver2.1から新機能が追加されました。

新機能が追加されたことによって今までの同じような記述を何度も繰り返さればならなかった箇所をスッキリさせることで、CircleCIを触ったことない人でもざっくりと何をやっているのかわかりやすい設定ファイルを作成できます。

新しく追加された機能と、バージョンアップの取り組みから学んだことを紹介します。

新機能

まず、はじめに新しく追加された機能について紹介します。

変更点はCircleCI 2.1 Config Overview に記載されています。

commands

steps配下の設定を集約したもので、steps内で呼び出すことによって再利用できます。

version: 2.1

commands:
  my-command:
    steps:
      - run: echo "a command is a collection of steps"
      - run: echo "this command has two steps"

jobs:
  my-job:
    steps:
      - my-command

executors

定義した実行環境の再利用を可能にする機能です。例えば、dockerenvironmentのキーなどです。

version: 2.1

executors:
  my-executor:
    docker:
      - image: python
  my-other-executor:
    docker:
      - image: ruby

jobs:
  my-job:
    executor: my-executor
    steps:
      - run: echo "i'm using my-executor"
  my-job2:
    executor: my-other-executor
    steps:
      - run: echo "i'm using my-other-executor"

jobs

jobsは、stepの集まりです。2.1からの変更点は、後述するparametersキーを設定できるようになったことです。

また、以前のjobsではworkflow内で同一の名前をつけることができませんでした。この問題は、version2.1から解決しました。

version: 2.1
workflows:
  build:
    jobs:
      - loadsay
      # This doesn't need an explicit name as it has no downstream dependencies
      - sayhello:
          saywhat: Everyone
          requires:
            - loadsay
      # This needs an explicit name for saygoodbye to require it as a job dependency
      - sayhello:
          name: SayHelloChad
          saywhat: Chad
      # Uses explicitly defined "sayhello"
      - saygoodbye:
          requires:
            - SayHelloChad

parameters

commandexecutorsjobsのサブキーです。パラメータを設定することで、似たような処理だが一部異なる箇所をまとめる時に利用します。現在は、以下のパラメータが設定できます。詳しい利用方法は、公式ドキュメントが一番わかりやすいです。

  • string
  • boolean
  • integer
  • enum
  • executor
  • steps
  • environment variable name

orbs

commandexecutorsjobsの集合体です。Docker-Hub、RubyのGemのように優秀なエンジニアが作成した最強のconfigファイルのパラメータを変更して使うイメージです。個人的には、専門分野でないならエコシステムが確立した際に、利用させて頂くのがよいのではないかと思います。

Orbsは、Explore Orbsで探すことができます。CircleCIの設定ファイルの書き方を勉強するための素材としても価値がとても高いので、CircleCIを始めたばかりの初心者にもオススメです。

私の場合だと、@sue445さんが作成したsue445/ruby-orbsを見て、多くのことを学びました。

以上が、新しく追加された機能です。

バージョンアップ時の注意点

CircleCIのGitHubにversion2.1の注意事項が記載されています。

以下、ざっくりとした日本語の要約を示しますが詳細はIMPORTANT: 2.1 Configuration Caveatsを確認してください。

  • config.ymlの一番最初に、version: 2.1と記載する

  • パラメータが導入されたことによって<< parameters.foo >>などを字句解析する際に問題となるので、<<という記号を使っていた場合、prefixとしてバックスラッシュ\をつける必要があります。

  • shellsetup_docker_engineキーが使えなくなりました。それぞれ、runsetup_remote_dockerに書き換えが必要です。

  • CLIによるローカルビルドをサポートしていません。circleci config processコマンドでファイルを書き出した後に、buildする必要があります。

circleci config process .circleci/config.yml > process.yml ## 変換と書き出し
circleci local execute -c process.yml --job *your_job_name*

バージョンアップで学んだこと

簡易的な文法のチェックは、circleci config validateで行います。

executorscommandsを利用したとしても、冗長な記述がなくなるだけなので設定ファイルの構造自体に変化が起きてはいけません。そこで、コミットする前にcircleci config process .circleci/config.ymlで書き出したファイルと元々のファイルをDiffcheckerなどを使って比較して差分がないことを確かめました。

バージョンアップ後のCircleCI設定ファイルでは、Parameterを利用しませんでした。Parameterを駆使したPullRequestを出したのですが、読みにくいと指摘を受けました。感覚値ですが、一つの処理に対して2つ以上Parameterを使っていた場合は、読みにくいので一つで表現できないなら利用する必要はないと思います。

少ない行数の設定ファイルだと、逆に行数が増えることがあるかもしれません。しかし、読みやすさは上がるので取り組む余地はあると思います。

CircleCI設定ファイル

ENVやRubyなどのバージョン情報は、適当な値に置き換えているのは、ご了承下さい。

バージョンアップしたことによる変更した主な場所は、jobs配下のsteps実行の部分です。特に、ほぼ同じような設定であるrubucopbrakemanが見やすくなっていることがわかります。RSpecにおいても、注目すべき箇所を判断しやすくなっています。

変更前

jobs:
  brakeman:
    docker:
      - image: circleci/ruby:version
    steps:
      - checkout
      - restore_cache:
          keys:
            - rubygems-dependencies-{{ checksum "Gemfile.lock" }}
            - rubygems-dependencies-
      - run:
          name: Install Ruby Dependencies
          command: |
            bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
      - save_cache:
          paths:
            - vendor/bundle
          key: rubygems-dependencies-{{ checksum "Gemfile.lock" }}
      - run:
          name: Run Brakeman
          command: |
            bundle exec brakeman
  rubocop:
    docker:
      - image: circleci/ruby:version
    steps:
      - checkout
      - restore_cache:
          keys:
            - rubygems-dependencies-{{ checksum "Gemfile.lock" }}
            - rubygems-dependencies-
      - run:
          name: Install Ruby Dependencies
          command: |
            bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
      - save_cache:
          paths:
            - vendor/bundle
          key: rubygems-dependencies-{{ checksum "Gemfile.lock" }}
      - run:
          name: Run rubocop (new and modified files)
          command: |
            bundle exec rubocop --parallel
  rspec:
    docker:
      - image: circleci/ruby:version-stretch-node-browsers
        environment:
          DATABASE_HOSTNAME: 
          DATABASE_USERNAME: ""
          DATABASE_PASSWORD: ""
          DATABASE_NAME_FOR_TEST: ""
          REDIS_PORT_6379_TCP_ADDR: ""
          REDIS_PORT_6379_TCP_PORT: ""
          HOST: ""
      - image: circleci/mysql:version
        environment:
          - MYSQL_ROOT_PASSWORD=
        command: mysqld hoge hoge
      - image: redis:version-alpine
    parallelism: 2
    steps:
      - checkout
      - run:
          name: Set Timezone
          command: sudo /bin/cp -f /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
      - restore_cache:
          keys:
            - rubygems-dependencies-{{ checksum "Gemfile.lock" }}
            - rubygems-dependencies-
      - restore_cache:
          keys:
            - v1-npm-packages-{{ checksum "yarn.lock" }}
            - v1-npm-packages-{{ checksum "package.json" }}
            - v1-npm-packages-
      - run:
          name: Install Ruby Dependencies
          command: |
            bundle check --path=vendor/bundle || bundle install --force --path=vendor/bundle --jobs=4 --retry=3
      - run:
          name: Install Node Dependencies
          command: |
            npm install
      - save_cache:
          paths:
            - vendor/bundle
          key: rubygems-dependencies-{{ checksum "Gemfile.lock" }}
      - save_cache:
          paths:
            - ./node_modules
          key: v1-npm-packages-{{ checksum "yarn.lock" }}
      - save_cache:
          paths:
            - ./node_modules
          key: v1-npm-packages-{{ checksum "package.json" }}
      - run:
          name: Create DB
          command: |
            bundle exec rake db:create db:schema:load --trace
      - run:
          name: Run RSpec
          command: |
            TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
            bundle exec rspec -b --format Fuubar  --format RspecJunitFormatter --out /tmp/test-results/rspec.xml -- ${TESTFILES}
          no_output_timout: 50m
      - store_artifacts:
          path: coverage
          destination: coverage
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

workflows:
  version: 2.0
  rubocop-and-rspec-brakeman:
    jobs:
      - rubocop
      - brakeman
      - rspec

変更後

version: 2.1

executors:
  default:
    docker:
      - image: circleci/ruby:verison
        environment:
          TZ: "Asia/Tokyo"
  extend:
    docker:
      - image: circleci/ruby:version-stretch-node-browsers
        environment:
          TZ: "Asia/Tokyo"
          DATABASE_HOSTNAME: 
          DATABASE_USERNAME: ""
          DATABASE_PASSWORD: ""
          DATABASE_NAME_FOR_TEST: ""
          REDIS_PORT_6379_TCP_ADDR: ""
          REDIS_PORT_6379_TCP_PORT: ""
          HOST: ""
      - image: circleci/mysql:version
        environment:
          MYSQL_ROOT_PASSWORD: ""
          TZ: "Asia/Tokyo"
        command: mysqld hoge hoge
      - image: redis:version-alpine
        environment:
          TZ: "Asia/Tokyo"
commands:
  restore_gems:
    steps:
      - restore_cache:
          keys:
            - rubygems-dependencies-{{ checksum "Gemfile.lock" }}
            - rubygems-dependencies-
  restore-npm-packages:
    steps:
      - restore_cache:
          keys:
            - v1-npm-packages-{{ checksum "yarn.lock" }}
            - v1-npm-packages-{{ checksum "package.json" }}
            - v1-npm-packages-
  install_gems:
    steps:
      - run:
          name: Install Ruby Dependencies
          command: |
            bundle check --path=vendor/bundle || bundle install --clean --force --path=vendor/bundle --jobs=4 --retry=3
  install_npm-packages:
    steps:
      - run:
          name: Install Node Dependencies
          command: |
            npm install
  save_gems:
    steps:
      - save_cache:
          paths:
            - vendor/bundle
          key: rubygems-dependencies-{{ checksum "Gemfile.lock" }}
  save_yarn:
    steps:
      - save_cache:
          paths:
            - ./node_modules
          key: v1-npm-packages-{{ checksum "yarn.lock" }}
  save_npm-packages:
    steps:
      - save_cache:
          paths:
            - ./node_modules
          key: v1-npm-packages-{{ checksum "package.json" }}

jobs:
  brakeman:
    executor: default
    steps:
      - checkout
      - restore_gems
      - install_gems
      - save_gems
      - run:
          name: Run Brakeman
          command: |
            bundle exec brakeman
  rubocop:
    executor: default
    steps:
      - checkout
      - restore_gems
      - install_gems
      - save_gems
      - run:
          name: Run rubocop (new and modified files)
          command: |
            bundle exec rubocop --parallel
  rspec:
    executor: extend
    parallelism: 2
    steps:
      - checkout
      - restore_gems
      - restore-npm-packages
      - install_gems
      - install_npm-packages
      - save_gems
      - save_yarn
      - save_npm-packages
      - run:
          name: Create DB
          command: |
            bundle exec rake db:create db:schema:load --trace
      - run:
          name: Run RSpec
          command: |
            TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
            bundle exec rspec -b --format Fuubar  --format RspecJunitFormatter --out /tmp/test-results/rspec.xml -- ${TESTFILES}
          no_output_timout: 50m
      - store_artifacts:
          path: coverage
          destination: coverage
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

workflows:
  version: 2.1
  rubocop-and-rspec-brakeman:
    jobs:
      - rubocop
      - brakeman
      - rspec

感想

CircleCIの設定ファイルを変更するとRSpecのテスト実行時間を短くすることができるかもという情報があり、CircleCIに慣れるためバージョンアップに取り組みました。RSpecのテストをFeature specとそれ以外に分けてworkflowsに新しく登録することで早くテストを終わらせることができるのではないかと考えているので、試してみたいと思います。

CircleCI内だけで完結できるテスト高速化の知見がありましたら、Twitterやコメントをしてくれると嬉しいです。

command: |
  TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | 
    circleci tests split --split-by=timings)
bundle exec rspec -b --format Fuubar  --format 
 RspecJunitFormatter --out /tmp/test-results/rspec.xml -- ${TESTFILES}

オススメのリンク

公式ドキュメントを読まずにざっと内容を把握した人向け

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

CircleCIの公式が出している日本語の動画

1.0から2.0への移行

CircleCIのDiscussページ

Useful tips and best practices when migrating to CircleCI 2.0