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

ローカルの Kubernetes で API を開発するとき、API が DB を使うなら、DB もクラスタの中に用意することが多い。その場合、DB を起動し、接続を受け付けられるようになるまで待ち、テーブルと初期データを入れてから API を起動する、という順番の決まった作業が、API の開発とは別に必要になる。
Tilt を使うと、この DB の用意も API と一緒に任せられる。tilt up を 1 回実行するだけで、DB の起動、初期データの投入、API の起動が順に行われる。
contents
構成
ディレクトリの構成は次のとおり。
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 は、最初の起動で次の順に動く。
- ネットワーク経由の接続(TCP)を受け付けない状態で PostgreSQL を一時的に起動し、データベースを作る
- 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 hello | psql(PostgreSQL に SQL を送るコマンド。postgres の image に入っている)で、ユーザー hello としてデータベース hello に接続する |
-v ON_ERROR_STOP=1 | SQL の途中でエラーが出たら止まり、失敗として終了する |
< db/init.sql | db/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 mode | deps のファイルが変わったとき |
|---|---|
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 は次の順に動く。
hello-api-compileがgo buildを実行し、終わるとpostgresが apply される(Tilt はlocal_resourceを先に実行する)postgresの Pod が Ready になると、db-initが動く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 が同じコマンドを実行し、テーブルを作り直して新しい行を入れる。

そのあと 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
- [official] Resource Dependencies | Tilt
- [official] Setting up Local Commands, Servers, and Tests | Tilt
- [official] Play and Pause Resources with Manual Update Control | Tilt
- [official] Tiltfile Concepts | Tilt
- [official] Tiltfile API Reference: local_resource | Tilt
- [official] Tiltfile API Reference: k8s_resource | Tilt
- [official] Liveness Probe、Readiness ProbeおよびStartup Probeを使用する | Kubernetes
- [official] postgres - Official Image | Docker Hub
- [official] PostgreSQL: Documentation: 18: 32.15. Environment Variables
- GitHub - jackc/pgx: PostgreSQL driver and toolkit for Go · GitHub

