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

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

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

サンプルリポジトリ

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

CircleCI CLI のインストール

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

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

Copied!
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
Copied!
version: 2.1

orbs:
  aws-cli: circleci/aws-cli@1.3.1
  aws-s3: circleci/aws-s3@2.0.0
  slack: circleci/slack@4.3.1

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 ディレクトリを作成しています。

ディレクト構成
Copied!
$ 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

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

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

Copied!
# ここに @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
Copied!
version: 2.1

orbs:
  aws-cli: circleci/aws-cli@1.3.1
  aws-s3: circleci/aws-s3@2.0.0
  slack: circleci/slack@4.3.1

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

circleci/commands/@commands.yml
Copied!
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
Copied!
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
Copied!
working_directory: ~/repo
docker:
  - image: circleci/node:14.15.5
    environment:
      TZ: Asia/Tokyo

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

circleci/jobs/prepare.yml
Copied!
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
Copied!
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
Copied!
version: 2

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

circleci/workflows/develop.yml
Copied!
jobs:
  - prepare:
      filters:
        branches:
          only:
            - develop
  - deploy_dev:
      requires:
        - prepare
      filters:
        branches:
          only:
            - develop
circleci/workflows/release.yml
Copied!
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 の作成

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

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

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

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

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

Copied!
circleci validate