Skip to content

Infrastructure Adapter Specification

アプリケーションを動かすクラウドアーキテクチャを構成する「インフラストラクチャ構成」を定義するInfrastructure Adapterの仕様について以下に示します。

以下の定義のように、Infrastructure Adapterは、大きく分けて、初期化時に利用するparameters、出力するResource Manifest(クラウド、およびアプリケーションの設定ファイル)を宣言するresources、および組み合わせるInfrastructure Adapterのリストcompositesから構成されます。

Qmonus Value Streamは、このInfrastructure Adapterを解決し、Resource Manifestを生成します。この処理をコンパイル、と呼びます。

cue
#DesignPattern: {
	name:        string
	description: string | *""

	// input parameters
	parameters: {[string]: _}

	// enumerates design pattern
	composites?: [...#CompositeDef]

	// whether to group resources after composite
	group: string | *""

	// resource declaration
	resources?: #ResourceOutput

	// lazy evaluation
	defer?: {
		#ResourceOutput
	}
}

Infrastructure Adapterの基本要素

nameおよびdescriptionフィールドは、Infrastructure Adapterについての説明を記述します。現在、これらのフィールドがコンパイル時に利用されることはありません。 groupフィールドは、出力するResource Manifestをグループ化する際に宣言します。本フィールドはDeprecatedであり、後述するcomposites内で指定するgroupフィールドを利用ください。

ここで、nameは必須フィールドです。

cue
name:        "pattern01"
description: "a sample infrastructure adapter"
group:       "group01"

Parameters

parametersフィールドは、Infrastructure Adapterの初期化時に、外から受け取るパラメータのリストを宣言します。コンパイル時に渡されたパラメータの値は、ここで宣言したparametersにバインドされます。 具体的には、QVS ConfigでInfrastructure Adapterを指定した際に一緒に宣言したパラメータparamsが、ここで宣言したparametersにバインドされます。また、後述するComposite処理においても、Infrastructure Adapterとそれに渡すパラメータを宣言でき、QVS Configと同様に、ここで宣言したparametersにバインドします。

ここで、parametersは必須フィールドです。

string型のパラメータであるnameおよびnamespace、int型のパラメータであるreplicas、#Secret型のパラメータであるapiKeyを受け取ります。

cue
parameters: {
	name:      string
	namespace: string
	replicas:  int
	apiKey:    #Secret
}

#Secret

parametersフィールドでは一般的な型に加えて、独自に#Secret型を定義することで、CLIによりSecretオブジェクトがBindingされるフィールドを宣言できます。 #Secret型は以下のようなkeyversionのフィールドを含む型である必要があります。

cue
#Secret: {
    key: string
    version: string
}

#Secretの各フィールドはそれぞれ以下の内容を意味しています。

フィールド名説明
key機密情報の管理サービスにおいて登録されている機密情報の名前
version機密情報の管理サービスにおいて登録されている機密情報のバージョン

Resources

resourcesフィールドは、Infrastructure Adapterが出力するResource Manifestについて、リソースIDをkeyとしたObjectの形で宣言します。 以下の#ResourceBaseに示すように、Resource Manifestは、プロバイダ定義、APIスキーマおよびメタデータの固定フィールドを持ちます。さらに、#ResourceSpecに示すように、APIスキーマごとの固有なフィールドを宣言したものがResource Manifestであり、#ResourceOutputで示すように、appまたはappSetupをkeyにまとめた形で、resourcesフィールドに定義します。

providerフィールドは、kubernetesgcpといった、プロバイダ定義を宣言します。apiVersionおよびkindフィールドは、リソースのAPIスキーマを宣言します。metadataフィールドは、リソースのメタ情報を宣言します。ここで、providerapiVersionkindmetadata.nameは必須フィールドです(outputは現在利用されません)。

appおよびappSetupは、それぞれリソースをデプロイするStageを宣言します。app stageは、アプリケーションを実際にインストール・アップデートするstageであるのに対し、appSetupは事前作業としてアプリケーションが動作するために必要な周辺のリソースをデプロイするstageを表します。

cue
// Infrastructure Adapterの抜粋
#DesignPattern: {
	// resource declaration
	resources?: #ResourceOutput
	...
}

#ResourceBase: {
	// cloud provider
	provider: string

	// API schema
	apiVersion: string
	kind:       string

	// metadata
	metadata: {
		name: string
		{[string]: _}
	}

	// output to save
	output: [...string]
}

#ResourceSpec: {
	#ResourceBase

	// any resource specific configuration
	{[string]: _}
}

#ResourceOutput: {
	// application setup stage
	appSetup?: {[string]: #ResourceSpec}

	// application deployment stage
	app?: {[string]: #ResourceSpec}
}

appSetup stageでServiceとIngressを、app stageでDeploymentリソースのManifestを出力します。

cue
resources: {
	app: deployment: {
		provider:   "kubernetes"
		apiVersion: "apps/v1"
		kind:       "Deployment"

		metadata: name: "app01"

		spec: ...
	}

	appSetup: service: {
		provider:   "kubernetes"
		apiVersion: "v1"
		kind:       "Service"

		metadata: name: "service01"

		spec: ...
	}

	appSetup: ingress: {
		provider:   "kubernetes"
		apiVersion: "networking.k8s.io/v1beta1"
		kind:       "Ingress"

		metadata: name: "ingress01"

		spec: ...
	}
}

Composite

compositesフィールドは、他のInfrastructure Adapterを組み合わせて新しいInfrastructure Adapterを定義するために、組み合わせるInfrastructure Adapterとそれに渡すパラメータを宣言します。Infrastructure Adapterのコンパイル時に、ここで宣言されたInfrastructure Adapterを読み出し、各Infrastructure Adapterが出力するResource Manifestを解決します。その後、解決したManifestを、resources フィールドに全て結合します。この一連の処理をComposite処理と呼びます。

以下の#CompositeDefで示すように、compositesフィールドは、組み合わせたInfrastructure Adapterについてのルールをリストで宣言します。具体的には、CompositeするInfrastructure Adapterを指定するpattern、バインドするパラメータを指定するparams、およびComposite時に指定したInfrastructure Adapterが出力するResource Manifestのグループ化を指定するgroupを宣言します。

ここで、patternおよびparamsは必須フィールドです。またgroupフィールドを指定することで、Composite処理で解決したリソース定義をグループ化し、resourcesフィールドに結合する際に、他のリソースと結合しないよう、分離できます。

cue
// Infrastructure Adapterの抜粋
#DesignPattern: {
	// resource declaration
	composites?: [...#CompositeDef]
	...
}

#CompositeDef: {
	// design pattern to composite
	pattern: #DesignPattern

	// parameters to bind on composite
	params: {[string]: _}

	// group composited resource to isolate from others
	group?: string
}

app Infrastructure AdapterをCompositeしたリソース定義に対して、独自のannotationをさらに追加します。

cue
import (
	app "module-to-load"
)

composites: [
	{
		pattern: app.DesignPattern
		params: {
			name:      "app01"
			namespace: "test-namespace"
			replicas:  3
		}
	}
]

resources: {
	app: deployment: {
		metadata: annotations: "my-original": "label"
	}
}

Defer

deferフィールドは、Infrastructure Adapterのコンパイル処理のなかで最後に評価するデータを宣言します。Composite処理後に解決したResource Manifestに追加のデータを加える、などといった遅延評価を実現できます。

app Infrastructure AdapterをCompositeしたリソース定義全てに対して、独自のannotationを追加します。

cue
import (
	app "module-to-load"
)

composites: [
	{
		pattern: app.DesignPattern
		params: {
			name:      "app01"
			namespace: "test-namespace"
			replicas:  3
		}
	}
]

resources: {}

defer: {
	app: {
		for key, res in resources.app {
			"\(key)": metadata: annotations: "my-original": "label"
		}
	}
}

Annotation付与によるデプロイ動作制御

Infrastructure Adapterを実装する際、特定のAnnotationを付与することで、リソースのデプロイ挙動を制御できます。以下の三種類のAnnotationが設定できます。

  • vs.axis-dev.io/dependsOn:リソース間の依存関係を指定できます。
  • vs.axis-dev.io/ignore-changes:リソースのパラメータを更新しないように制御できます。
  • vs.axis-dev.io/keepOnDelete: リソースを削除しないよう制御できます。

dependsOn

dependsOnによるリソース間の依存関係を示すことで、依存先のリソースから先に作成・更新されるようになります。逆に削除のときは依存元のリソースが消えてから、依存先のリソースが削除されます。 依存先のリソースを更新するケースとして、依存元リソース名をsource、依存先の旧リソース名をtarget-old、新リソース名をtarget-newとすると、

  1. target-newが作成される
  2. sourceが更新される
  3. target-oldが削除される

という順序でデプロイが行われます。デプロイ失敗時には、後続の更新や削除が行われません。

Annotationとして指定する際の命名規則は以下のようになります。

"vs.axis-dev.io/dependsOn": ${apiGroup}:${kind}::${namespace}/${name}

  • ${apiGroup}は、apiVersionの中でversionを除いたもの(例: apiVersion: networking.k8s.io/v1 の場合、 networking.k8s.io)を指定します。version以外の指定がない場合、core を指定します (例:apiVersion:v1のみの場合、coreになります)。
  • ${kind}は、manifestのkindをそのまま指定します。
  • ${namespace}が指定されていない場合、 ${name}のみ指定します。 (/を除きます)

sample-appDeployment にsample-externalsecret-\(_hash)External Secret への依存関係を持たせることで、External Secretの更新とDeploymentの更新順序を制御する例を挙げます。

cue
_deployment: {
	apiVersion: "apps/v1"
	kind: "Deployment"
	metadata: {
		name: "sample-app"
		namespace: "test-namespace"
		annotations: "vs.axis-dev.io/dependsOn": "external-secrets.io:ExternalSecret::test-namespace/sample-externalsecret-\(_hash)"
	}
	[...]
}

External Secretのリソース名は、そのSpecによって決定するハッシュ値 _hash を含んでいると仮定した場合、External SecretのSpecを更新する際に、リソース名が変わります。Qmonus Value Streamは、デプロイ時に新しい名前でExternal Secretリソースを作成し、古いリソースは削除します。 上記のサンプルコードのように、DeploymentリソースからExternal Secretリソースへの依存関係を宣言することで、Qmonus Value Streamは、新しいExternal Secretリソースが作成された後にDeploymentリソースを更新するため、Deploymentリソースから新しいSecret情報を確実に参照できます。また、Deploymentリソースの更新が成功しない限り古いExternal Secretリソースを削除しないため、Deploymentリソースの更新失敗時には古いExternal Secretを参照したままの状態、すなわち失敗時の状態を保つことができます。

ignore-changes

ignore-changesにリソースのプロパティ名を指定することで、該当プロパティの値の変更を無視します。Qmonus Value Streamは、ignore-changesで指定されたプロパティについては、実際にリソースに適用されている値を尊重し、値を上書きすることはありません。本機能は、特定のプロパティをCI/CDのサイクルの外で変更し、その値を維持したい場合に有効です。 例外として、初回リソース作成時のみ、ignore-changesは効きません。

Annotationとして指定する際の命名規則は以下のようになります。

  • ドット区切りで指定します。 (例: spec: { selector: { app: x } } は、spec.selector.app)
  • リストの場合、[]で指定します。(例: containers: [ { image: x }]は、containers[0].image)
  • カンマ "," 区切りで指定することで複数指定できます。

HorizontalPodAutoscalerがCI/CDの外で動作し、Deploymentのreplica数をコントロールしている中で、Qmonus Value Streamにsample-deployment Deployment のspec.replicasの値の更新を無視させます。

cue
_deployment: {
	apiVersion: "apps/v1"
	kind: "Deployment"
	metadata: {
		name: "sample-deployment"
		annotations: "vs.axis-dev.io/ignore-changes": "spec.replicas"
	}
	spec: {
		replicas: 2 # 初回以外の値の更新を無視する
		[...]
	}
}

このようにすることで、Horizontal Pod Autoscalerを使って、以下のようにDeploymentのreplica数を動的に変更できます。

  1. Qmonus Value Streamを使って、Deploymentのreplica数を指定してデプロイします。
  2. Horizontal Pod Autoscalerを使って、CI/CDの外でDeploymentのreplica数を動的に変更します。
  3. ignore-changesreplicasプロパティを指定することで以降のデプロイ時にreplicasの変更を無視します。

keepOnDelete

keepOnDeleteのannotationを指定してデプロイされたリソースはデプロイ処理中の削除処理から保護されるようになります。また、annotationの値(以下、keepOnDeleteキー)には同じ目的でデプロイするリソースに同じ名前のキーを指定してください。そうすることでkeepOnDelete キーの単位でリソースをまとめてバージョン管理をし、将来は古いバージョンのリソースを自動で削除する機能を提供予定です。

本機能を利用することで、Blue/Green デプロイメントでConfigMap/Secretリソース等を使用するアプリケーションをデプロイする際に、Blue/Greenが切り替わる前のコンテナが参照している古いConfigMap/Secretを削除せずに維持できます。

ただし以下の注意点があります。

  • ConfigMap/Secretリソースを適切にrevision管理するため下記例のようにリソース名がユニークなものになるように検討してください。
  • keepOnDelete設定をせずにデプロイされているリソースは本機能の対象にならないため、既にデプロイされているリソースを削除したくない場合は手動でkeepOnDeleteannotationをリソースに付与してください。暗黙的にrevisionの値を"0"が指定されているものとして扱います。(*1)
  • 手動でrevisionのannotationを追加・編集・削除をすることはしないでください。

(*1) CLI (kubectl)を利用して手動でannotationを付与する場合は以下のようなコマンドで実施できます。

e.g. sample-namespace にある sample-config-abcd ConfigMapリソースを削除処理から保護したい。keepOnDeleteキーはsample-configとする。

# kubectl annotate コマンドを利用します
kubectl -n sample-namespace annotate configmaps sample-config vs.axis-dev.io/keepOnDelete=sample-config

ConfigMapリソースをapp-configというキーを持っているリソース群でrevision管理する場合の例を下記に記載します。また、下記の例ではConfigMapリソースが保持するデータが異なれば別リソースであるという考えを基に、dataフィールドの内容をhash化したものをリソース名のsuffixとして付与するという方法をとっています。

cue
import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/yaml"
	"strings"
)

// ConfigMap の data 部分だけYaml化する
let _data = yaml.Marshal(_configMap.data)

// Yaml化したデータをハッシュ化し、最初の10文字だけ取り出す
let _hash = strings.SliceRunes(hex.Encode(sha256.Sum256(_data)), 0, 10)

_configMap: {
	apiVersion: "apps/v1"
	kind:       "ConfigMap"
	metadata: {
		name: "sample-configmap-\(_hash)"
		annotations: "vs.axis-dev.io/keepOnDelete": "app-config" // app-config キーでrevisionを管理
	}
	data: {
		host:      "example.co.jp"
		log_level: "verbose"
	}
}

上記サンプルをコンパイルし生成されたConfigMapリソース名がsample-configmap-308a0d0c4cだとし、このリソースを環境にデプロイするとします。環境には既に以下のようなConfigMapリソースがデプロイ済みであった場合、

yaml
apiVersion: apps/v1
kind: ConfigMap
metadata:
  name: sample-configmap-2ed2af4518
  annotations:
    vs.axis-dev.io/keepOnDelete: app-config
    vs.axis-dev.io/revision: "1"
data:
  host: example.co.jp
  log_level: info

最終的にデプロイされるリソースの状態は以下のようになります。

  • 既にデプロイされていたsample-configmap-2ed2af4518リソースは従来では削除されます。しかし上記のように keepOnDeleteを指定することで削除処理から保護されて残り続けます。
  • 新規にデプロイされるsample-configmap-308a0d0c4cリソースはrevisionが"2"として設定された状態でデプロイされます。