🧑‍🏭

コピペで使いまわしが出来る Skaffold + Kustomize 設定

2023/04/19に公開

はじめに

この辺りの記事に書きましたが、最近オンプレミスで動かしていた k8s 環境を GKE に移行しました。

https://zenn.dev/soumi/articles/26c91f246ac630
https://zenn.dev/soumi/articles/5d01179ddb7763

元々 k8s にデプロイする時には Skaffold + Kustomize を使用しており、アプリを増やす時は設定ファイル群をコピペする事で使いまわしていました。
ところが、移行の際に Kukstomizevarsdeprecated になっている事に気付き、代替機能である replacement を使用して書き換える事にしました。

色々と躓いたところもあり、せっかくなので今回記事にしてみようと思います。
また、今回紹介した内容を git にアップしておいたので、興味があれば使ってみて下さい。

https://github.com/soumi-akira/kustomize-sample

フォルダ構造

最終的に、以下のようなフォルダ構造になりました。
(縦長な画像なので、閉じておきます)

フォルダ構造

ルートに Dockerfileskaffold.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 --from=build /app/node_modules /app/node_modules
COPY --from=build /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.nameAPP_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 コンポーネントをベースに、 DeploymentHTTPRouteService の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

アプリ毎に必要な設定が異なるため、一例となります。
ここでは、GKESpot 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:
    - $${{ ルーティングに使用するドメイン }}

ゲートウェイについては、こちらの記事に少し記述しています。

https://zenn.dev/soumi/articles/26c91f246ac630#gatewayapiを有効に

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.yamlprofiles に staging を追加

configs/stagingdeploy/staging 内の構造は production の物と変わりません。

おわりに

productionstaging 等の環境は一度基本形を作ってしまったらあとはフォルダをコピーするだけで済みます。
僕は新しいアプリを作ったら ops フォルダと Dockerfileskaffold.yaml を既にデプロイまで終わっているプロジェクトからコピペして使いまわしていました。

この辺りは各人秘伝のタレを持っている気がしますし、もっと効率の良い方法もあるかもしれないのですが、まずはこれからやってみようという方の参考になれば幸いです。

Discussion