tech.guitarrapc.cóm

Technical updates

CircleCI Orbをいくつか作った話

幾つかCircleCI Orbを作っていたのですが、記事にしていませんでした。

どれも必要になって作ったもので、いずれもプロダクションで万単位が実行されれているのでいい感じで使えそうなら幸いです。

概要

  • NuGet Cacheを制御するならguitarrapc/nuget-cache
  • Shallow Cloneを制御するならguitarrapc/Git-shallow-clone
  • Chatworkにメッセージを投げるならguitarrapc/chatwork
  • CircleCIは、公式Orbでやっているやり方をドキュメントに反映してほいしい。Orbを初めて作るときに把握から始まるのは結構ツライ

Orbs とは

CircleCIはJobやCommandにまとめていた処理をOrbとして共有できます。

https://circleci.com/docs/ja/2.0/orb-intro/

これって結構強力でいい仕組みで、Executorを含んだり含まないことができます。

  • CircleCIでYAMLが特定の処理のためにやけに長くなってしまった (Executor依存がないとよりよい)
  • 特定の操作をしたい

特定のシーンにスコープを絞ればExecutor依存してもいいし、特定の処理に絞って汎用的に使いたいならExecutor依存しないようにするといいでしょう。

CircleCIでやっていこうと思ったら、何かしたいときはOrbをチェックするのは標準的に行うといい流れを作ろうとしています。

既存のOrbsで提案があれば適当にPR出したりもいいでしょう。 どのみちconfig.ymlは公開されますしね。

https://tech.guitarrapc.com/entry/2019/04/14/075914

公開したOrbs

3つ作って公開しています。

どれも当初はcommandsとして定義していたのですが、ほかのプロジェクトでも使えるし公開しました。

guitarrapc/Git-shallow-clone

https://circleci.com/orbs/registry/orb/guitarrapc/git-shallow-clone

Git shallow cloneをするOrbは実はいくつかあります。 有名なのは、ganta/gitでしょうか。

https://qiita.com/ngmy/items/9c27b09ecc58cfe8ea18

しかしこれらのOrbはどれもForkプロジェクトは考慮されていません。 また、GitHubやBitBucketが死んでることを想定していないので、案外失敗します。

guitarrapc/Git-shallow-cloneは、Forkプロジェクトにも対応しつつ、頻繁に使っても問題ないまでブラッシュアップしてあります。

利用はいたってシンプルです。

orbs:
  git: guitarrapc/git-shallow-clone@1.0.1
version: 2.1
workflows:
  build:
    jobs:
      - git/checkout_shallow

keyscan_bitbucketとkeyscan_githubで、実際のリポジトリをkeyscanするか選べます。 デフォルトは、鍵の更新がしばらくないのでハードコードしてあります。

depthとfetch_depthで、深さを指定できます。 Shallow Cloneは、depthで1指定しておいて、fetch depthでは少し深く5~10指定しないとブランチとかをいい感じで処理できないのでその対策も入っています。 clone時にsingle branchを使ってると気にしなくていいのですが。

cloneを高速に終わらせて、汎用的に使いたいならお勧めです。 毎日結構な回数動いてて安定しているので、大概のケースでいけるかと。

このOrbはテストを書くのが面倒でしたが、ひとこねしたテストも書けるのはCircleCI強かった。

guitarrapc/nuget-cache

https://circleci.com/orbs/registry/orb/guitarrapc/nuget-cache

.NET Coreのビルドで困るのが、nuget packageのrestoreサイズは大きく時間がかかるということです。 別に一切気にしないという手もありますが、キャッシュしておくほうが爆速になるのでやるでしょう。

ただ、cacheをするにはキャッシュキーを決める必要があります。 利用するパッケージに変更があったらキャッシュを切り替えたいわけです。 .NET Coreならcsprojの変化を見るのが一番間違いないでしょう。(Directory.Build.propsは考慮したくないですね! )

guitarrapc/nuget-cacheを使うと、任意のファイルを拾ってmd5を取得してキャッシュキーに利用しつつ、cacheのsave/restoreが可能になります。

想定している流れ場、nuget_cache/nuget_restore_cacheして、dotnet restoreしてnuget_cache/nuget_save_cacheです。

orbでいい感じのキーでnuget-restoreして、dotnet cliでdotnet restoreするのはキャッシュがあれば秒で終わります。 キャッシュがずれていたら適切にdotnet restoreでリストアされるので、あとはいい感じのキーでsaveしてあげれば次回以降のビルドで生きます。

orbs:
  nuget-cache: guitarrapc/nuget-cache@0.1.1
version: 2.1
executors:
  dotnet31:
    docker:
      - image: 'mcr.microsoft.com/dotnet/core/sdk:3.1'
    environment:
      DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
      NUGET_XMLDOC_MODE: skip
jobs:
  build:
    executor: dotnet31
    steps:
      - checkout
      - nuget_cache/nuget_restore_cache:
          cache_key: test_save_cache
          project: nuget-cache-orb
      - run: dotnet restore
      - nuget_cache/nuget_save_cache:
          cache_key: test_save_cache
          project: nuget-cache-orb
          project_cache: false
      - run: dotnet build

シンプルですが、こういうのが適当にできるのがいいなぁということで作りました。 いろんなプロジェクト構成に対応できるように、cache_keyやproject、working fidrectoryやtarget file patternも指定できるようにしてあります。

実際、外部リポジトリを使った複雑なプロジェクト構成でも機能しているのでだいたいうまくいくはずです。

guitarrapc/chatwork

https://circleci.com/orbs/registry/orb/guitarrapc/chatwork

Chatworkのルームにメッセージを送信します。 さくっとメッセージを送るだけなら、Chatwork APIを使うのがいいでしょう。(こんなことでOAuthしたくない)

ということで、curlでサクッと送ります。

orb:
  chatwork: guitarrac/chatwork@0.0.2
version: 2.1
workflow:
  build:
    jobs:
      - chatwork/notify:
          message: Hello+Chatwork%21
          room_id: '12345'

環境変数CHATWORK_TOKENにAPI Tokenだけ仕込んでおけばサクッと使えます。 また、whenに対応してあるので、「on_success成功時 (デフォルト)」「on_fail失敗時」「alwaysいつでも」の状況に合わせた送信ができます。

Orb を作る

基本は公式を見ましょう。

https://circleci.com/docs/2.0/creating-orbs/

namespaceとか、circleci cliの基本操作は沿っておく必要があります。

ただし、公式CircleCI Orbはいずれもこの構成とはずれています。ひどい。 ということで、実際どうやって構成しているのかを把握します。

Orb の構成を把握する

Orbを使うのはともかく、作ること自体は簡単です。 ただ、「テストやCI/CD」まで含めると面倒ですが、テンプレート化してしまえばあとは流れ作業で作れます。

このテンプレート化はドキュメントがなく、CircleCI公式Orbでもどれもバラバラなので把握が面倒です。

公式を見つつどんな構造化把握します。

基本構造

公式Orbもそうなのですが、Orbのディレクトリは基本的に次の構造になるはずです。

.
├── .circleci
│   └── config.yml
├── LICENSE.md
├── README.md
├── src
│   ├── commands
│   │   └── your_command.yml
│   ├── examples
│   │   └── your_command.yml
│   └── @orb.yml
└── tests
    ├── your_test.yml
    └── your_test_n.yml

シンプルに、@orb.ymlに全部定義というのもいいでしょう。

https://github.com/ganezasan/auto-cancel-workflow-orb

.circleci

Orb自体のCI/CDはCircleCI自身で行います。後述。

src/@orb.yml

OrbのDescriptionと依存するOrbなどを書きます。

例えば、次のように書く

version: 2.1

description: >
  Build images and push them to the Amazon Elastic Container Registry.
  See this orb's source: https://github.com/circleci-public/aws-ecr-orb
orbs:
  aws-cli: circleci/aws-cli@0.1.13

Orbのページでは次のように表示されます。

実際のところ、@orb.ymlじゃなくてもorb.yamlで動作します。 私はorb.ymlにしていますが、公式はみんな@orb.ymlなのでそっちにしておけばいいでしょう。

src/commands/

commandsフォルダの中に置いたファイル名がコマンド名になります。 いいですね、この割り切り。

ecr-loginというコマンドなら、ecr-login.ymlというファイル名にします。

description: "Authenticate into the Amazon ECR service"

parameters:
  region:
    type: env_var_name
    default: AWS_REGION
    description: >
      Name of env var storing your AWS region information,
      defaults to AWS_REGION
steps:
  - run:
      name: Log into Amazon ECR
      command: |
        # aws ecr get-login returns a login command w/ a temp token
        LOGIN_COMMAND=$(aws ecr get-login --no-include-email --region $<<parameters.region>>)
        # save it to an env var & use that env var to login
        $LOGIN_COMMAND

すると、ecr-login としてコマンド定義が公開される。

src/examples/

コマンドのサンプルを定義します。 examplesフォルダの中に置いたファイル名がExample名になります。

なのでコマンドごとにファイルを用意したり、Workflowとして一連の流れを定義したりです。

たとえばWorkflow simple-build-and-push-imageを定義して、それをOrb画面でsimple-build-and-pushとして表示したいなら次の定義になります。

description: Log into AWS, build and push image to Amazon ECR

usage:
  version: 2.1

  orbs:
    aws-ecr: circleci/aws-ecr@x.y.z

  workflows:
    build_and_push_image:
      jobs:
        # build and push image to ECR
        - aws-ecr/build-and-push-image:

            # required if any necessary secrets are stored via Contexts
            context: myContext

            # AWS profile name, defaults to "default"
            profile-name: myProfileName

            # name of env var storing your AWS Access Key ID, defaults to AWS_ACCESS_KEY_ID
            aws-access-key-id: ACCESS_KEY_ID_ENV_VAR_NAME

            # name of env var storing your AWS Secret Access Key, defaults to AWS_SECRET_ACCESS_KEY
            aws-secret-access-key: SECRET_ACCESS_KEY_ENV_VAR_NAME

            # name of env var storing your AWS region, defaults to AWS_REGION
            region: AWS_REGION_ENV_VAR_NAME

            # name of env var storing your ECR account URL, defaults to AWS_ECR_ACCOUNT_URL
            account-url: AWS_ECR_ACCOUNT_URL_ENV_VAR_NAME

            # name of your ECR repository
            repo: myECRRepository

            # set this to true to create the repository if it does not already exist, defaults to "false"
            create-repo: true

            # ECR image tags (comma separated string), defaults to "latest"
            tag: latest,myECRRepoTag

            # name of Dockerfile to use, defaults to "Dockerfile"
            dockerfile: myDockerfile

            # path to Dockerfile, defaults to . (working directory)
            path: pathToMyDockerfile

Orb画面は、simple-build-and-pushとファイル名になります。

tests

テストを定義しておきます。

ここはCircleCI上で解釈されて実行されるので、いい感じにファイルを置きます。 もちろんCircleCI上でconfig.yamlを走らせることもできて、その場合はconfig.yamlの記法に沿っておく必要があります。

公式Orbは案外testsフォルダを持っていないのですが、devに挙げたorbで挙動をテストするようにしています。

testsフォルダを設けている例として、mafuyukさんや私のOrbを例にします。

https://github.com/mafuyuk/terraform-orb

https://github.com/guitarrapc/chatwork-orb

例えば、mafuyuk/terraform-orbはterraform操作するOrbです。 そのtest.ymlは次のようになっています。

  version: 2.1

jobs:
  build:
    executor:
      name: terraform/default
      tag: 0.11.11
    steps:
      - checkout
      - run:
          name: Create Sample Terraform
          command: |
            mv test/backend.tf backend.tf
            mv test/main.tf main.tf
            mv test/provider.tf provider.tf
            mv test/variables.tf variables.tf
#      - terraform/init
#      - terraform/plan
#      - terraform/apply:
#          use-plan: false

コメントアウトされていますが、ニュアンスはわかります。

テストのときは、cacheのようなファイル操作ができないのはcircleci cliの制約を食らうのでそこは注意です。 なんだそれ感はある。

もし、外部リポジトリをクローンするなどSSH Keyを渡したりといった込み入ったことをする必要があるなら、こっちのOrbが参考になるでしょう。

https://github.com/guitarrapc/Git-shallow-clone-orb

ポイントは、テストをどう走らせるかです。

  test:
    # ref: https://github.com/ganta/git-orb/blob/master/.circleci/config.yml
    # Workaround for passing a SSH key with orb-tools/local-test-build command
    # https://github.com/CircleCI-Public/orb-tools-orb/blob/330d11bb0cdef3c2f24cb4e2c595ca01e55bfd8c/src/%40orb.yml#L107-L134
    commands:
      local-test-build:
        parameters:
          path:
            type: string
        steps:
          - run: cp << parameters.path >> tmp-config-src/config.yml
          - run: mkdir -pv $(dirname tmp-config-src.yml)
          - run: circleci config pack tmp-config-src > uncompiled-config.yml
          - run: cat uncompiled-config.yml
          - run: circleci config process uncompiled-config.yml > config.yml
          # Cannot access "~/.ssh" from working directory.
          # pass spin up env's CIRCLE_TAG to local circleci command.
          - run:
              command: |
                cp ~/.ssh/id_rsa ./id_rsa
                circleci local execute --checkout-key ./id_rsa -c config.yml -e CIRCLE_TAG=${CIRCLE_TAG} | \
                  tee local_build_output.txt /dev/stderr | \
                  tail -n 1 | \
                  grep "Success"

Orb のCI/CD をする

Orbを作ったら、CircleCI上でOrbのCI/CDをしたいでしょう。

CI/CDでは当然、OrbのLint、テストをしたり、Orb Devに挙げて動作を事前テストしたり、DevからProductionにPromoteしたりしたいです。 PromoteはGitHub Tagに準じるといいでしょう。

つまりこれ。

  • CircleCI Orbで提供する処理を定義する
  • CircleCI Orbで何ができるのかDescriptionやExampleを定義する
  • OrbのCIを行う
  • OrbのDevへのCDを行う
  • OrbのProductionへのCD (Promote) を行う

CircleCI-Public/orb-tools-orb

OrbリリースをするためのツールとしてCircleCIが用意しているのがCircleCI-Public/orb-tools-orbです。

https://github.com/CircleCI-Public/orb-tools-orb

これを使うと、YAMLのLintチェックから、パッケージング、テストの実行、GitHub Release連動、DevへのCD、ProductionへのCDまで一通りできます、便利! ドキュメントがなくExampleだけで動作もわかりにくいですが、がんばれ。

https://qiita.com/ganta/items/0c517f9a69faf06e3541

9.x 系

orb-tools-orbですが、9.0.0でコマンドを悉く変えています。 9系はコミットでの制御に切り替わり、コマンド自体が消えたりパラメーターが丸っと変わるなど破壊的変更が入りました。

9.0.0をやってるケースもあるので、気が向いたらあげよう。

https://qiita.com/sawadashota/items/007497215c2ffd817d9f

CircleCI公式の各種Orbは8.27.3以前のバージョンで止まっていたりします。 対応確定するまでは8.27.3をターゲットで私は書いています。(8.27.6時点でもうダメだったりする)

基本的なパターン

公式Orbを参考にするといいでしょう。

circleci-orbs/config.yml at master · CircleCI-Public/circleci-orbs

slack-orb/config.yml at staging · CircleCI-Public/slack-orb

aws-ecr-orb/config.yml at master · CircleCI-Public/aws-ecr-orb

8.27.3をターゲットにします。

定義

公式Orb各種がそうですが、概ね次の流れです。

slack-orb/config.yml at staging · CircleCI-Public/slack-orb

lint_pack-validate_publish-dev

PRへのプッシュでトリガーします。

  • orb-tools/lint
  • orb-tools/pack
  • orb-tools/publish-dev
  • orb-tools/trigger-integration-workflow
  • orb-tools/trigger-integration-workflow

integration_tests-prod_deploy

テストはintegrationタグやmaster-タグでトリガーします。 dev-promote-prodは、各種master-patch / master-minor / master-majorのタグでトリガーされます。

  • 各種テスト
  • orb-tools/dev-promote-prod (patch)
  • orb-tools/dev-promote-prod (minor)
  • orb-tools/dev-promote-prod (major)

挙動はconfig.yamlだけ見ても全然分からず、動かしたりCircleCI-Public/orb-tools-orbをみるしかないでしょう。

リリースの流れ

PRを作って、CIとdevへのCDが走るので通るのを確認、でマージするとOrbにpushされます。

Renovateを使うという手も。

https://www.kaizenprogrammer.com/entry/2019/02/07/090143

Orbのテンプレートにする

適当にguitarrapc/chatwork-orbあたりはシンプルなのでテンプレートにしやすいでしょう。

https://github.com/guitarrapc/chatwork-orb

Orbのnamespaceを用意したら、これをforkしたり適当にベースにして、OrbのコマンドとExampleとorb.ymlを変えてテスト書けば行けるはずです。(orb名やテストの置換はしましょう)

TIPS

既存のOrbで時々使いにくいやつがあるので、そうならないようにメモ。

  • Jobとしての提供なら、OrbでExecutor指定しよう
    • 案外こういうのは多いので、どう使いたいか次第でまず決めるといい
  • Stepsとしての提供なら、Orbはなるべく依存小さくするほうがいい
    • Bash依存とか避けたほういいのは間違いない
    • コマンドある前提で組んでしまって、コマンドないならサクッとエラーになるのでユーザーに任せるほうがユーザーにとっても幸せ

REF

https://qiita.com/na0ya/items/0118434ff1bb6e3bb3c0

https://www.kaizenprogrammer.com/entry/2018/12/01/111145

https://sue445.hatenablog.com/entry/2018/12/20/000000