みなさん、エンジニア楽しんでいますか?
Goの学習をしていたところ、Makefileというのが出てきました。
実務では触れたことがなく、調べてところGoの開発で使われることがあるとのことで、備忘録的に調べたことを共有します。
同じように初めてMakefileを書く方向けに、実際に使っているMakefileを例にして書き方を解説します。
環境はmacOSで、DBはDocker Compose上のPostgreSQL、マイグレーションは golang-migrate のCLIで実行しています。
Makefileとは
Makefileは、make コマンドが読み込むファイルです。
もともとはC言語のビルドに使われていたもので、「このファイルが変わったら、このコマンドで作り直す」という手順を書いておくためのものでした。
Goでは go build がビルドを全部やってくれるので、この使い方はあまりしません。
代わりに、長いコマンドに短い名前を付けておく用途で使われています。
package.json や composer.json の scripts と同じような使い方です。
私の場合、マイグレーションのたびにこのコマンドを打っていました。
migrate -path migrations -database "postgres://stockade:stockade@localhost:5432/stockade?sslmode=disable" up
これをMakefileに書いておくと、make migrate-up で実行できるようになります。
ファイル名は Makefile で、拡張子は付けません。
小文字の makefile でも動きますが、Makefile と書くのが一般的です。
置き場所はプロジェクトのルートです。
まずは書いてみる
DBのコンテナを起動するコマンドを登録してみます。
up:
docker compose up -d
up: の部分をターゲット、その下の行をレシピと呼びます。
up という名前は自分で決めたもので、make up と打つと up のレシピが実行されます。
$ make up
docker compose up -d
実行したコマンドが最初に表示され、そのあとにdocker composeの出力が続きます。
表示させたくない場合は、レシピの先頭に @ を付けます。
レシピの行は、必ずタブでインデントします。
スペースでインデントすると、次のエラーになります。
$ make up
Makefile:2: *** missing separator. Stop.
エラー文にはタブのことが書かれていないので、初見だと原因が分かりません。
このエラーが出たら、エディタの設定でタブがスペースに変換されていないか確認してください。
.PHONYを付ける
Makeは、ターゲット名を「これから作るファイルの名前」として扱います。
そのため、フォルダに up という名前のファイルがあると、もう作ってあると判断してレシピを実行しません。
$ touch up
$ make up
make: 'up' is up to date.
ファイルを作らないターゲットは、.PHONY に書いておきます。
.PHONY に書いたターゲットは、同じ名前のファイルがあっても毎回実行されます。
.PHONY: up
up:
docker compose up -d
.PHONY は、Makeにもともと用意されている名前です。
複数のコマンドを書く
同じ書き方でターゲットを増やしていきます。
.PHONY: up down reset
up:
docker compose up -d
down:
docker compose down
reset:
docker compose down -v
docker compose up -d
reset のように、レシピは複数行書けて、上の行から順番に実行されます。
ただし、1行ごとに別のシェルで実行されるので、cd で移動しても次の行には引き継がれません。
bad:
cd migrations
pwd
$ make bad
cd migrations
pwd
/path/to/stockade
migrations に移動したつもりでも、pwd は元のフォルダを表示しています。
移動してから何かしたい場合は、cd migrations && pwd のように1行で書きます。
また、make とだけ打つと、一番上に書いたターゲットが実行されます。
このMakefileなら up が実行されます。
変数を渡す
マイグレーションファイルを作るコマンドは、ファイル名を毎回変える必要があります。
こういう場合は、変数を使います。
migrate-create:
migrate create -ext sql -dir migrations -seq $(NAME)
make migrate-create NAME=add_index
Makefileの中の $(NAME) が、コマンドで渡した add_index に置き換わって実行されます。
Makeの変数は $(名前) の形で書き、NAME の部分は自分で決めた名前です。
実際にどんなコマンドになるかは、make -n で確認できます。
-n を付けると、コマンドを実行せずに表示だけしてくれます。
$ make -n migrate-create NAME=add_index
migrate create -ext sql -dir migrations -seq add_index
.envを読み込む
DBの接続情報は .env に書いていて、最初はこうなっていました。
POSTGRES_USER=stockade
POSTGRES_PASSWORD=stockade
POSTGRES_DB=stockade
POSTGRES_PORT=5432
DATABASE_URL=postgres://stockade:stockade@localhost:5432/stockade?sslmode=disable
POSTGRES_ で始まる4つはDocker ComposeがDBを作るときに使い、DATABASE_URL はアプリとmigrateが接続に使います。
ユーザー名とパスワードを2回書いているので、パスワードを変えたときに DATABASE_URL を直し忘れそうです。
なので、.env には4つだけを残して、DATABASE_URL はMakefileで組み立てることにしました。
include .env
export
DATABASE_URL := postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable
include .env で、.env の中身をMakefileに読み込んでいます。
これで、.env に書いた POSTGRES_USER などを $(POSTGRES_USER) で使えるようになります。
export は、Makefileの変数を環境変数としてレシピのコマンドに渡す指定です。
何も付けずに export とだけ書くと、Makefileで作った DATABASE_URL も含めて、全部の変数が渡ります。
これを書かないと、Makefileの中では $(DATABASE_URL) が使えても、レシピから起動したアプリからは読めません。
:= は、その行の時点で値を確定させる代入です。
include、export、:= はMakeの機能で、DATABASE_URL は自分で決めた名前です。
.envの書き方に注意
include は .env をMakefileとして読み込むので、シェルと同じ感覚で書くと値が変わってしまうことがあります。
以下は、それぞれ組み立てた DATABASE_URL がどうなるかです。
# クォートも値に含まれる → postgres://stockade:"stockade"@...
POSTGRES_PASSWORD="stockade"
# $w が変数として展開されて消える → postgres://stockade:paord@...
POSTGRES_PASSWORD=pa$word
# コメントの前の空白が値に残る → postgres://stockade:stockade @...
POSTGRES_PASSWORD=stockade # ローカル用
.env の値はクォートで囲まず、$ や行末のコメントも使わないようにしておきましょう。
また、.env が無いとMakefile自体が動きません。
$ make up
Makefile:1: .env: No such file or directory
make: *** No rule to make target '.env'. Stop.
cloneした直後など、.env を作り忘れているとこのエラーが出ます。
完成したMakefile
今使っているMakefileです。
include .env
export
DATABASE_URL := postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable
.PHONY: up down reset migrate-up migrate-down migrate-create migrate-version psql test
# DB コンテナを起動する
up:
docker compose up -d
# DB コンテナを止める(データは残る)
down:
docker compose down
# データごと消して作り直す
reset:
docker compose down -v
docker compose up -d
# マイグレーションを最新まで適用する
migrate-up:
migrate -path migrations -database "$(DATABASE_URL)" up
# マイグレーションを1つ戻す
migrate-down:
migrate -path migrations -database "$(DATABASE_URL)" down 1
# マイグレーションファイルを作る(例:make migrate-create NAME=add_index)
migrate-create:
migrate create -ext sql -dir migrations -seq $(NAME)
# 今どのバージョンまで適用されているか
migrate-version:
migrate -path migrations -database "$(DATABASE_URL)" version
# コンテナの中の psql に入る
psql:
docker compose exec db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB)
# テストを実行する
test:
gotestsum --format testname -- -count=1 ./...
レシピの中の migrate はgolang-migrateのコマンドで、gotestsum はテスト結果を見やすく表示してくれるツールです。
DBを起動してテーブルを作り、中身を確認するところまでは、この3つで済みます。
$ make up
docker compose up -d
[+] up 14/14
✔ Image postgres:17-alpine Pulled 6.0s
✔ Network stockade_default Created 0.0s
✔ Volume stockade_pgdata Created 0.0s
✔ Container stockade-db Started 0.2s
$ make migrate-up
migrate -path migrations -database "postgres://stockade:stockade@localhost:5432/stockade?sslmode=disable" up
1/u init_schema (21.91525ms)
$ make psql
docker compose exec db psql -U stockade -d stockade
psql (17.11)
Type "help" for help.
make migrate-upで表示されるコマンドには、パスワードがそのまま出ています。
ローカル用の値なので私はそのままにしていますが、気になる場合はその行に @ を付けてください。
おわりに
学習程度で使っているだけですが、結構面白いなと思いました。
特にdocker系のコマンドなど、長くて実行するのが面倒だなと感じていたのですが、そう感じた時はMakefileに書こーってなります。
タブでインデントすることと、.PHONYを付けることさえ守ればすぐに書けると思います。