コピペで使いまわしが出来る Skaffold + Kustomize 設定
はじめに
この辺りの記事に書きましたが、最近オンプレミスで動かしていた k8s
環境を GKE
に移行しました。
元々 k8s
にデプロイする時には Skaffold
+ Kustomize
を使用しており、アプリを増やす時は設定ファイル群をコピペする事で使いまわしていました。
ところが、移行の際に Kukstomize
の vars
が deprecated
になっている事に気付き、代替機能である replacement
を使用して書き換える事にしました。
色々と躓いたところもあり、せっかくなので今回記事にしてみようと思います。
また、今回紹介した内容を git
にアップしておいたので、興味があれば使ってみて下さい。
フォルダ構造
最終的に、以下のようなフォルダ構造になりました。
(縦長な画像なので、閉じておきます)
フォルダ構造
ルートに Dockerfile
と skaffold.yaml
があり、詳細な設定は ops
ディレクトリ以下に置いてあります。
ファイル詳細
変数について
ファイルの中に出てくる $${{ xxxxx }}
は環境によって置き替える変数になります。
ルートディレクトリ
Dockerfile
Dockerfile
についてはアプリによって書き方が異なるので、以下は一例です。node
のアプリをマルチステージビルドを使って書き出しています。
FROM node:17.3.0-stretch AS build
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . .
RUN yarn generate
RUN yarn build
FROM node:17.3.0-alpine AS production
WORKDIR /app
COPY . .
COPY /app/node_modules /app/node_modules
COPY /app/dist /app/dist
CMD ["node", "dist/main.js"]
skaffold.yaml
今回は本番環境のみの想定で進めていますが、他のデプロイ環境がある場合は、以降の設定も環境分追加した上でこのファイルにも記述が必要です。
apiVersion: skaffold/v4beta1
kind: Config
metadata:
name: $${{ アプリ名 }}
build:
tagPolicy:
dateTime: {}
artifacts:
- image: $${{ Dockerイメージをpushするリポジトリ }}
context: .
local:
useBuildkit: true
profiles:
- name: production
manifests:
kustomize:
paths:
- ops/production
# 他のデプロイ環境がある場合は以下のように追加
# - name: staging
# manifests:
# kustomize:
# paths:
# - ops/staging
ops/components
ops/components/core/basic.yaml
このファイルがデプロイの基本ファイルになり、「どんなアプリをリリースするにしてもここだけは共通設定になる」という部分が記述されています。
apiVersion: v1
kind: Service
metadata:
name: APP_NAME
labels:
app: APP_NAME
service: APP_NAME
spec:
ports:
selector:
app: APP_NAME
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: APP_NAME
labels:
app: APP_NAME
version: v1
spec:
replicas: 1
selector:
matchLabels:
app: APP_NAME
version: v1
template:
metadata:
labels:
app: APP_NAME
version: v1
spec:
serviceAccountName: APP_NAME
containers:
- name: APP_NAME
image: registry
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef:
name: env-config
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: APP_NAME
spec:
hostnames:
- "*"
ops/components/core/kustomization.yaml
前項のファイルをコンポーネント化しています。
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- basic.yaml
前述の通りこのコンポーネントは基本設定が書いてあるため、以降に出てくるファイルに読み込まれる形で使用される事になります。
なぜコンポーネント化するのか?
replacements
による置き換えは vars
と違い都度実行されれてしまうようで、以降のフェーズで出てくる patchesStrategicMerge
を使用する際に必要な metadata.name
も置き替えられてしまいます。
今回は metadata.name
を APP_NAME
とする事で色々なアプリで使いまわしたいというコンセプトで作っており、カスタマイズの際にもなるべく変更点が多くないように設計したいという意図がありました。
詳細までは理解していませんが、コンポーネント化する事で replacements
がビルドの最後に実行されているような挙動になったので、今回はコンポーネントを採用しました。
ops/components/custom/kustomization.yaml
基本設定の中でカスタムしたい事があればここに書きます。また replacements
の設定もここで行います。
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
replacements:
- path: replacement.yaml
images:
- name: registry
newName: $${{ Dockerイメージをpushするリポジトリ }}
labels:
- includeSelectors: true
pairs:
app: $${{ アプリ名 }}
ops/components/custom/replacement.yaml
replacements
を使用するためには置き替え元として使用する source
と、置き換え対象の targets
を設定する必要があります。
ここでは、基本コンポーネントに対する置き換えを定義しています。
- source:
kind: Service
name: APP_NAME
fieldPath: metadata.labels.app
targets:
# 基本設定用
- select:
kind: Service
name: APP_NAME
fieldPaths:
- metadata.labels.service
- metadata.name
- select:
kind: Deployment
name: APP_NAME
fieldPaths:
- spec.template.spec.containers.[name=APP_NAME].name
- metadata.name
- select:
kind: HTTPRoute
name: APP_NAME
fieldPaths:
- spec.rules.*.backendRefs.[name=APP_NAME].name
- metadata.name
# production向け
- select:
kind: PodDisruptionBudget
name: APP_NAME
fieldPaths:
- metadata.name
ops/configs
ops/configs/base/kustomization.yaml
アプリ毎の設定を記述していきます。
ここでは前項で設定した core
コンポーネントをベースに、 Deployment
、HTTPRoute
、Service
の3つを編集しています。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patchesStrategicMerge:
- custom/deployment.yaml
- custom/http-route.yaml
- custom/service.yaml
components:
- ../../components/core
ops/configs/base/custom/deployment.yaml
アプリ毎に必要な設定が異なるため、一例となります。
ここでは、アプリの待ち受けポートを設定しています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: APP_NAME
spec:
template:
metadata:
labels:
app: APP_NAME
spec:
containers:
- name: APP_NAME
ports:
- containerPort: $${{ アプリケーションが待ち受けているポート }}
ops/configs/base/custom/http-route.yaml
アプリ毎に必要な設定が異なるため、一例となります。
ここでは、アプリの待ち受けパスとポートを設定しています。
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: APP_NAME
spec:
rules:
- matches:
- path:
type: PathPrefix
value: $${{ アプリケーションをルーティングするパス }}
backendRefs:
- name: APP_NAME
port: $${{ アプリケーションが待ち受けているポート }}
ops/configs/base/custom/service.yaml
アプリ毎に必要な設定が異なるため、一例となります。
ここではアプリの待ち受けポートとプロトコル設定しています。
apiVersion: v1
kind: Service
metadata:
name: APP_NAME
spec:
ports:
- port: $${{ アプリケーションが待ち受けているポート }}
name: http
ops/configs/production/kustomization.yaml
各環境向けのデプロイファイルとなります。
ここでは production
想定で書いていますが、 development
なら PodDisruptionBudget
が不要になるかもしれませんし、環境変数も変わると思います。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base
namespace: $${{ デプロイ先の名前空間 }}
resources:
- custom/pdb.yaml
patchesStrategicMerge:
- custom/deployment.yaml
- custom/http-route.yaml
configMapGenerator:
- name: env-config
literals:
- NODE_ENV=production
envs:
- .env
ops/configs/production/.env
デプロイ中に使用できる環境変数になります。
VARNAME=変数の内容
のような形で記述していきます。
SAMPLE=hello
ops/configs/production/custom/pdb.yaml
アプリ毎に必要な設定が異なるため、一例となります。
ここでは、本番環境に PDB
を設定するとして、以下のような設定ファイルを追加します。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: APP_NAME
spec:
minAvailable: 1
selector:
matchLabels:
app: APP_NAME
ops/configs/production/custom/deployment.yaml
アプリ毎に必要な設定が異なるため、一例となります。
ここでは、GKE
の Spot Pod
を有効化し、コンテナのリソースも設定しています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: APP_NAME
spec:
template:
spec:
nodeSelector:
cloud.google.com/gke-spot: "true"
containers:
- name: APP_NAME
resources:
requests:
cpu: 500m
ephemeral-storage: 100Mi
memory: 512Mi
ops/configs/production/custom/http-route.yaml
アプリ毎に必要な設定が異なるため、一例となります。
ここでは、ルーティングに使用するドメインと使用するゲートウェイを設定しています。
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: APP_NAME
spec:
parentRefs:
- name: $${{ 使用するゲートウェイ }}
hostnames:
- $${{ ルーティングに使用するドメイン }}
ゲートウェイについては、こちらの記事に少し記述しています。
ops/deploy
ops/deploy/production/kustomization.yaml
デプロイ時に最終的に使用する kustomization
ファイルです。今まで設定したものは全てここに集約されています。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../configs/production
components:
- ../../components/custom
書き出されるyamlを確認する
設定が完了したら、設定が間違っていないか、一度書き出して確認してみる事をお勧めします。
kustomize build .\ops\deploy\production\
変数を編集せずに書き出すと、以下のようなyamlファイルが得られます。
(長いので閉じておきます)
最終的に書き出されるyaml
apiVersion: v1
data:
NODE_ENV: production
SAMPLE: hello
kind: ConfigMap
metadata:
labels:
app: $${{ 置き替えられるアプリ名 }}
name: env-config-t928667ttc
namespace: $${{ デプロイ先の名前空間 }}
---
apiVersion: v1
kind: Service
metadata:
labels:
app: $${{ 置き替えられるアプリ名 }}
service: $${{ 置き替えられるアプリ名 }}
name: $${{ 置き替えられるアプリ名 }}
namespace: $${{ デプロイ先の名前空間 }}
spec:
ports:
- name: http
port: $${{ アプリケーションが待ち受けているポート }}
selector:
app: $${{ 置き替えられるアプリ名 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: $${{ 置き替えられるアプリ名 }}
version: v1
name: $${{ 置き替えられるアプリ名 }}
namespace: $${{ デプロイ先の名前空間 }}
spec:
replicas: 1
selector:
matchLabels:
app: $${{ 置き替えられるアプリ名 }}
version: v1
template:
metadata:
annotations:
sidecar.istio.io/proxyCPU: 125m
sidecar.istio.io/proxyMemory: 128Mi
labels:
app: $${{ 置き替えられるアプリ名 }}
version: v1
spec:
containers:
- envFrom:
- configMapRef:
name: env-config-t928667ttc
image: $${{ Dockerイメージをpushするリポジトリ }}
imagePullPolicy: IfNotPresent
name: $${{ 置き替えられるアプリ名 }}
ports:
- containerPort: $${{ アプリケーションが待ち受けているポート }}
resources:
requests:
cpu: 500m
ephemeral-storage: 100Mi
memory: 512Mi
nodeSelector:
cloud.google.com/gke-spot: "true"
serviceAccountName: APP_NAME
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
labels:
app: $${{ 置き替えられるアプリ名 }}
name: $${{ 置き替えられるアプリ名 }}
namespace: $${{ デプロイ先の名前空間 }}
spec:
minAvailable: 1
selector:
matchLabels:
app: $${{ 置き替えられるアプリ名 }}
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
labels:
app: $${{ 置き替えられるアプリ名 }}
name: $${{ 置き替えられるアプリ名 }}
namespace: $${{ デプロイ先の名前空間 }}
spec:
hostnames:
- $${{ ルーティングに使用するドメイン }}
parentRefs:
- name: $${{ 使用するゲートウェイ }}
rules:
- backendRefs:
- name: $${{ 置き替えられるアプリ名 }}
port: $${{ アプリケーションが待ち受けているポート }}
matches:
- path:
type: PathPrefix
value: $${{ アプリケーションをルーティングするパス }}
複数の環境に対応する
今回は production
のみの環境で解説しましたが、例えば development
という環境を追加したい場合は以下の手順で可能です。
- configs/staging を作成
- deploy/staging を作成
-
skaffold.yaml
のprofiles
に staging を追加
configs/staging
や deploy/staging
内の構造は production
の物と変わりません。
おわりに
production
や staging
等の環境は一度基本形を作ってしまったらあとはフォルダをコピーするだけで済みます。
僕は新しいアプリを作ったら ops
フォルダと Dockerfile
、skaffold.yaml
を既にデプロイまで終わっているプロジェクトからコピペして使いまわしていました。
この辺りは各人秘伝のタレを持っている気がしますし、もっと効率の良い方法もあるかもしれないのですが、まずはこれからやってみようという方の参考になれば幸いです。
Discussion