堅牢で、管理しやすく、再利用可能な機能を開発することは、私のような CI/CD エンジニアにとって重要な仕事です。最近のブログ記事で、パイプライン設定ファイル内にパイプライン変数を導入して実装し、再利用可能なパイプライン設定ファイルを管理する方法について書きました。そのチュートリアルで説明したように、パイプライン変数と CircleCI Orbs はこのプロセスをいくらか柔軟にしますが、それでも多少の制限があります。パイプライン設定ファイルの性質上、特定のビルド プロセスに適したソリューションが欲しい場合には、”回避策” を要することがあります。たとえば、コミット前フックでスクリプトを実行し、コミット前に設定ファイルを生成するという方法が挙げられます。また、ジョブを使用し、API 経由でパイプライン実行をトリガーして、パイプライン パラメーターを設定するという方法も取られていました。これらでうまくいく場合もありますが、非効率的で複雑すぎたり、馴染みのない回避策が必要になったり、簡単に解決できないエッジ ケースが生じたりするという問題もはらんでいました。

CircleCI はこの問題を解決するため、ダイナミック コンフィグをリリースいたしました。ダイナミック コンフィグは、パイプライン設定ファイルに “動的さ” をネイティブに挿入できるようにするもので、スクリプトを使用して別の設定ファイルを実行することを可能にします。これは柔軟性を高めるための大きな一歩です。設定ファイルのどのセクションをテストと検証の対象にするのかを、カスタマイズできるようになったからです。また、複数の config.yml ファイルを 1 つのコード リポジトリに保持し、プライマリ config.yml ファイルを選んで実行することも可能です。ダイナミック コンフィグは強力な機能を幅広く備えており、多岐にわたる動的なパイプラインのワークロードを簡単に指定して実行できます。

このブログ記事では、設定ファイル フォルダーの外に設定ファイルを用意しておくことで、ダイナミック コンフィグを実装する方法について説明します。

セットアップ ワークフローによるダイナミック コンフィグの使用を開始する

ここでは、例としてこちらのコード リポジトリとコード (英語)を使用します。プロジェクトをフォークするか、お使いのバージョンのブランチを実行して手順を進める場合は、プロジェクトをインポートしてください。サンプルのフォークまたはインポートが完了したら、CircleCI にプロジェクトを追加し、セットアップ ワークフローによるダイナミック コンフィグを有効にします。以下のように簡単な手順で有効化することができます。

  1. CircleCI アプリケーションの [Projects (プロジェクト)] ダッシュボードに移動します。
  2. 使用するプロジェクトを選択します。
  3. 右上隅の [Project Settings (プロジェクト設定)] を選択します。
  4. 左側のパネルで [Advanced (詳細設定)] を選択します。
  5. 画面下部にある [Enable dynamic config using setup workflows (セットアップ ワークフローによるダイナミック コンフィグを有効にする)] をオンにします。

これで、プロジェクトでセットアップ ワークフローを使用したダイナミック コンフィグを実行できるようになりました。

手順を開始する前に、.circleci/ ディレクトリ内に config.yml ファイルを新しく作成する必要があります。このファイル内の構文として、値が true となる setup: キーを指定します。これが、構文の中で定義可能な各種コマンドを実行またはトリガーするエントリ ポイントになります。コマンドには、パラメーター値を渡す処理や、デフォルトの .circleci/ ディレクトリ外に存在する別の config.yml パイプラインをトリガーする処理などがあります。

シェル スクリプトから config.yml ファイルを生成する

次に、スクリプトを使用して、ダイナミック コンフィグによって別の設定ファイルを実行します。

基本的に、このパターンでは、.circleci/config.yml ファイルをトリガーして scripts/generate-pipeline-config スクリプトを実行します。このスクリプトは、後続のステップで処理する新しい設定ファイルを、configs/ ディレクトリ内に生成します。

# このファイルは、ダイナミック コンフィグにより、スクリプトを使用して別の設定ファイルを実行する方法を示します。

version: 2.1
setup: true
orbs:
  continuation: circleci/continuation@0.1.0
jobs:
  generate-config:
    executor: continuation/default
    steps:
      - checkout
      - run: 
          name: パイプラインの generated_config.yml ファイルの生成
          command: |
            # generate-pipeline-config スクリプトは、1) Terraform のバージョンと 2) DigitalOcean CLI のバージョンという 2 つの引数を持ちます

            ./scripts/generate-pipeline-config "0.14.5" "1.59.0"
      - continuation/continue:
          parameters: '{}'
          configuration_path: configs/generated_config.yml
workflows:
  setup-workflow:
    jobs:
      - generate-config

このサンプル コードでは、setup: キーを true に設定しています。これにより設定ファイルが、セットアップ ワークフローを使用するダイナミック コンフィグ ファイルになります。continuation: circleci/continuation@0.1.0 orb では、プライマリ設定ファイルの実行をオーケストレーションできるようにしています。そして、generate-config: ジョブの steps リストで、次のコマンドを実行しています。

./scripts/generate-pipeline-config "0.14.5" "1.59.0"

このコマンドは、generate-pipeline-config という bash スクリプトを実行します。このスクリプトが、以下 2 つの引数を必要とする新しいパイプライン設定ファイルを生成します。

  • 引数 1 はインストールする Terraform のバージョン
  • 引数 2 はインストールする DigitalOcean CLI のバージョン

これらの引数が必要な理由は、生成される設定ファイルに、Terraform と DigitalOcean CLI のツールを使用してイメージを作成し DigitalOcean Kubernetes クラスタにデプロイするジョブがあるためです。generate-pipeline-config スクリプトは、実行されると、configs/ ディレクトリと、ディレクトリ内の新しい設定ファイル generated_config.yml を作成します。その後、2 つの引数を使用して、バージョンの値を適切な場所に動的に挿入します。generate-pipeline-config ファイルの詳細は後ほど説明しますが、まずはセットアップ ワークフロー設定ファイルの continuation/continue: 要素について見ていきましょう。

parameters: キー値は、パイプライン設定ファイルのパラメーターを指定します。このパラメーターは、configuration_path: 要素で定義する対象の設定ファイルに渡すことができます。今回、生成する設定ファイルではパイプライン変数を定義しないため、parameters: 要素には {} を使用して空の値を割り当てています。最後に、configuration_path: 要素で、セットアップ ワークフロー パイプラインが次に実行する設定ファイル (ここでは configs/generated_config.yml) を指定しています。このファイルは、前の “パイプラインの generated_config.yml ファイルの生成” 実行ステップで生成されたものです。

今度は generate-pipeline-config の内容を掘り下げて、その仕組みを詳しく理解していきましょう。

generate-pipeline-config ファイルの内容

セットアップ ワークフローを使用したダイナミック コンフィグでは、generate-pipeline-config スクリプトを実行しました。このスクリプトは、後続のパイプラインとして実行する configs/generated_config.yml ファイルを生成します。

#!/bin/bash 
set -o pipefail

TF_VERSION=$1 # インストールする Terraform CLI のバージョン
DOCTL_VERSION=$2 # インストールする Digital Ocean CLI のバージョン

mkdir configs/
cat << EOF > configs/generated_config.yml
version: 2.1
orbs:
  docker: circleci/docker@1.5.0
  node: circleci/node@4.2.0
  snyk: snyk/snyk@0.0.12
  terraform: circleci/terraform@2.0.0  
jobs:
  scan_app:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - node/install-packages:
          override-ci-command: npm install
          cache-path: ~/project/node_modules 
      - snyk/scan:
          fail-on-issues: false
          monitor-on-build: false
  scan_push_docker_image:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - setup_remote_docker
      - docker/check
      - docker/build:
          image: \$DOCKER_LOGIN/\$CIRCLE_PROJECT_REPONAME
          tag: 0.1.<< pipeline.number >>             
      - snyk/scan:
          fail-on-issues: false
          monitor-on-build: false
          target-file: "Dockerfile"
          docker-image-name: \$DOCKER_LOGIN/\$IMAGE_NAME:0.1.<< pipeline.number >>
          project: \${CIRCLE_PROJECT_REPONAME}/\${CIRCLE_BRANCH}-app
      - docker/push:
          image: \$DOCKER_LOGIN/\$CIRCLE_PROJECT_REPONAME
          tag: 0.1.<< pipeline.number >>
  run_tests:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - node/install-packages:
          override-ci-command: npm install
          cache-path: ~/project/node_modules
      - run:
          name: 単体テストの実行
          command: |
            ./node_modules/mocha/bin/mocha test/ --reporter mochawesome --reporter-options reportDir=test-results,reportFilename=test-results
      - store_test_results:
          path: test-results
      - store_artifacts:
          path: test-results
  create_do_k8s_cluster:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: ローカルでの .terraformrc ファイルの作成
          command: echo "credentials \"app.terraform.io\" {token = \"\$TERRAFORM_TOKEN\"}" > \$HOME/.terraformrc   
      - terraform/install:
          terraform_version: $TF_VERSION
          arch: "amd64"
          os: "linux"
      - terraform/init:
          path: ./terraform/do_create_k8s
      - run:
          name: DigitalOcean での K8s クラスタの作成
          command: |
            export CLUSTER_NAME=\${CIRCLE_PROJECT_REPONAME}
            export TAG=0.1.<< pipeline.number >>
            curl -sL https://github.com/digitalocean/doctl/releases/download/v$DOCTL_VERSION/doctl-$DOCTL_VERSION-linux-amd64.tar.gz | tar -xzv
            sudo mv doctl /usr/local/bin
            export DO_K8S_SLUG_VER="\$(doctl kubernetes options versions -o json -t \$DIGITAL_OCEAN_TOKEN | jq -r '.[0] | .slug')"
            cd terraform/do_create_k8s
            terraform init
            terraform apply -var do_token=\$DIGITAL_OCEAN_TOKEN -var cluster_name=\$CLUSTER_NAME -var do_k8s_slug_ver=\$DO_K8S_SLUG_VER -auto-approve
  deploy_to_k8s:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: ローカルでの .terraformrc ファイルの作成
          command: echo "credentials \"app.terraform.io\" {token = \"\$TERRAFORM_TOKEN\"}" > \$HOME/.terraformrc
      - terraform/install:
          terraform_version: $TF_VERSION
          arch: "amd64"
          os: "linux"
      - run:
          name: DigitalOcean の K8s へのアプリのデプロイ
          command: |
            export CLUSTER_NAME=\${CIRCLE_PROJECT_REPONAME}
            export TAG=0.1.<< pipeline.number >>
            export DOCKER_IMAGE="\${DOCKER_LOGIN}/\${CIRCLE_PROJECT_REPONAME}:\$TAG"
            curl -sL https://github.com/digitalocean/doctl/releases/download/v$DOCTL_VERSION/doctl-$DOCTL_VERSION-linux-amd64.tar.gz | tar -xzv
            sudo mv doctl /usr/local/bin          
            cd terraform/do_k8s_deploy_app
            doctl auth init -t \$DIGITAL_OCEAN_TOKEN
            doctl kubernetes cluster kubeconfig save \$CLUSTER_NAME
            terraform init
            terraform apply -var do_token=\$DIGITAL_OCEAN_TOKEN -var cluster_name=\$CLUSTER_NAME -var docker_image=\$DOCKER_IMAGE -auto-approve
            # ロード バランサーのパブリック IP アドレスを保存
            export ENDPOINT="\$(terraform output lb_public_ip)"
            mkdir -p /tmp/do_k8s/
            echo 'export ENDPOINT='\${ENDPOINT} > /tmp/do_k8s/dok8s-endpoint
      - persist_to_workspace:
          root: /tmp/do_k8s
          paths:
            - "*"
  smoketest_k8s_deployment:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - attach_workspace:
          at: /tmp/do_k8s/
      - run:
          name: K8s アプリ デプロイのスモーク テスト
          command: |
            source /tmp/do_k8s/dok8s-endpoint
            ./test/smoke_test \$ENDPOINT              
  destroy_k8s_cluster:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: ローカルでの .terraformrc ファイルの作成
          command: echo "credentials \"app.terraform.io\" {token = \"\$TERRAFORM_TOKEN\"}" > \$HOME/.terraformrc && cat \$HOME/.terraformrc
      - terraform/install:
          terraform_version: $TF_VERSION
          arch: "amd64"
          os: "linux"
      - terraform/init:
          path: ./terraform/do_k8s_deploy_app
      - run:
          name: アプリ デプロイの破棄 
          command: |
            export CLUSTER_NAME=\${CIRCLE_PROJECT_REPONAME}
            export TAG=0.1.<< pipeline.number >>
            export DOCKER_IMAGE="\${DOCKER_LOGIN}/\${CIRCLE_PROJECT_REPONAME}:\$TAG"
            curl -sL https://github.com/digitalocean/doctl/releases/download/v$DOCTL_VERSION/doctl-$DOCTL_VERSION-linux-amd64.tar.gz | tar -xzv
            sudo mv doctl /usr/local/bin
            cd terraform/do_k8s_deploy_app/
            doctl auth init -t \$DIGITAL_OCEAN_TOKEN
            doctl kubernetes cluster kubeconfig save \$CLUSTER_NAME
            terraform init
            terraform destroy -var do_token=\$DIGITAL_OCEAN_TOKEN -var cluster_name=\$CLUSTER_NAME -var docker_image=\$DOCKER_IMAGE -auto-approve
      - terraform/init:
          path: ./terraform/do_create_k8s/ 
      - run:
          name: K8s クラスタの破棄
          command: |
            export CLUSTER_NAME=\${CIRCLE_PROJECT_REPONAME}
            export TAG=0.1.<< pipeline.number >>
            cd terraform/do_create_k8s/
            terraform init
            terraform destroy -var do_token=\$DIGITAL_OCEAN_TOKEN -var cluster_name=\$CLUSTER_NAME -auto-approve
workflows:
  scan_deploy:
    jobs:
      - scan_app
      - scan_push_docker_image
      - run_tests
      - create_do_k8s_cluster
      - deploy_to_k8s:
          requires:
            - create_do_k8s_cluster
            - scan_push_docker_image
      - smoketest_k8s_deployment:
          requires:
            - deploy_to_k8s
      - approve_destroy:
          type: approval
          requires:
            - smoketest_k8s_deployment
      - destroy_k8s_cluster:
          requires:
            - approve_destroy
EOF

このサンプルはヒアドキュメント (ファイル内でファイルを定義する方法) を指定する Bash スクリプトであり、パイプライン設定ファイルの構文をスクリプト内で定義しています。またインストールする CLI ツールの各バージョン番号を指定するために、引数の変数として TF_VERSION および DOCTL_VERSION を使用しています。これらの変数は、ヒアドキュメント設定ファイルの構文内で代入され、スクリプトの実行時および最終的な configs/generated_config.yml ファイルの作成時に適用されています。このサンプルでは、セットアップ ワークフローとうまく連携可能なダイナミック コンフィグを、スクリプト言語を使用して簡単かつ継続的に生成する方法について示しています。

このスクリプトで定義しているダイナミック コンフィグは以下のとおりです。

  • Orb を定義および実行する
  • 単体テストを実行する
  • セキュリティ スキャンを実行する
  • Docker イメージをビルドする
  • Terraform を使用して、DigitalOcean で Kubernetes クラスタを新規作成する
  • Terraform を使用して、新しい Kubernetes クラスタにアプリをデプロイする
  • デプロイでバリデーション テストを実行する
  • Kubernetes クラスタを破棄するために承認ステップをトリガーする

このパターンは、ほとんどの言語、フレームワーク、スタックで実行することができます。今回は Bash を選びましたが、Python や JavaScript などの他の言語を使用しても構いません。

まとめ

ダイナミック コンフィグを利用すれば、独自のソフトウェア開発プロセスを実行するカスタム CI/CD パイプラインを作成でき、開発の柔軟性が高まります。この記事ではダイナミック コンフィグの概念を説明し、1 つのユースケースを紹介しました。もちろんダイナミック コンフィグには他にもたくさんのユースケースとパターンがあり、ソフトウェア開発の選択肢を広げ、ワークロードのオーケストレーションをカスタマイズするのに役立ちます。

別の機会に、特定のファイル セットに行われた変更に基づきワークフローを条件付きで実行する方法について取り上げる予定です。これは特に、マイクロサービスに携わっている開発者の方や、モノレポ (単一のリポジトリ) に格納されているコードを扱う開発者の方に関心を持っていただけるはずです。このようなニーズを抱えるお客様向けに、コードの任意のセクションにバリデーションとテストを行える path-filtering Orb の提供も開始されています。

ダイナミック コンフィグが役立ちそうなユースケースやパターンがありましたら、ぜひお聞かせいただければ幸いです。ご意見、ご感想は Twitter で @circleci 宛てにお寄せください。

最後までお読みいただき、ありがとうございました。