🙆

CircleCI : Config Policy Management ~ Organization 単位で実行可能パイプラインの制御

に公開

今日はConfig Policy Managementという機能を見ていきます。
https://circleci.com/docs/guides/config-policies/config-policy-management-overview/
(本機能はFreeバージョンでは利用できずScale以上のプランが必要です)
https://circleci.com/ja/pricing/

この機能はCircleCIのOrganization単位で実行可能なパイプラインのルールを制御するものです。ルールというのは例えばインストールを行うミドルウェアのバージョンやOSの設定など指定することで、パイプラインが許可されていないバージョンを利用しようとした場合、アラートを出力(soft_fail)するかパイプラインを停止(hard_fail)させることが可能です。

Open Policy Agent(OPA)と Rego言語

このポリシーによるパイプライン制御の挙動はOPA (Open Policy Agent)により行われます。OPAに対して制御ルールを記載するものが、Regoと呼ばれるポリシーを定義するドキュメント言語です。
https://www.openpolicyagent.org/docs/policy-language

OPAはCNCFのオープンソース・ポリシーエンジンで、様々なシステム(Kubernetes、CI/CD、APIゲートウェイなど)にポリシーを適用可能となり、CircleCI専用エージェントではありません。Regoはそのシステム向けの宣言型言語で、JSONなどの構造化データに対して強力な条件検査・制御が行えます。

package で名前空間(モジュール)を定義し、変数やルール(rules)を記述します。例えば以下の様に宣言を行うとパイプラインの中で同じ変数に対して異なる値が指定されるとエラーが発生します。

package example
pi := 3.14
rect := {"width":2, "height":4}

さっそくやってみる

OPAやRegoについては機会があればまた詳細に触れたいと思いますが今日は全容を理解するためにシンプルなパイプライン制御を行ってみます。
パイプラインがNodeを利用する場合、必ず20以上でなければパイプラインを強制停止させます。

1. CircleCI CLIの設定

この機能はCirclCI CLIから操作を行う必要があるため以下の記事を参考にCLI環境を整備します。
https://zenn.dev/kameoncloud/articles/707a448101e5c0

2. Policy機能の有効化

以下のコマンドでまずはPolicyによるパイプライン制御の機能を有効化します。

circleci policy settings --enabled=true --owner-id <your-organization-ID>

trueがレスポンスで戻れば成功です。

{
  "enabled": true
}

3. config.yml の入れかえとパイプライン実行

version: 2.1
jobs:
  build:
    docker:
      - image: cimg/node:18.17
    steps:
      - checkout
      - run: node -v
workflows:
  main:
    jobs:
      - build


Nodeバージョン18.17がパイプラインにより起動されるデプロイ環境にインストールされていることを確認します。

4. policy.regoの作成 と 適用

ポリシードキュメントはかならずディレクトリ単位でCircleCIに登録する必要があります。このためディレクトリを作成します。

mkdir policy

つぎにpolicy.regoを作成します。

policy.rego
package org
import future.keywords

# ポリシー名(必須)
policy_name["node_image_policy"]

# このルールを有効化し、違反時は HARD_FAIL
enable_hard["enforce_node20_min"]

# Docker イメージが cimg/node の場合、バージョンを確認
enforce_node20_min[reason] {
  some job_name, job in input.jobs
  some container in job.docker
  startswith(container.image, "cimg/node:")

  parts := split(container.image, ":")
  version := parts[1]
  major := to_number(split(version, ".")[0])

  major < 20
  reason := sprintf(
    "job %q uses %q (Node < 20 is not allowed)",
    [job_name, container.image],
  )
}

config.ymlで指定しているNode環境用Dockerイメージのバージョンを確認しています。これによりバージョン20未満のNodeがインストールされることを防止します。
次にpolicy.regoを適応します。

circleci policy push ./policy/ --owner-id <orgid>

<orgid>は皆さんの環境ごとに入れ替えてください。以下の様にSuccessfullyが表示されれば適応完了です。

The following changes are going to be made: {
  "created": [
    "node_image_policy"
  ]
}

Do you wish to continue? (y/N) Y

Policy Bundle Pushed Successfully

diff: {
  "created": [
    "node_image_policy"
  ]
}

コンソールでも以下の様に確認が行えます。

5. パイプラインの実行

最後にパイプラインを実行します。

policy evaluation failed: enforce_node20_min: job "build" uses "cimg/node:18.17" (Node < 20 is not allowed)
パイプライン定義がポリシーに違反しているため、エラーにより処理が停止されています。

6. soft fail

policy.regoenable_hardenable_ruleに変更して再度パイプラインを実行します。

この通りアラートが表示されながらもパイプラインは実行されます。

7. ログの確認

circleci policy logs --owner-id <orgid>

を実行すると以下の通りアラートの一覧が出力されます。

[
  {
    "created_at": "2025-09-07T06:05:12.867027Z",
    "decision": {
      "enabled_rules": [
        "enforce_node20_min"
      ],
      "hard_failures": [
        {
          "reason": "job \"build\" uses \"cimg/node:18.17\" (Node \u003c 20 is not allowed)",
          "rule": "enforce_node20_min"
        }
      ],
      "status": "HARD_FAIL"
    },
    "id": "849c1838-066c-4f8f-92e0-72ba8f95145f",
    "metadata": {
      "build_number": 66,
      "project_id": "5cc2323a-511c-4945-a439-e07e04a4530a",
      "vcs": {
        "branch": "main",
        "origin_repository_url": "https://github.com/h-kameda-sakura/apitest",
        "target_repository_url": "https://github.com/h-kameda-sakura/apitest"
      }
    },
    "policies": {
      "node_image_policy": "6bc8d9c7981499030642475a39130db31cd714717fd5049f18e05c6118c2051b3a44ba953754df8082acf0a7ab4cf3a07f49cdaee12737a45184016a9c2d4d94"   
    },
    "time_taken_ms": 1
  },
  {
    "created_at": "2025-09-07T06:43:50.865578Z",
    "decision": {
      "enabled_rules": [
        "enforce_node20_min"
      ],
      "soft_failures": [
        {
          "reason": "job \"build\" uses \"cimg/node:18.17\" (Node \u003c 20 is not allowed)",
          "rule": "enforce_node20_min"
        }
      ],
      "status": "SOFT_FAIL"
    },
    "id": "a840382a-8337-437b-8b7d-00009db7e10d",
    "metadata": {
      "build_number": 70,
      "project_id": "5cc2323a-511c-4945-a439-e07e04a4530a",
      "vcs": {
        "branch": "main",
        "origin_repository_url": "https://github.com/h-kameda-sakura/apitest",
        "target_repository_url": "https://github.com/h-kameda-sakura/apitest"
      }
    },
    "policies": {
      "node_image_policy": "90ddeff8deb8ac3f025aebacc20c01ef8b6b27dd9ebaa9f6e679f2663461e00e85c5b313c513d6974d345d1a88d6bf82316a79edcc3f5c36f7ca5580696520f5"   
    },
    "time_taken_ms": 1
  }
]

Discussion