circleci config pack で分割したymlから設定ファイル(config.yml)を作成してみた


概要

最近CircleCIに入門しました。config.ymlを書いているうちに記述量は少ないもののそれでもSlack通知のテンプレなど記述すると見通しが悪くなるので、なんとかしたいと思って調べてみるとCircleCI CLIcircleci config packというコマンドがありそれで、分割されたymlをパッケージ化できることを知ったので使ってみました。
因みにCircleCI2.1の想定です。

サンプルリポジトリ

サンプルとなるソースをリポジトリにまとめてみたのでこれを参考に紹介していきます。

CircleCI CLI

なにわともわれこれがないと進まないので以下のリンクを参照してcircleciコマンドを使えるようにします。

因みに自分はbrewを使ってインストールしました。

brew install circleci

# Mac 版の Docker を既にインストールしている場合は、
brew install --ignore-dependencies circleci

circleci config pack

コマンドは

circleci config pack <ディレクトリ名>

となります。
分割したymlを格納しているディレクトリを指定します。

以下のドキュメントに詳しく書かれています。

CLI の pack コマンドを使用すると、複数のファイルをまとめて 1 つの YAML ファイルを作成できます。 pack コマンドには、ディレクトリ ツリー内の複数ファイルにまたがる YAML ドキュメントを解析する FYAML が実装されています。 これは、容量の大きな Orbs のソース コードを分割している場合に特に利便性が高く、Orbs の YAML 構成のカスタム編成を行うことができます。 circleci config pack は、ディレクトリ構造とファイルの内容に基づいて、ファイル システム ツリーを 1 つの YAML ファイルに変換します。 pack コマンドを使用するときのファイルの名前や編成に応じて、最終的にどのような orb.yml が出力されるかが決まります。 以下のフォルダー構造を例に考えます。

以上、ドキュメントに書かれているように、ファイル名やディレクトリ構成などの制約が結構強いです。
また、CircleCI2.0で使用していたエイリアス・アンカーを別ファイルに分離して扱うのはサポートされていません。

完成予定のconfig.yml

見ての通りSlackのテンプレートが幅をきかせてます。

onfig.yml
version: 2.1

orbs:
  aws-cli: circleci/[email protected]
  aws-s3: circleci/[email protected]
  slack: circleci/[email protected]

commands:
  install_yarn_version:
    description: Install specific Yarn version
    steps:
      - run:
          command: |
            curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.10
            echo 'export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"' >> $BASH_ENV
          name: Install specific Yarn version
  notify_slack_fail:
    steps:
      - slack/notify:
          custom: |
            {
              "text": "CircleCI job failed.",
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "Job Failed. :red_circle:",
                    "emoji": true
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Job*: ${CIRCLE_JOB}"
                    }
                  ]
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Project*:\\n$CIRCLE_PROJECT_REPONAME"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Branch*:\\n$CIRCLE_BRANCH"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Author*:\\n$CIRCLE_USERNAME"
                    }
                  ],
                  "accessory": {
                    "type": "image",
                    "image_url": "https://assets.brandfolder.com/otz5mn-bw4j2w-6jzqo8/original/circle-logo-badge-black.png",
                    "alt_text": "CircleCI logo"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Mentions*:\\n$SLACK_PARAM_MENTIONS"
                    }
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Job"
                      },
                      "url": "${CIRCLE_BUILD_URL}"
                    }
                  ]
                }
              ]
            }
          event: fail
  notify_slack_pass:
    steps:
      - slack/notify:
          custom: |
            {
              "text": "CircleCI job succeeded!",
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "Job Succeeded. :white_check_mark:",
                    "emoji": true
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Job*: ${CIRCLE_JOB}"
                    }
                  ]
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Project*:\\n$CIRCLE_PROJECT_REPONAME"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Branch*:\\n$CIRCLE_BRANCH"
                            },
                            {
                      "type": "mrkdwn",
                      "text": "*Commit*:\\n$CIRCLE_SHA1"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Author*:\\n$CIRCLE_USERNAME"
                    }
                  ],
                  "accessory": {
                    "type": "image",
                    "image_url": "https://assets.brandfolder.com/otz5mn-bw4j2w-6jzqo8/original/circle-logo-badge-black.png",
                    "alt_text": "CircleCI logo"
                  }
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Job"
                      },
                      "url": "${CIRCLE_BUILD_URL}"
                    }
                  ]
                }
              ]
            }
          event: pass
  restore_node_modules_cache:
    steps:
      - restore_cache:
          keys:
            - node_modules-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}
          name: Restore node_modules cache
  restore_yarn_cache:
    steps:
      - restore_cache:
          keys:
            - yarn-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}
          name: Restore Yarn cache
  run_yarn_install:
    description: Install dependencies
    steps:
      - run:
          command: yarn install --frozen-lockfile
          name: Install dependencies
  save_node_modules_cache:
    description: Save node_modules cache
    steps:
      - save_cache:
          key: node_modules-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}
          name: Save node_modules cache
          paths:
            - node_modules
  save_yarn_cache:
    description: Save Yarn cache
    steps:
      - save_cache:
          key: yarn-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}
          name: Save Yarn cache
          paths:
            - ~/.cache/yarn

executors:
  default:
    docker:
      - environment:
          TZ: Asia/Tokyo
        image: circleci/node:14.15.5
    working_directory: ~/repo

jobs:
  deploy_dev:
    executor: default
    steps:
      - checkout
      - attach_workspace:
          at: .
      - aws-s3/sync:
          arguments: --delete
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-region: AWS_DEFAULT_REGION
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          from: ./dist/
          to: s3://dev.7890987.xyz
      - aws-cli/setup:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-region: AWS_DEFAULT_REGION
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          profile-name: default
      - run:
          command: |
            aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths '/*'
          name: Restore CloudFront cache
      - notify_slack_fail
      - notify_slack_pass
  prepare:
    executor: default
    steps:
      - checkout
      - install_yarn_version
      - restore_yarn_cache
      - restore_node_modules_cache
      - run_yarn_install
      - save_yarn_cache
      - save_node_modules_cache
      - run:
          command: yarn build
          name: Build
      - persist_to_workspace:
          paths:
            - node_modules
            - dist
          root: .
      - notify_slack_fail
  slack:
    executor: default
    steps:
      - run:
          command: echo test
          name: Send Notification to Slack
      - notify_slack_pass

workflows:
  version: 2
  deploy_dev:
    jobs:
      - prepare:
          filters:
            branches:
              only:
                - develop
      - deploy_dev:
          filters:
            branches:
              only:
                - develop
          requires:
            - prepare
  release:
    jobs:
      - prepare:
          filters:
            branches:
              only:
                - main
            tags:
              only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
      - slack:
          filters:
            branches:
              only:
                - main
            tags:
              only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
          requires:
            - prepare

ディレクトリ構成とマッピング

.circleciディレクトリとは別に分割したファイルだけ管理するcircleciディレクトリを作成しています。

ディレクト構成
$ tree
.
└── circleci
    ├── @orb.yml
    ├── commands
    │   ├── @commands.yml
    │   └── @slack.yml
    ├── executors
    │   └── default.yml
    ├── jobs
    │   ├── deploy_dev.yml
    │   ├── deploy_prod.yml
    │   └── prepare.yml
    └── workflows
        ├── @workflows.yml
        ├── develop.yml
        └── release.yml

上記のディレクトリ構成の場合、以下のようにマッピングされます。

@ で始まるファイルの内容は、その親フォルダーのレベルにマージされます。

マッピング
# ここに @orb.yml の内容が表示されます
commands:
    # ここに @commands.yml の内容が表示されます
    # ここに @slack.yml の内容が表示されます
executors:
  default: # この default フォルダーは default.ymlに @ が付いていない為です。
    # ここに default.yml の内容が表示されます
jobs:
  deploy_dev:
    # ここに deploy_dev.yml の内容が表示されます
  deploy_prod:
    # ここに deploy_prod.yml の内容が表示されます
  prepare:
    # ここに prepare.yml の内容が表示されます
workflows:
    # ここに @workflows.yml の内容が表示されます
  develop:
    # ここに develop.yml の内容が表示されます
  release:
    # ここに release.yml の内容が表示されます

ファイルの中身

circleci/@orb.yml
version: 2.1

orbs:
  aws-cli: circleci/[email protected]
  aws-s3: circleci/[email protected]
  slack: circleci/[email protected]


circleciのバージョンとorbの宣言のみにしてます。

circleci/commands/@commands.yml
install_yarn_version:
  description: 'Install specific Yarn version'
  steps:
    - run:
        name: Install specific Yarn version
        command: |
          curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.10
          echo 'export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"' >> $BASH_ENV

restore_yarn_cache:
  steps:
    - restore_cache:
        name: Restore Yarn cache
        keys:
          - yarn-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}

save_yarn_cache:
  description: 'Save Yarn cache'
  steps:
    - save_cache:
        name: Save Yarn cache
        key: yarn-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}
        paths:
          - ~/.cache/yarn

run_yarn_install:
  description: 'Install dependencies'
  steps:
    - run:
        name: Install dependencies
        command: yarn install --frozen-lockfile

restore_node_modules_cache:
  steps:
    - restore_cache:
        name: Restore node_modules cache
        keys:
          - node_modules-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}

save_node_modules_cache:
  description: 'Save node_modules cache'
  steps:
    - save_cache:
        name: Save node_modules cache
        key: node_modules-{{ .Branch }}-packages-{{ checksum "yarn.lock" }}
        paths:
          - node_modules


yarnのインストールやキャッシュ周りのコマンドを集約してます。

circleci/commands/@slack.yml
notify_slack_pass:
  steps:
    - slack/notify:
        event: pass
        custom: |
          {
            "text": "CircleCI job succeeded!",
            "blocks": [
              {
                "type": "header",
                "text": {
                  "type": "plain_text",
                  "text": "Job Succeeded. :white_check_mark:",
                  "emoji": true
                }
              },
              {
                "type": "section",
                "fields": [
                  {
                    "type": "mrkdwn",
                    "text": "*Job*: ${CIRCLE_JOB}"
                  }
                ]
              },
              {
                "type": "section",
                "fields": [
                  {
                    "type": "mrkdwn",
                    "text": "*Project*:\\n$CIRCLE_PROJECT_REPONAME"
                  },
                  {
                    "type": "mrkdwn",
                    "text": "*Branch*:\\n$CIRCLE_BRANCH"
                          },
                          {
                    "type": "mrkdwn",
                    "text": "*Commit*:\\n$CIRCLE_SHA1"
                  },
                  {
                    "type": "mrkdwn",
                    "text": "*Author*:\\n$CIRCLE_USERNAME"
                  }
                ],
                "accessory": {
                  "type": "image",
                  "image_url": "https://assets.brandfolder.com/otz5mn-bw4j2w-6jzqo8/original/circle-logo-badge-black.png",
                  "alt_text": "CircleCI logo"
                }
              },
              {
                "type": "actions",
                "elements": [
                  {
                    "type": "button",
                    "text": {
                      "type": "plain_text",
                      "text": "View Job"
                    },
                    "url": "${CIRCLE_BUILD_URL}"
                  }
                ]
              }
            ]
          }

notify_slack_fail:
  steps:
    - slack/notify:
        event: fail
        custom: |
          {
            "text": "CircleCI job failed.",
            "blocks": [
              {
                "type": "header",
                "text": {
                  "type": "plain_text",
                  "text": "Job Failed. :red_circle:",
                  "emoji": true
                }
              },
              {
                "type": "section",
                "fields": [
                  {
                    "type": "mrkdwn",
                    "text": "*Job*: ${CIRCLE_JOB}"
                  }
                ]
              },
              {
                "type": "section",
                "fields": [
                  {
                    "type": "mrkdwn",
                    "text": "*Project*:\\n$CIRCLE_PROJECT_REPONAME"
                  },
                  {
                    "type": "mrkdwn",
                    "text": "*Branch*:\\n$CIRCLE_BRANCH"
                  },
                  {
                    "type": "mrkdwn",
                    "text": "*Author*:\\n$CIRCLE_USERNAME"
                  }
                ],
                "accessory": {
                  "type": "image",
                  "image_url": "https://assets.brandfolder.com/otz5mn-bw4j2w-6jzqo8/original/circle-logo-badge-black.png",
                  "alt_text": "CircleCI logo"
                }
              },
              {
                "type": "section",
                "fields": [
                  {
                    "type": "mrkdwn",
                    "text": "*Mentions*:\\n$SLACK_PARAM_MENTIONS"
                  }
                ]
              },
              {
                "type": "actions",
                "elements": [
                  {
                    "type": "button",
                    "text": {
                      "type": "plain_text",
                      "text": "View Job"
                    },
                    "url": "${CIRCLE_BUILD_URL}"
                  }
                ]
              }
            ]
          }


Slack周りをここに集約してます。これが一番邪魔だったんでスッキリしました。

circleci/executors/default.yml
working_directory: ~/repo
docker:
  - image: circleci/node:14.15.5
    environment:
      TZ: Asia/Tokyo


今はdefaultだけですが、環境毎のExecutorファイルをこのディレクトリで管理。

circleci/jobs/prepare.yml
executor: default
steps:
  - checkout
  - install_yarn_version
  - restore_yarn_cache
  - restore_node_modules_cache
  - run_yarn_install
  - save_yarn_cache
  - save_node_modules_cache
  - run:
      name: Build
      command: yarn build
  - persist_to_workspace:
      root: .
      paths:
        - node_modules
        - dist
  - notify_slack_fail

circleci/jobs/deploy_dev.ymlとcircleci/jobs/deploy_prod.yml
executor: default
steps:
  - checkout
  - attach_workspace:
      at: .
  - aws-s3/sync:
      from: ./dist/
      to: "s3://buecketname"
      aws-access-key-id: AWS_ACCESS_KEY_ID
      aws-secret-access-key: AWS_SECRET_ACCESS_KEY
      aws-region: AWS_DEFAULT_REGION
      arguments: --delete
  - aws-cli/setup:
      profile-name: default
      aws-access-key-id: AWS_ACCESS_KEY_ID
      aws-secret-access-key: AWS_SECRET_ACCESS_KEY
      aws-region: AWS_DEFAULT_REGION
  - run:
      name: Restore CloudFront cache
      command: |
        aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths '/*'
  - notify_slack_fail
  - notify_slack_pass

circleci/workflows/@workflows.yml
version: 2


Workflowのバージョンだけの為のファイル

circleci/workflows/develop.yml
jobs:
  - prepare:
      filters:
        branches:
          only:
            - develop
  - deploy_dev:
      requires:
        - prepare
      filters:
        branches:
          only:
            - develop

circleci/workflows/release.yml
jobs:
  - prepare:
      filters:
        tags:
          only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
        branches:
          only:
            - main
  - deploy_prod:
      requires:
        - prepare
      filters:
        tags:
          only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
        branches:
          only:
            - main

config.ymlの作成

以下のコマンドを実行します。

# circleci config pack <ディレクトリ名>
circleci config pack circleci

すると統合された結果が出力されます。
ただ、出力される内容はお世辞にも綺麗ではないです。。。
なのでリダイレクトを使うなどしてconfig.ymlに直接上書きしてしまうのが手っ取り早いかと思います。

circleci config pack circleci >| .circleci/config.yml

また、config.ymlを作成後はconfig.ymlのバリデーションをお勧めします。
自分の場合、インデント周りでエラーがでまくりでした。

circleci validate