CUEでCloud RunのYAMLファイルを作ってみる
この記事はAll About Group(株式会社オールアバウト)Advent Calendar 2023の25日目の投稿です。
Cloud RunのYAMLファイルについて
Cloud Runをデプロイする方法はいくつかありますが、k8sと同じように設定をYAMLファイルに書いてデプロイすることができます。
具体的には以下のようなファイルになります。k8sのマニフェストファイルを書いたことがある方なら初見でも理解できる内容かと思います。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: hello-01
annotations:
run.googleapis.com/ingress: all
spec:
template:
spec:
containerConcurrency: 80
timeoutSeconds: 300
serviceAccountName: hello-world@sample-project.iam.gserviceaccount.com
containers:
- name: hello-1
image: us-docker.pkg.dev/cloudrun/container/hello:latest
ports:
- name: http1
containerPort: 8080
resources:
limits:
cpu: 1000m
memory: 512Mi
startupProbe:
timeoutSeconds: 240
periodSeconds: 240
failureThreshold: 1
tcpSocket:
port: 8080
このYAMLファイルを生成する方法はいくつかありますが(もちろん直接書くのも一つです)、CUE言語を使うことで、記述内容の省略とガードレール設定の両方を比較的容易に実現することができるので今回紹介していきます。
CUEの概要
CUEは設定ファイルの生成を簡易にすることを目的に作られた言語でJSONのスーパーセットで、データ内容に合わせた詳細なvalidationを設定したり、設定ファイルにありがちな定型的な記述を省略・簡略化したりすることができます。
詳しくはGithubのREADMEの記載をご確認ください。
国内での利用例はあまり多くありませんが、メルカリやカウシェなどで既に使われており、今後の利用拡大が期待される技術の一つだと思っています。
CUEでCloud RunのYAMLファイルを生成
それでは本題に入っていきます。
まずは全てのCloud Runで共通で利用するテンプレートファイルを作成していきます。
template.cue
package template
apiVersion: "serving.knative.dev/v1"
kind: "Service"
#Metadata: {
name: string
annotations?: {
"run.googleapis.com/launch-stage"?: "BETA"
"run.googleapis.com/ingress": "all" | "internal-only" | "internal-and-cloud-load-balancing" | *"internal-and-cloud-load-balancing"
}
}
#Spec: {
template: {
metadata: {
annotations: {
"run.googleapis.com/execution-environment": "gen1" | "gen2" | *"gen1"
"autoscaling.knative.dev/minScale": int & <=10 | *0
"autoscaling.knative.dev/maxScale": int & <=50 | *5
"run.googleapis.com/cpu-throttling": bool | *true
"run.googleapis.com/vpc-access-connector"?: string & =~ "projects/.*/locations/.*/connectors/.*"
"run.googleapis.com/vpc-access-egress"?: string & =~ "all|private-ranges-only"
"run.googleapis.com/startup-cpu-boost"?: int & >=2 & <=10
}
}
spec: {
containerConcurrency: int & <=100 | *10
serviceAccountName: string & =~".*iam.gserviceaccount.com$"
timeoutSeconds: int & <=300 | *5
containers: [
...{
name: string
image: string & =~"(asia-east1-docker.pkg.dev|asia-northeast1-docker.pkg.dev)/prj-.*/.*/.*"
ports: [
{
name: "http1" | "h2c" | *"http1"
containerPort: int & >=0 & <=65535 | *8080
}
]
resources: {
limits: {
cpu: "500m" | "1000m" | "1500m" | "2000m" | *"1000m"
memory: "512Mi" | "1024Mi" | "2048Mi" | *"1024Mi"
}
}
livenessProbe: {
initialDelaySeconds: int & >=0 & <=300 | *3
timeoutSeconds: int & >=1 & <=300 | *1
periodSeconds: int & >=1 & <=300 | *10
successThreshold: int & >=1 & <=10 | *1
failureThreshold: int & >=1 & <=10 | *3
httpGet: {
path: string
port: int & >=0 & <=65535 | *8080
httpHeaders?: [
...{
name: string
value: string
}
]
}
}
startupProbe: {
initialDelaySeconds: int & >=0 & <=300 | *3
timeoutSeconds: int & >=1 & <=300 | *1
periodSeconds: int & >=1 & <=300 | *10
successThreshold: int & >=1 & <=10 | *1
failureThreshold: int & >=1 & <=10 | *3
httpGet: {
path: string
port: int & >=0 & <=65535 | *8080
httpHeaders?: [
...{
name: string
value: string
}
]
}
}
args?: [...string]
env?: [
...{
name: string
value?: string
valueFrom?: {
secretKeyRef: {
name: string
key: int | "in-use"
}
}
}
]
volumeMounts?: [
...{
name: string
mountPath: string
}
]
}
]
volumes?: [
...{
name: string
secret: {
secretName: string
item: {
key: int | "in-use"
path: string
}
}
}
]
}
}
}
かなり長くなりました。全て説明しているととんでもない量になるのでトピックに絞って解説していきます。
package service
apiVersion: "serving.knative.dev/v1"
kind: "Service"
CUEにはパッケージの概念があるので先頭でパッケージ名を宣言しています。こうすることで別ファイルからパッケージ名でimportできるようになります。その下は固定値で入る値をそのまま設定しています。
次にmedataです。
#Metadata: {
name: string
annotations?: {
"run.googleapis.com/launch-stage"?: "BETA"
"run.googleapis.com/ingress": "all" | "internal-only" | "internal-and-cloud-load-balancing" | *"internal-and-cloud-load-balancing"
}
}
CUEではこのようにデータ型を定義したり、取り得る値を列挙したりすることができます。またデフォルト値を設定することもできます。
上記の例ですとmetadata.annotations."run.googleapis.com/ingress"
は「all」、「internal-only」、「internal-and-cloud-load-balancing」のいずれかでデフォルトでは「internal-and-cloud-load-balancing」になります。またキー名に「?」が付いている項目は省略可能です。
#Spec: {
template: {
spec: {
serviceAccountName: string & =~".*iam.gserviceaccount.com$"
containers: [
...{
image: string & =~"(asia-east1-docker.pkg.dev|asia-northeast1-docker.pkg.dev)/prj-.*/.*/.*"
ports: [
{
name: "http1" | "h2c" | *"http1"
containerPort: int & >=0 & <=65535 | *8080
}
]
}
]
}
}
}
CUEでは正規表現を使ったvalidationも設定できるのでサービスアカウントやコンテナイメージのように形式が決まっている文字列は上記のように定義することでタイポ等の思わぬエラーがデプロイ時に発生するのを防止できます。
また spec.template.spec.containers[0].ports[0].containerPort
のように範囲を定義しつつデフォルト値も設定する、といった組み合わせも可能です。このようにCUEの定義方法はとても柔軟性があります。
service.yml
import (
"github.com/sample-org/sample-repos/service"
)
apiVersion: template.apiVersion
kind: template.kind
metadata: template.#Metadata & {
name: "sample-app"
}
spec: template.#Spec & {
template:spec: {
serviceAccountName: "sample-app@sample-project.iam.gserviceaccount.com" containers: [
{
name: "sample-app",
image: "asia-northeast1-docker.pkg.dev/sample-project/sample-repos/sample-app:latest",
ports: [{
containerPort: 8080
}],
livenessProbe: httpGet: {
path: "/health",
port: 8080
},
startupProbe: httpGet: {
path: "/health",
port: 8080
},
}
]
}
}
template.cueをもとにサービス固有の値を設定していきます。基本的にJSONと同じ記法ですが、コメントを書くことができたり、以下のような省略記法があったりとJSONには存在しない記法もいくつか存在します。
template:spec: {
YAMLファイルを生成
以下コマンドを実行することでYAMLファイルを生成できます。
$ cue export service.cue --out yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: sample-app
spec:
template:
metadata:
annotations:
run.googleapis.com/execution-environment: gen1
autoscaling.knative.dev/minScale: 0
autoscaling.knative.dev/maxScale: 5
run.googleapis.com/cpu-throttling: true
spec:
containerConcurrency: 10
serviceAccountName: sample-app@sample-project.iam.gserviceaccount.com
timeoutSeconds: 5
containers:
- name: sample-app
image: "asia-northeast1-docker.pkg.dev/sample-project/sample-repos/sample-app:latest"
ports:
- name: http1
containerPort: 8080
resources:
limits:
cpu: 1000m
memory: 1024Mi
livenessProbe:
initialDelaySeconds: 3
timeoutSeconds: 1
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
httpGet:
path: /health
port: 8080
startupProbe:
path: /health
port: 8080
なお、CUEコマンドは以下からインストールできます。
外部パッケージを利用する場合の注意点
2023年12月現在CUEには外部リポジトリで作成したパッケージをダウンロードする機能がありません。そのためimportを利用してパッケージを参照する場合、事前に所定のディレクトリにCUEのコードを配置しておく必要があります。
例えば以下のようにimportする場合 cue.mod/pkg/github/sample-org/sample-repos
ディレクトリにテンプレートとなるCUEファイルを設置してからcue export...
を実行する必要があります。
import (
"github.com/sample-org/sample-repos/template"
)
なお、以下discussionによるとパッケージのダウンロード機能に関しては開発中とのことなので近い将来機能として追加される見込みです。
まとめ
プロダクトチームが複数あるような中規模以上の組織では、プロダクトごとに乖離しがちな設定ファイルをどうガバナンスしていくかが検討事項の一つになるかと思います。CUEを使うことで各チームに過度な負担や学習コストを強いることなくガバナンスを効かせることができるので設定ファイルに関して悩みを持っている場合はぜひ一度検討してみては如何でしょうか?
今回の例はCloud Runでしたが、github actionsでCI/CDを作る時に活用しても面白いと思います。
以上「CUEでCloud RunのYAMLファイルを作ってみる」でした。
Discussion