Ritolabo
  1. Home
  2. Kubernetes
  3. Tilt
  4. Tilt で DB の起動と初期データの投入をまとめる

Tilt で DB の起動と初期データの投入をまとめる

  • 公開日
  • カテゴリ:Tilt
  • タグ:Kubernetes,Tilt,学習メモ
Tilt で DB の起動と初期データの投入をまとめる

ローカルの Kubernetes で API を開発するとき、API が DB を使うなら、DB もクラスタの中に用意することが多い。その場合、DB を起動し、接続を受け付けられるようになるまで待ち、テーブルと初期データを入れてから API を起動する、という順番の決まった作業が、API の開発とは別に必要になる。

Tilt を使うと、この DB の用意も API と一緒に任せられる。tilt up を 1 回実行するだけで、DB の起動、初期データの投入、API の起動が順に行われる。

contents

  1. 構成
  2. DB の Deployment と Service
    1. 接続情報の Secret
    2. readinessProbe
  3. 初期データ
  4. API
  5. Tiltfile
    1. DB の resource
    2. db-init
    3. resource_deps
    4. trigger mode
  6. tilt up
  7. 初期データの入れ直し
  8. tilt down
  9. 参考 URL

構成

ディレクトリの構成は次のとおり。

app/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile          # 本番用
├── Dockerfile.tilt     # Tilt で開発するときだけ使う
├── .dockerignore
├── .gitignore
├── db/
│   └── init.sql        # テーブルと初期データ
├── k8s/
│   ├── deployment.yaml # API の Deployment
│   ├── service.yaml    # API の Service
│   ├── postgres.yaml   # DB の Deployment と Service
│   └── db-secret.yaml  # DB の接続情報の Secret
└── Tiltfile

Tilt の管理画面(http://localhost:10350/)の一覧には、Tiltfile の読み込みの行のほかに、次の 4 つの resource(Tilt が管理する処理の単位)が並ぶ。

  • postgres: DB の Deployment と Service を apply する。Pod が Ready になったら準備完了
  • db-init: DB にテーブルを作り、初期データを入れる
  • hello-api-compile: ローカルの開発環境で API の go build を実行する
  • hello-api: API の image を作り、Deployment と Service を apply する

動く順番は次のとおり。矢印の先の resource は、矢印の元の resource の準備ができてから動く。

postgres  →  db-init  ──────┐
                            ├→  hello-api
hello-api-compile  ─────────┘

hello-api は、hello-api-compile と db-init の両方が終わるのを待つ。

DB の Deployment と Service

DB には PostgreSQL を使う。

# k8s/postgres.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:18-alpine
          env:
            - name: POSTGRES_USER
              value: hello
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: hello-db-credentials
                  key: POSTGRES_PASSWORD
            - name: POSTGRES_DB
              value: hello
          ports:
            - containerPort: 5432
          readinessProbe:
            exec:
              command: ["pg_isready", "-h", "127.0.0.1", "-U", "hello", "-d", "hello"]
            periodSeconds: 2
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

PostgreSQL の公式 image は、最初の起動で env の POSTGRES_USER・POSTGRES_PASSWORD・POSTGRES_DB の値を使って、ユーザーとデータベースを作る。パスワードは YAML に直接書かず、secretKeyRef で Secret hello-db-credentials の POSTGRES_PASSWORD から読む。

また、データを Pod の外に置く設定(PersistentVolumeClaim)は行っていない。Pod やコンテナが作り直されると、DB のデータも消える。

接続情報の Secret

DB のパスワードは、Secret(パスワードなどの機密情報を入れる Kubernetes のデータ)に置く。DB と API の Deployment は、どちらもこの Secret を参照する。

# k8s/db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: hello-db-credentials
type: Opaque
stringData:
  POSTGRES_PASSWORD: hello-dev-password

値は開発用のダミー(hello-dev-password はこの記事用のもの)。このファイルは開発環境の Secret を作るためのもので、本番の Secret はこのファイルでは作らない。本番では、Secret を暗号化して管理するか、外部のサービスから取得する。Deployment は Secret の名前とキーを参照するだけなので、開発と本番で同じ Deployment の YAML を使える。

readinessProbe

Pod の Ready は、Kubernetes がその Pod に通信を送ってよいと判断した状態。readinessProbe は、Pod がこの状態かどうかを Kubernetes が確かめるための設定で、設定した確認(ここではコマンド)が成功すると Pod が Ready になる。

ここでは、コンテナの中で pg_isready(PostgreSQL が接続を受け付けるかを確かめるコマンド)を 2 秒ごとに実行する。後述の resource_deps で、Tilt はこの Ready を待ってから db-init を動かす。

-h 127.0.0.1 は、データベースを作り終える前に Pod が Ready にならないようにする指定。PostgreSQL の公式 image は、最初の起動で次の順に動く。

  1. ネットワーク経由の接続(TCP)を受け付けない状態で PostgreSQL を一時的に起動し、データベースを作る
  2. PostgreSQL を起動し直し、ネットワーク経由の接続も受け付ける

-h を付けないと、pg_isready はコンテナの中だけで使う接続口(Unix ドメインソケット)で確かめるので、1. の途中で Ready になることがある。Tilt は一度 Ready になれば待つのをやめて db-init を動かすので、その直後に 1. の PostgreSQL が止まると、db-init の接続が失敗する。-h 127.0.0.1 を付けて TCP で確かめれば、2. のあとにだけ Ready になる。

初期データ

DB に作るテーブルと入れるデータは、SQL のファイル db/init.sql に書く。

-- db/init.sql
DROP TABLE IF EXISTS greeting;

CREATE TABLE greeting (
  message text NOT NULL
);

INSERT INTO greeting (message) VALUES ('Hello from PostgreSQL');

テーブル greeting に、API が返す文字列を 1 行入れる。最初にテーブルを消すので、何度実行しても 1 行だけ入った状態に戻る。

API

GET / のリクエストを受けるたびに、テーブル greeting から文字列を読んで返す API。DB への接続には、Go のライブラリ pgx を使う。

// main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/jackc/pgx/v5/pgxpool"
)

func main() {
	ctx := context.Background()

	pool, err := pgxpool.New(ctx, "")
	if err != nil {
		log.Fatalf("invalid database settings: %v", err)
	}
	if err := pool.Ping(ctx); err != nil {
		log.Fatalf("cannot connect to database: %v", err)
	}
	log.Println("connected to database")

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s %s", r.Method, r.URL.Path)

		var message string
		err := pool.QueryRow(r.Context(), "SELECT message FROM greeting LIMIT 1").Scan(&message)
		if err != nil {
			log.Printf("query failed: %v", err)
			http.Error(w, "database error", http.StatusInternalServerError)
			return
		}
		fmt.Fprintln(w, message)
	})

	log.Println("listening on :8000")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

起動時に pool.Ping で DB に一度接続し、接続できなければエラーで終了する。そのため、API は DB の準備ができてから起動する必要がある。

pgxpool.New に渡す接続先の文字列は空にしている。空にすると、pgx は PostgreSQL のクライアントライブラリ(libpq)が決めている環境変数(PGHOST・PGPASSWORD など)から接続先を読む(libpq の Environment Variables)。

これらの環境変数は、API の Deployment で渡す。

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-api
  template:
    metadata:
      labels:
        app: hello-api
    spec:
      containers:
        - name: api
          image: hello-api
          env:
            - name: PGHOST
              value: postgres
            - name: PGPORT
              value: "5432"
            - name: PGUSER
              value: hello
            - name: PGDATABASE
              value: hello
            - name: PGSSLMODE
              value: disable
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: hello-db-credentials
                  key: POSTGRES_PASSWORD
          ports:
            - containerPort: 8000

各環境変数の値は、次の箇所と対応する。

環境変数意味対応する箇所
PGHOST接続先のホストk8s/postgres.yaml の Service の name
PGPORT接続先のポートk8s/postgres.yaml の Service の port
PGUSERユーザーk8s/postgres.yaml の POSTGRES_USER
PGDATABASEデータベースk8s/postgres.yaml の POSTGRES_DB
PGPASSWORDパスワードSecret の POSTGRES_PASSWORD

パスワードは、DB の POSTGRES_PASSWORD と API の PGPASSWORD が Secret の同じキーを参照する。パスワードを変えるときは、Secret の 1 か所を直せばよい。パスワード以外の接続先は機密情報ではないので、Deployment に直接書いている。

PGSSLMODE の disable は、通信を暗号化しない指定。本番の DB には TLS(暗号化した通信)で接続し、verify-full などで DB の証明書も確かめる。この記事の PostgreSQL は TLS を有効にしていないので、暗号化しないことを明示している。

API の Service(k8s/service.yaml)は、8000 番ポートで受ける ClusterIP の Service。

Tiltfile

# Tiltfile
load('ext://restart_process', 'docker_build_with_restart')

# DB
k8s_yaml(['k8s/db-secret.yaml', 'k8s/postgres.yaml'])
k8s_resource('postgres', objects=['hello-db-credentials:secret'])

local_resource(
    'db-init',
    'kubectl exec -i deploy/postgres -- psql -U hello -d hello -v ON_ERROR_STOP=1 < db/init.sql',
    deps=['db/init.sql'],
    trigger_mode=TRIGGER_MODE_MANUAL,
    resource_deps=['postgres'],
)

# API
local_resource(
    'hello-api-compile',
    'CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .',
    deps=['main.go', 'go.mod', 'go.sum'],
)

docker_build_with_restart(
    'hello-api',
    '.',
    dockerfile='Dockerfile.tilt',
    entrypoint=['/app/build/hello-api-server'],
    only=['./build'],
    live_update=[
        sync('./build', '/app/build'),
    ],
)

k8s_yaml(['k8s/deployment.yaml', 'k8s/service.yaml'])

k8s_resource(
    'hello-api',
    port_forwards=8000,
    resource_deps=['hello-api-compile', 'db-init'],
)

API の部分(hello-api-compile、docker_build_with_restart、Dockerfile.tilt)は、image を作り直さずにソースの変更をコンテナへ反映する Live Update の設定で、この記事では説明を省く。設定の内容は Tilt の Live Update: image を作り直さずにコンテナを更新する で扱っている。

port_forwards=8000 は、ローカルの開発環境の localhost:8000 を API の Pod の 8000 番につなぐ指定。

DB の resource

k8s_yaml(['k8s/db-secret.yaml', 'k8s/postgres.yaml'])
k8s_resource('postgres', objects=['hello-db-credentials:secret'])

この 2 行で、DB を管理画面の一覧の resource postgres にする。

k8s_yaml(['k8s/db-secret.yaml', 'k8s/postgres.yaml']) で、Secret と DB の YAML を Tilt に渡す。Tilt は渡された YAML の中から Pod を作るもの(Deployment など)を探し、その名前で resource を作る(Tiltfile Concepts)。Deployment postgres から resource postgres ができ、Service postgres も同じ resource にまとめられる。

Secret は、DB の Pod がパスワードを読むためのものなので、DB と同じ postgres の resource に含めたい。ただ、Tilt が自動で resource にまとめるのは Pod を作るもの(Deployment)と Service だけで、Secret は自動では postgres の resource に入らない。

そこで k8s_resource('postgres', objects=['hello-db-credentials:secret']) と書く。objects は、resource に追加で含める Kubernetes のデータを「名前:種類」の形で書く引数で、ここでは Secret hello-db-credentials を postgres の resource に含める(Tiltfile Concepts)。これで Secret は、DB の Deployment と Service と一緒に apply される。

db-init

local_resource(
    'db-init',
    'kubectl exec -i deploy/postgres -- psql -U hello -d hello -v ON_ERROR_STOP=1 < db/init.sql',
    deps=['db/init.sql'],
    trigger_mode=TRIGGER_MODE_MANUAL,
    resource_deps=['postgres'],
)

local_resource は、ローカルの開発環境でコマンドを実行する resource。deps には、変更を見張るファイルを書く(db-init では db/init.sql)。変更があったときにすぐ実行するかどうかは、後述の trigger mode で決まる。

db-init は次のコマンドを実行する。

kubectl exec -i deploy/postgres -- psql -U hello -d hello -v ON_ERROR_STOP=1 < db/init.sql
部分意味
kubectl exec deploy/postgres -- ...Deployment postgres の Pod の中で、-- のあとのコマンドを実行する
-iローカルの開発環境の標準入力を、Pod の中のコマンドに渡す
psql -U hello -d hellopsql(PostgreSQL に SQL を送るコマンド。postgres の image に入っている)で、ユーザー hello としてデータベース hello に接続する
-v ON_ERROR_STOP=1SQL の途中でエラーが出たら止まり、失敗として終了する
< db/init.sqldb/init.sql の中身を、psql への入力として渡す(local_resource のコマンドはシェルで実行されるので、< が使える)

ローカルの開発環境に PostgreSQL のコマンドは入れず、Pod の中の psql を使う。

resource_deps

local_resource(
    'db-init',
    ...
    resource_deps=['postgres'],
)

k8s_resource(
    'hello-api',
    ...
    resource_deps=['hello-api-compile', 'db-init'],
)

resource_deps は、ある resource を、別の resource の準備ができるまで動かさない指定で、待つ相手の resource の名前を書く。準備ができた状態は、相手の resource の種類ごとに次のとおり(Resource Dependencies)。

相手の resource準備ができた状態
local_resourceコマンドが 1 回成功した
Pod を作る resource(Deployment など)Pod が動いていて、すべてのコンテナが Ready

この Tiltfile では次の 2 か所で使う。

  • db-init の resource_deps=['postgres']: DB の Pod が Ready になってから、初期データを入れる。DB の YAML の readinessProbe がここで効く
  • hello-api の resource_deps=['hello-api-compile', 'db-init']: 実行ファイルができて、初期データが入ってから API を起動する。db-init が postgres を待つので、hello-api も DB の Ready を待つことになる

resource_deps が効くのは、tilt up のあとの最初の 1 回だけ。待つ相手の準備が一度できたあとは、db-init を実行し直しても、hello-api の Pod は作り直されない。

trigger mode

local_resource(
    'db-init',
    ...
    deps=['db/init.sql'],
    trigger_mode=TRIGGER_MODE_MANUAL,
    ...
)

trigger mode は、resource が自動で動くか、ボタンで動くかの設定(Manual Update Control)。

trigger modedeps のファイルが変わったとき
TRIGGER_MODE_AUTO(既定)すぐに実行する
TRIGGER_MODE_MANUAL実行しない。変更があったことを管理画面に表示し、ボタンが押されたら実行する

hello-api-compile は AUTO なので、main.go を保存するとすぐに go build が実行される。db-init を MANUAL にしているのは、テーブルを消して作り直す SQL が db/init.sql の保存のたびに実行されると、開発中に DB に入れたデータが消えるため。

tilt up の最初の 1 回は、MANUAL でも実行される。最初に実行するかどうかは auto_init という別の引数で決まり、指定しなければ実行する。

tilt up

Tiltfile のあるディレクトリで tilt up を実行すると、resource は次の順に動く。

  1. hello-api-compile が go build を実行し、終わると postgres が apply される(Tilt は local_resource を先に実行する)
  2. postgres の Pod が Ready になると、db-init が動く
  3. hello-api-compile と db-init の両方が終わると、hello-api が image を作り、API の Pod を起動する

管理画面に出る db-init のログは次のとおり。最初はテーブルがないので、DROP TABLE IF EXISTS は何もせずに NOTICE を出し、そのあとテーブルを作って 1 行入れている。

Running cmd: kubectl exec -i deploy/postgres -- psql -U hello -d hello -v ON_ERROR_STOP=1 < db/init.sql
DROP TABLE
NOTICE:  table "greeting" does not exist, skipping
CREATE TABLE
INSERT 0 1

すべての resource の準備ができたら、Pod と API の返事を確認する。postgres の Pod は hello-api の Pod より先に作られ、どちらも READY が 1/1 になっている。API は、DB のテーブル greeting から読んだ文字列を返す。

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
hello-api-855db69c69-snq97   1/1     Running   0          102s
postgres-7b478dbcb4-xbjn4    1/1     Running   0          118s

$ curl http://localhost:8000/
# => Hello from PostgreSQL

初期データの入れ直し

db/init.sql の INSERT の行を変えて保存する。

INSERT INTO greeting (message) VALUES ('Hello from PostgreSQL, reset');

db-init は MANUAL なので、保存しても実行されず、API の返事も変わらない。

$ curl http://localhost:8000/
# => Hello from PostgreSQL

管理画面で db-init の行にある円形の矢印のボタン(マウスを載せると「Trigger update」と出る)を押すと、db-init が同じコマンドを実行し、テーブルを作り直して新しい行を入れる。

Tilt の管理画面の db-init の行。円形の矢印のボタンにマウスを載せると Trigger update と表示される

そのあと API の返事と Pod を確認すると、API の返事は変わるが、Pod の名前は tilt up のあとと同じ。postgres と hello-api の Pod は入れ替わっておらず、変わったのは DB の中のデータだけ。

$ curl http://localhost:8000/
# => Hello from PostgreSQL, reset

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
hello-api-855db69c69-snq97   1/1     Running   0          2m58s
postgres-7b478dbcb4-xbjn4    1/1     Running   0          3m14s

tilt down

tilt up を実行しているターミナルで Ctrl+C を押してから、同じディレクトリで tilt down を実行する。Tiltfile の k8s_yaml で渡した API と DB の Deployment と Service、Secret が、すべて削除される。

$ tilt down
Loading Tiltfile at: .../app/Tiltfile
Successfully loaded Tiltfile (772.888833ms)
Deleting kubernetes objects:
→ Deployment/hello-api
→ Service/hello-api
→ Deployment/postgres
→ Service/postgres
→ Secret/hello-db-credentials

$ kubectl get deployments,services
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   4d6h

DB のデータは Pod の中にしかないので、データも消える。次に tilt up を実行すると、db-init が db/init.sql の中身を入れ直す。

まとめ

  • API が使う DB も、YAML を k8s_yaml で渡せば Tilt の resource になり、tilt up で API と一緒に起動する
  • resource_deps は、相手の resource の準備ができるまで待つ。相手が Deployment などなら Pod の Ready を待つので、DB の readinessProbe で「接続を受け付けられる」状態を定義しておく
  • DB の初期化は local_resource で行う。ローカルの開発環境で kubectl exec を実行し、Pod の中の psql に SQL を渡す
  • trigger_mode=TRIGGER_MODE_MANUAL にした resource は、ファイルが変わっても自動では実行されず、管理画面のボタンで実行する。tilt up の最初の 1 回は実行される
  • tilt down で、k8s_yaml で渡した DB と API の Deployment と Service、Secret が削除される

参考 URL


[Prev] Tilt の Live Update: image を作り直さずにコンテナを更新する

Author

rito

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級