CircleCI Config SDKの学習と感想
ブログをチラ見した感じではAWS CDKのようにプログラミング言語のオブジェクトで定義したものをYAMLに変換するというアプローチっぽい。
ブログやREADMEのサンプルだと非常に簡単なconfig.ymlの例しかないので、複雑なconfig.ymlを表現するためにはどれぐらい大変なのかを実験してみる。
実験用に書いたコードと感想を雑に書いていくが、気が向いたら清書してブログにすると思う。
実験用のリポジトリ https://github.com/Kesin11/circleci-config-sdk-sandbox
TODO
- 基本
- reusable
- parameter
- condition
- orbs
- 実践 https://github.com/Kesin11/my-circleci-docker-build-sandbox/blob/master/.circleci/config.yml の一部を適当に
- 実践 手動トリガー
まず最初はサンプルをちょっとだけ変更して2つのジョブを持つワークフロー
const CircleCI = require("@circleci/circleci-config-sdk")
const main = async () => {
const config = new CircleCI.Config()
const workflow = new CircleCI.Workflow('build')
config.addWorkflow(workflow)
const nodeExecutor = new CircleCI.executors.DockerExecutor('cimg/node:lts', 'small')
const buildJob = new CircleCI.Job('build', nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
name: 'install',
command: 'npm ci',
}),
new CircleCI.commands.Run({
name: 'build',
command: 'npm run build',
})
])
config.addJob(buildJob)
workflow.addJob(buildJob)
const testJob = new CircleCI.Job('test', nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
name: 'install',
command: 'npm ci',
}),
new CircleCI.commands.Run({
name: 'test',
command: 'npm run test',
})
])
config.addJob(testJob)
workflow.addJob(testJob)
console.log(config.stringify())
console.warn(config.stringify())
}
main()
version: 2.1
setup: false
jobs:
build:
docker:
- &a1
image: cimg/node:lts
resource_class: small
steps:
- checkout
- run:
name: install
command: npm ci
- run:
name: build
command: npm run build
test:
docker:
- *a1
resource_class: small
steps:
- checkout
- run:
name: install
command: npm ci
- run:
name: test
command: npm run test
workflows:
build:
jobs:
- build
- test
js上は nodeExecutor
を使いまわしているだけだが、YAMLにしたときに自動的にアンカーに変換されて2重定義されないようになっているのが偉い
Reusable
という気になる文字列が見えたので試す。ここからブログにもREADMEにも載っていないので手探りで試していく。
const CircleCI = require("@circleci/circleci-config-sdk")
const main = async () => {
const config = new CircleCI.Config()
const workflow = new CircleCI.Workflow('build')
config.addWorkflow(workflow)
// Define executor
const nodeExecutor = new CircleCI.executors.DockerExecutor('cimg/node:lts', 'small')
const reusableExecutor = new CircleCI.reusable.ReusableExecutor('node', nodeExecutor)
config.addReusableExecutor(reusableExecutor)
// Define command
const reusableInstallCommand = new CircleCI.reusable.ReusableCommand('install', [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
name: 'Install',
command: 'npm ci',
}),
])
config.addReusableCommand(reusableInstallCommand)
// Define reusing executor and command
const reusedExecutor = new CircleCI.reusable.ReusedExecutor(reusableExecutor)
const reusedInstallCommand = new CircleCI.reusable.ReusedCommand(reusableInstallCommand)
// build job
const buildJob = new CircleCI.Job('build', reusedExecutor, [
reusedInstallCommand,
new CircleCI.commands.Run({
name: 'build',
command: 'npm run build',
})
])
config.addJob(buildJob)
workflow.addJob(buildJob)
// test job
const testJob = new CircleCI.Job('test', reusedExecutor, [
reusedInstallCommand,
new CircleCI.commands.Run({
name: 'test',
command: 'npm run test',
})
])
config.addJob(testJob)
workflow.addJob(testJob)
// Output
console.log(config.stringify())
console.warn(config.stringify())
}
main()
version: 2.1
setup: false
commands:
install:
steps:
- checkout
- run:
name: Install
command: npm ci
executors:
node:
docker:
- image: cimg/node:lts
resource_class: small
jobs:
build:
executor:
name: node
steps:
- install
- run:
name: build
command: npm run build
test:
executor:
name: node
steps:
- install
- run:
name: test
command: npm run test
workflows:
build:
jobs:
- build
- test
ReusableExecutor
と ReusableCommand
を定義すると executors
と commands
が作られる。そのために config.addReusable{Executor, Comamnd}
も必要なので注意。
jobの中でReusableなものを使うためには ReusedExecutor
, ReusedCommand
を使う必要がある。これによって生成されるyamlのコードはアンカーは使わず、config 2.1からのローカルのorbsという扱いで参照させるコードになる。
ちなみにこの時点で既にブログやドキュメントを見ても使い方は全く書かれていないため、テストコードから使い方を理解した
引き続きReusableからExecutorとCommandの一部をパラメータ化したパターン
Executorの cimg/node
のタグをパラメータ化して外から渡せるようにしたのと、Commandは npm run
で実行するタスクをパラメータ指定できるように汎用化してjobから呼び出している。
そもそもyamlで書いても記述量が多く面倒な部類だが、jsでもyamlの複雑さに比例してコード量もだいぶ増える印象。
const CircleCI = require("@circleci/circleci-config-sdk")
const main = async () => {
const config = new CircleCI.Config()
const workflow = new CircleCI.Workflow('build')
config.addWorkflow(workflow)
// Define executor
const nodeExecutor = new CircleCI.executors.DockerExecutor('cimg/node:<< parameters.tag >>', 'small')
const reusableExecutor = new CircleCI.reusable.ReusableExecutor(
'node',
nodeExecutor,
new CircleCI.parameters.CustomParametersList([
new CircleCI.parameters.CustomParameter('tag', 'string', 'lts')
])
)
config.addReusableExecutor(reusableExecutor)
// Define commands
const reusableInstallCommand = new CircleCI.reusable.ReusableCommand('install', [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
name: 'Install',
command: 'npm ci',
}),
])
config.addReusableCommand(reusableInstallCommand)
const allowedNpmTasks = ['build', 'test']
const reusableNpmCommand = new CircleCI.reusable.ReusableCommand('npm_run', [
new CircleCI.commands.Run({
name: 'npm << parameters.task >>',
command: 'npm run <<parameters.task >>',
}),
],
new CircleCI.parameters.CustomParametersList([
new CircleCI.parameters.CustomEnumParameter('task', allowedNpmTasks)
])
)
config.addReusableCommand(reusableNpmCommand)
// Define reusing executor and command
const reusedExecutor = new CircleCI.reusable.ReusedExecutor(reusableExecutor)
const reusedInstallCommand = new CircleCI.reusable.ReusedCommand(reusableInstallCommand)
// build job
// Using ParameterizedJob for define job parameters
const buildJob = new CircleCI.reusable.ParameterizedJob('build', reusedExecutor)
buildJob
.addStep(reusedInstallCommand)
.addStep(new CircleCI.reusable.ReusedCommand(reusableNpmCommand, { task: 'build' }))
.defineParameter('tag', 'string')
config.addJob(buildJob)
workflow.addJob(buildJob, { 'tag': 'lts' })
// test job
const testJob = new CircleCI.reusable.ParameterizedJob('test', reusedExecutor)
testJob
.addStep(reusedInstallCommand)
.addStep(new CircleCI.reusable.ReusedCommand(reusableNpmCommand, { task: 'test' }))
.defineParameter('tag', 'string')
config.addJob(testJob)
workflow.addJob(testJob, { 'tag': '16' })
// Output
console.log(config.stringify())
console.warn(config.stringify())
}
main()
version: 2.1
setup: false
commands:
install:
steps:
- checkout
- run:
name: Install
command: npm ci
npm_run:
parameters:
task:
type: enum
enum:
- build
- test
steps:
- run:
name: npm << parameters.task >>
command: npm run <<parameters.task >>
executors:
node:
docker:
- image: cimg/node:<< parameters.tag >>
resource_class: small
parameters:
tag:
type: string
default: lts
jobs:
build:
parameters:
tag:
type: string
executor:
name: node
steps:
- install
- npm_run:
task: build
test:
parameters:
tag:
type: string
executor:
name: node
steps:
- install
- npm_run:
task: test
workflows:
build:
jobs:
- build:
tag: lts
- test:
tag: "16"
conditionはstepとworkflowの2種類がある
stepのcondition
SDKにはこれに対応するメソッドやオプションが存在しない??CircleCI.logic.When
や and
, or
などは存在するが、Conditionalのテストを見てもandやorなどの条件式パーツのテストしか存在しなかった
workflowでのwhenはテストが存在するのと、CircleCI.Workflow()
の引数にWhenのオブジェクトを渡せるのでこれは現状でも間違いなく使えそう。
orbsも例によってドキュメントが一切無いのでテストコードから使い方を見るしかない。
orbsのコードで最も謎、かつ面倒なのは OrbImport()
の引数でOrbImportManifestのオブジェクトを渡すところ。
後でorbのexecutors, jobs, commandsを参照するときのキーは 予めここで最低限の定義をしておかないとエラーでyamlの生成すらできない っぽい。
先人の https://t28.dev/circleci-config-app-with-sdk/#fnref-6 の記事を見たときに何を言っているのか全然分からなかったが、自分で書いてみたら本当にそのような挙動になっていることを確認した。謎すぎる。
orbsで使いたい機能のキーを自分で毎回定義する必要があるのは面倒すぎないか?将来的にOrbsのyaml自体がこのSDKで生成されるようになり、対となるjsのコードも生成されてそこでOrbImportManifestもimportできるようになれば話は変わってくるが、そんな遠回りを必要とせずに任意のキーを入れたらそのままエラーを出さずにyamlの生成までは完走してほしい。
const CircleCI = require("@circleci/circleci-config-sdk")
const main = async () => {
const config = new CircleCI.Config()
const workflow = new CircleCI.Workflow('build')
config.addWorkflow(workflow)
const nodeOrb = new CircleCI.orb.OrbImport(
'node',
'circleci',
'node',
'5.0.3',
'Description: circleci/node orbs',
// OrbImportManifest
{
executors: {
'default': new CircleCI.parameters.CustomParametersList([])
},
jobs: {
'test': new CircleCI.parameters.CustomParametersList([])
},
commands: {
'install-packages': new CircleCI.parameters.CustomParametersList([])
}
}
)
config.importOrb(nodeOrb)
// Using orb executors and commands
const nodeExecutor = new CircleCI.reusable.ReusedExecutor(nodeOrb.executors['default'], { tag: 'lts'})
const buildJob = new CircleCI.Job('build', nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.reusable.ReusedCommand(nodeOrb.commands['install-packages'], {
'pkg-manager': 'npm',
'cache-version': 'v2'
}),
new CircleCI.commands.Run({
name: 'build',
command: 'npm run build',
})
])
config.addJob(buildJob)
workflow.addJob(buildJob)
// Using orb jobs
workflow.addJob(nodeOrb.jobs['test'], {
'run-command': 'test:ci',
'version': 'lts'
})
console.log(config.stringify())
console.warn(config.stringify())
}
main()
version: 2.1
setup: false
jobs:
build:
executor:
name: node/default
tag: lts
steps:
- checkout
- node/install-packages:
pkg-manager: npm
cache-version: v2
- run:
name: build
command: npm run build
workflows:
build:
jobs:
- build
- node/test:
run-command: test:ci
version: lts
orbs:
node: circleci/node@5.0.3
orbsを使うコードは circleci/node のサンプルなどを見ながらよく使いそうなパターンを実装してみた
実践編
過去の登壇のときに作成したdocker buildxでビルドするconfig.ymlの一部を再現してみる
command
に複数行のコマンドを書く場合に |-
をどうやってjsに書けばいいのかと思ったが、改行を含む文字列を渡せば自動的に |-
を追加してくれるらしい。
const CircleCI = require("@circleci/circleci-config-sdk")
const main = async () => {
const config = new CircleCI.Config()
const workflow = new CircleCI.Workflow('build')
config.addWorkflow(workflow)
// Define executor
const dockerExecutor = new CircleCI.executors.DockerExecutor('cimg/base:2022.03', undefined, { environment: {
IMAGE: "ghcr.io/kesin11/circleci-cli-sandbox"
}})
const reusableExecutor = new CircleCI.reusable.ReusableExecutor('cimg-docker', dockerExecutor)
config.addReusableExecutor(reusableExecutor)
// Define command
const setupDockerCommand = new CircleCI.reusable.ReusableCommand('setup-docker', [
new CircleCI.commands.SetupRemoteDocker({version: "20.10.11"}),
new CircleCI.commands.Run("docker version")
])
const setupBuildxCommand = new CircleCI.reusable.ReusableCommand('setup-buildx', [
new CircleCI.commands.Run({
name: "Show docker info",
command: [
"docker version",
"docker buildx version",
"docker context inspect"
].join('\n')
}),
new CircleCI.commands.Run({
name: "Setup docker buildx",
command: [
"docker context circleci",
"docker buildx create --use circleci",
"docker buildx ls",
"docker context inspect circleci",
].join('\n')
})
])
config.addReusableCommand(setupDockerCommand)
config.addReusableCommand(setupBuildxCommand)
// Define reusing executor and command
const reusedExecutor = new CircleCI.reusable.ReusedExecutor(reusableExecutor)
const reusedSetupDocker = new CircleCI.reusable.ReusedCommand(setupDockerCommand)
const reusedSetupBuildx = new CircleCI.reusable.ReusedCommand(setupBuildxCommand)
// build job
const buildJob = new CircleCI.Job('docker-build-registry-cache', reusedExecutor, [
new CircleCI.commands.Checkout(),
reusedSetupDocker,
reusedSetupBuildx,
new CircleCI.commands.Run({
name: 'docker login',
command: 'echo $GITHUB_CR_PAT | docker login ghcr.io -u kesin11 --password-stdin',
}),
new CircleCI.commands.Run({
name: 'Docker build with registry cache',
command: 'docker buildx build --progress=plain -f Dockerfile --cache-to=type=registry,mode=max,ref=$IMAGE:cache --cache-from=type=registry,ref=$IMAGE:cache -t $IMAGE:latest .'}),
])
config.addJob(buildJob)
workflow.addJob(buildJob)
// Output
console.log(config.stringify())
console.warn(config.stringify())
}
main()
version: 2.1
setup: false
commands:
setup-docker:
steps:
- setup_remote_docker:
version: 20.10.11
- run: docker version
setup-buildx:
steps:
- run:
name: Show docker info
command: |-
docker version
docker buildx version
docker context inspect
- run:
name: Setup docker buildx
command: |-
docker context create circleci
docker buildx create --use circleci
docker buildx ls
docker context inspect circleci
executors:
cimg-docker:
docker:
- image: cimg/base:2022.03
environment:
IMAGE: ghcr.io/kesin11/circleci-cli-sandbox
resource_class: medium
jobs:
docker-build-registry-cache:
executor:
name: cimg-docker
steps:
- checkout
- setup-docker
- setup-buildx
- run:
name: docker login
command: echo $GITHUB_CR_PAT | docker login ghcr.io -u kesin11 --password-stdin
- run:
name: Docker build with registry cache
command: docker buildx build --progress=plain -f Dockerfile --cache-to=type=registry,mode=max,ref=$IMAGE:cache --cache-from=type=registry,ref=$IMAGE:cache -t $IMAGE:latest .
workflows:
build:
jobs:
- docker-build-registry-cache
実践編
CircleCIのAPIからデプロイジョブを手動でトリガーするジョブのパターン
自分がpublicにしているリポジトリにはいい感じのconfig.ymlが存在しなかったのでサンプルを見ながら適当に作った
andやorを複数使う複雑な when
はテストコードから見つかったが、単純にbooleanのパラメータのtrue/falseを比較するだけの when: << pipeline.parameters.manual_deploy >>
を記述する方法がよく分からなかった。
他にそれっぽいものが存在しないかConditionを調べたところTruthyというものを見つけたので試したみたところビンゴだった
new CircleCI.logic.conditional.Truthy(`<< pipeline.parameters.${TRIGGER_NAME} >>`)
全体としてはこうなる
const CircleCI = require("@circleci/circleci-config-sdk")
// Manual trigger sample
// https://support.circleci.com/hc/en-us/articles/360050351292-How-to-Trigger-a-Workflow-via-CircleCI-API-v2
const main = async () => {
const TRIGGER_NAME = 'manual_deploy'
const config = new CircleCI.Config()
// Workflow condition
const when = new CircleCI.logic.When(
new CircleCI.logic.conditional.Truthy(`<< pipeline.parameters.${TRIGGER_NAME} >>`)
)
const workflow = new CircleCI.Workflow('deploy', [], when)
config.addWorkflow(workflow)
// Pipeline parameters
config.defineParameter(TRIGGER_NAME, 'boolean', false)
const nodeExecutor = new CircleCI.executors.DockerExecutor('cimg/node:lts', 'small')
const deployJob = new CircleCI.Job('deploy', nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
name: 'install',
command: 'npm ci',
}),
new CircleCI.commands.Run({
name: 'deploy',
command: 'npm run deploy',
})
])
config.addJob(deployJob)
workflow.addJob(deployJob)
// Output
console.log(config.stringify())
console.warn(config.stringify())
}
main()
version: 2.1
setup: false
parameters:
manual_deploy:
type: boolean
default: false
jobs:
deploy:
docker:
- image: cimg/node:lts
resource_class: small
steps:
- checkout
- run:
name: install
command: npm ci
- run:
name: deploy
command: npm run deploy
workflows:
deploy:
when: << pipeline.parameters.manual_deploy >>
jobs:
- deploy
一通りを試し終わったので感想。
率直に言ってどういうユーザー向けを目指しているのかよく分からなかった。少なくとも自分は今までのconfig.ymlを手書きする方法から積極的に乗り換えたいと思う要素はなかった。
まずyamlを手書きする方法に比べてどうしても記述量が増えるが、jsからyamlを生成するというアプローチなのでこれは仕方ない。
少なくともVSCodeを使えばjsのままでも型があるのは一見嬉しく思えるが、そもそもVSCodeのyamlの汎用extensionがインストールされていればyamlのままでもCircleCIのconfig.ymlとして有効なキーは補完が効くので特にメリットではない。
このSDKを使う最大のメリットはReusableで定義したexecutorやcommand、あるいはjobそのものをexportしてnpmパッケージ化してしまえば社内で共有したり、誰かがnpmにpublishしたものを使うことができる点だと思うが、それってOrbsで実現できることと同じでは?
もしもこのSDKがCircleCIにorbsというyamlの一部を実質的にimportする機能や、条件分岐のためのwhen
が追加されるより前の時代に登場していたのであれば流行ったかもしれない。
現在ではconfig.ymlの一部をorbsとして共有可能だし、例えば社内利用のような一部のユーザーにだけ共有することもprivate orbによって可能になっている。
もし when
が存在しなければDynamic Configurationと併用してjs側で条件分岐して生成されるconfig.ymlを変化させて柔軟性を持たせることが可能だったかもしれないが、今ではそもそも when
とand, or, notを組み合わせることで複雑な条件分岐も大体可能になっている。
自分が思いつく限りではSDKで可能なことは既にyamlで実現可能なので、わざわざjsというレイヤーを1つ増やしてconfig.ymlをさらに複雑にすることのメリットを見いだせなかった。
特にこのSDKを使ってさらに外部のOrbsを使うようなコードの場合、最終的なyamlが生成されるロジックを完全に把握しようと思ったときに見る必要があるコードが多く、場所もバラバラなのでカオスになりそうな予感がある。
- 自分がjsで書いたコード
- npm installしたjsのコード
- 使用するOrbsのyamlの中身
- (Dynamic Configuratonで動的にconfig.ymlを生成する場合)
setup: true
を書いている元のconfig.yml- このSDKでDynamic Configuration用の
setup: true
のconfig.ymlを生成することも可能そうなので、その場合はそこもjsのコードを読むことになる
- このSDKでDynamic Configuration用の
今回試したコードのリポジトリ
参考ドキュメント