😮

Skaffoldのスゴさを語る!

2024/03/18に公開

この記事は、2024/3/15に登壇したJagu'e'r クラウドネイティブ分科会 俺の考える最強のCI/CD
のリマスターになります。

k8sアプリケーション開発の悩み

突然ですが皆さん、k8sでアプリを動かす時にこんな悩み、イライラはありませんか?

k8sで検証する時には必ず通る道だと思います。
効率よく検証するにはどうしたものか、、

Skaffoldはそんな悩みを解決してくれます😄

Skaffoldとは?

概要

Skaffold[1]は、コンテナベース及びKubernetesアプリケーションの継続的開発(Continuous Development = CD)を容易にするOSSコマンドツールです。
Googleが開発元であるため、Google Cloudの一部サービスで利用されています。

  • Cloud Build内でSkaffoldを使用したビルド / テスト / デプロイ
  • Cloud DeployでSkaffoldマニュフェストのデプロイ定義をレンダリング

実行コマンド

代表的なコマンドは以下の通りです[2]

  • skaffold init : カレントディレクトリにskaffold.yamlを作成する。
  • skaffold dev : skaffold.yamlで定義したパイプラインを実行する。継続的開発に特化した様々な機能が盛り込まれている(後述)。
  • skaffold run : skaffold.yamlで定義したパイプラインを1回実行する。

基本的にはskaffold devを実行して検証していく流れになると思います。

パイプライン

skaffoldはざっくり以下のステップで各処理を実行します[3]

Skaffoldのココがスゴい👍

ここからは、Skaffoldの主要機能であるskaffold devコマンドのスゴいポイントを紹介します。

ホットリロード

skaffold devコマンドを実行すると、パイプラインがReconciliation loopで起動し、アプリコードやdockerfileなどのファイル修正の度に自動的にパイプラインが再実行されます。
ファイル修正の度にdocker build -> docker push -> kubectl applyの一連のコマンドを実行してくれるイメージです。

file sync

buildステップ内でsyncプロパティを使用すると、アプリコード修正時に再buildするのではなく、変更された箇所をリモートコンテナに同期させます。プログラミング言語側のホットリロードと組み合わせれば、ローカルで修正した内容でリモートコンテナが再ビルドする、なんてことも可能です。

file syncの設定は3種類あります[4]

  • infer : DockerfileのADD句やCOPY句で定義されたパスを監視し、同じフォルダ構成で同期する。
  • auto : ソースコードの修正を自動的に検知しsyncする。JibとBuildpacksでのみサポートされている。Buildpacksではdefaultで有効になっている。
  • manual : 同期元と同期先のパスをそれぞれ定義し、柔軟に同期を行う。

ローカルイメージの自動prune

skaffold devの実行終了時、自動でローカルイメージをprune出来ます[5]
コマンド実行時にオプションを指定する方法と環境変数を設定する方法の二つがあります。

<実行時にオプションを指定>
skaffold dev --no-prune=false --cache-artifacts=false
<環境変数を設定>
SKAFFOLD_NO_PRUNE=false
SKAFFOLD_CACHE_ARTIFACTS=false

なお、環境変数はファイルで定義することも可能です。

skaffold.env
SKAFFOLD_NO_PRUNE=false
SKAFFOLD_CACHE_ARTIFACTS=false

ポートフォワード

kubectl port-forwardを実行せずともポートフォワードが可能です。
以下を定義します。

portForward:
- resourceType: pod
  resourceName: hogehoge-app
  port: 3000
  localPort: 3000

自動リソース削除

skaffold devの実行終了時、デプロイしたリソースを全て削除します。
これにより、環境にゴミを残さずに検証することが可能です。

ログの自動tail

実行中のワークロードのログをコンソール上に表示します。
docker-compose upでログが表示されるイメージで問題ないです。
複数アプリケーションをデプロイすると可読性が下がるため、あくまでおまけ程度の機能となります。

Skaffoldで実現するCD(Continuous Development)

パイプライン

今まで紹介した機能をモリモリ組み込めば、こんなCDを構築することが可能です。

サンプルコード

上記のパイプラインを簡単に体験出来るサンプルコードです。
GKE上にNodes.jsのpodをデプロイします。

skaffold.yaml
apiVersion: skaffold/v4beta9
kind: Config
profiles:
- name: nodejs-app
  activation:
  - kubeContext: <GKEのコンテキスト>
  build:
    artifacts:
    - image: <Artifact Registoryのパス>/nodejs-app
      context: ./
      docker:
        dockerfile: Dockerfile.skaffold
      sync:
        infer: ["**/*"]
    tagPolicy:
      customTemplate: 
        template: "skaffold-{{.DIGEST}}"
        components:
        - name: DIGEST
          inputDigest: {}
  manifests:
    rawYaml:
    - k8s/*.yaml
  deploy:
    kubectl: {}
  portForward:
  - resourceType: pod
    resourceName: nodejs-app
    port: 3000
    localPort: 3000
  • profilesプロパティを使用することで、パイプラインを複数定義することが可能です。上記のyamlはskaffold dev --profile=nodejs-appで実行可能です。
  • Skaffold用の開発Dockerfileを用意します。(Dockerfile.skaffold)
  • tagPolicyプロパティで、skaffold-<ハッシュ>のタグを作成します。Artifact Registryのクリーンアップポリシーで、プレフィックスがskaffold-と一致するタグを自動的に削除する設定を行えばskaffoldと合わせて完全なゴミ掃除を実現出来ます。
skaffold.env
SKAFFOLD_NO_PRUNE=false
SKAFFOLD_CACHE_ARTIFACTS=false
Dockerfile.skaffold
FROM node:21-alpine

WORKDIR /app

COPY package*.json ./
COPY tsconfig.json ./
COPY nodemon.json ./
COPY src ./src
COPY dist ./dist 

RUN npm install

CMD npm run build && npm run start:skaffold
  • syncのinfer/manualプロパティ両方ともDockerfileのADD/COPY句に記載されているパスが対象として認識される挙動のため、イケてないですがCOPY dist ./distを記載しています。
  • modemonを使用し、nodejsのホットリロードを実現します。
package.json
{
  "scripts": {
    "start": "node dist/index.js",
    "start:skaffold": "nodemon dist/index.js"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.20",
    "nodemon": "^3.1.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}
  • nodemon起動用のコマンドstart:skaffoldを用意します。
tsconfig.json
{
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
      "rootDir": "./src",
      "outDir": "./dist",
      "esModuleInterop": true,
      "strict": true
    },
    "include": ["src/**/*"]
}
nodemon.json
{
    "watch": ["src"],   
    "ext": "ts",    
    "exec": "ts-node ./src/index.ts"
}
src/index.ts
import express, { Express, Request, Response } from 'express';

const app: Express = express();
const port = 3000;
-
app.get('/', (req: Request, res: Response) => {
  res.send('Hello World.');
  console.log(`Hello World.`);
});

app.listen(port, () => {
  
});
k8s/pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nodejs-app
spec:
  containers:
  - name: nodejs-app
    image: <Artifact Registoryのパス>/nodejs-app
  • imageは、buildステップと同じ名前を定義します。skaffoldがimage名を検知し、タグ名をレンダリングしてデプロイします。

検証

skaffold dev --profile=nodejs-appを実行します。

curl localhost:3000を実行すると、kubectl port-forwardせずともレスポンスを取得できます。

続いて、ホットリロードの検証をします。
skaffold devを実行した状態で、ファイルを修正します。

src/index.ts
app.get('/', (req: Request, res: Response) => {
-  res.send('Hello World.');
-  console.log(`Hello World.`);
+  res.send('Hoge World.');
+  console.log(`Hoge World.`);
});

ファイルがsyncされます。

この状態で、もう一度curl localhost:3000を実行すると、レスポンスが変わっていることがわかります。

最後に、skaffold devをキャンセルします。
imageのpruneとリモート環境のリソース削除が行われたログが表示されました。

skaffoldの実際

色々skaffoldを触ってみましたが、これ一つで開発が全て効率化するかと言われるとそうではない印象です。
アプリケーションに閉じた開発であれば極力ローカルで開発し、k8s周辺のエコシステムと連携するロジックを追加する場合にskaffoldを上手く活用すると、開発の効率を上げられると思います。

適材適所にskaffoldを使ってみてください。

脚注
  1. https://skaffold.dev/docs/ ↩︎

  2. https://skaffold.dev/docs/references/cli/
    ↩︎

  3. https://skaffold.dev/docs/#skaffold-workflow-and-architecture
    ↩︎

  4. https://skaffold.dev/docs/filesync/ ↩︎

  5. https://skaffold.dev/docs/cleanup/#image-pruning ↩︎

Discussion