Makefileで環境別デプロイをやってみました

こんにちは、さるまりんです🐒
開発をしていると、ローカル(local)、開発(dev)、ステージング(stg)、本番(prd)といろんな環境を扱います。
各環境ごとに設定やコマンドが異なり、ややこしいうえに事故の原因にもなります。
今回はMakefileを使って環境を切り替え、安全に運用する方法を紹介します。
想定構成は以下の通りです。
  • local: LocalStack
  • dev/stg/prd: AWS
まずは local と dev を対象に、環境切り替えの仕組みを作っていきます。

1. 事前準備

LocalStackを起動

docker run -d --name localstack \
  -p 4566:4566 \
  -e SERVICES=s3 \
  localstack/localstack
起動確認はこれでいけます。
aws --endpoint-url=http://localhost:4566 s3 ls

AWS CLIとdevプロファイル

AWSで開発環境接続用のプロファイルを作ります(なければ)
aws configure --profile my-dev-profile

2. 環境変数ファイルを作る

local, dev用の環境変数を集めたファイルを作ります。

.env.local

ENV=local
LOCALSTACK_ENDPOINT=http://localhost:4566
AWS_REGION=ap-northeast-1
APP_PREFIX=my-app

.env.dev

ENV=dev
AWS_REGION=ap-northeast-1
AWS_PROFILE=my-dev-profile
BUCKET=our-shared-dev-assets
KEY_PREFIX=users/myname/

devのAWS_PROFILEには準備で用意した開発環境接続用のプロファイルを、BUCKETは開発チームメンバー皆で使う共有バケットを設定します。

KEY_PREFIXはバケット上の自分専用の領域を示すためのもので、必ず末尾の/をつけます。

3. LocalStackで動作確認

local環境、LocalStackでそれぞれのコマンドを叩いてみます。

設定を確認

make env

出力例:

ENV=local
AWS_REGION=ap-northeast-1
LOCALSTACK_ENDPOINT=http://localhost:4566
BUCKET=my-app-local-assets
KEY_PREFIX=
OBJKEY=package.txt

デプロイ

make deploy

出力例:

bucket ready: s3://my-app-local-assets
uploaded to s3://my-app-local-assets/package.txt

中身を確認

make listmake fetchで確認します。
make list

2025-08-25 07:20:11       56 package.txt

make fetch

------ downloaded content ------
generated at 2025-08-25 07:20:11 (UTC)
hello from local!
--------------------------------

後片付け

make teardownでバケットごとお掃除します。
make teardown

removed: s3://my-app-local-assets
これで一通りの操作ができました。

4. AWS(dev)にデプロイ

今度は開発環境(dev)、実際にAWSの環境に対しての操作です。

設定確認

make env ENV=dev
出力例:
ENV=dev
AWS_REGION=ap-northeast-1
AWS_PROFILE=my-dev-profile
BUCKET=our-shared-dev-assets
KEY_PREFIX=users/myname/
OBJKEY=package.txt

デプロイ

make deploy ENV=dev

出力例:

uploaded to s3://our-shared-dev-assets/users/myname/package.txt

一覧・取得

こちらもmake listmake fetchで確認します。
make list ENV=dev

2025-08-25 07:25:10       58 package.txt

make fetch ENV=dev

------ downloaded content ------
generated at 2025-08-25 07:25:10 (UTC)
hello from dev!
--------------------------------
アップしたファイルの中身の表示までできました。

5. 自分の領域だけ掃除

後片付けの時にローカルではバケットごと消してしまっていました。
が、みんなで使う環境でそれは危険!なので、自分専用領域だけ掃除するようにしています。

確認

make list-namespace ENV=dev

2025-08-25 07:25:10       58 package.txt

削除

make clean-namespaceで自分のものを掃除します。
make clean-namespace ENV=dev

[plan] will remove objects under: s3://our-shared-dev-assets/users/myname/
...
Type 'YES' to delete ONLY your namespace: YES
[done] removed: s3://our-shared-dev-assets/users/myname/
入力を促されるのでYESとタイプすると自分の領域を掃除できます。
安全に運用するために、localは自由に作成・削除可、AWS(dev)は共有バケット必須として、 KEY_PREFIXで衝突防止、削除は確認付き&自分の領域だけです。
これで怖がらずに作業できますね。

実際のコードです!

Makefile(環境切替・名前空間掃除つき / S3専用)

# =========================
# Makefile: env-aware S3 ops
# =========================

# デフォルト環境(local = LocalStack)
ENV ?= local

# .env 読み込み
ENV_FILE := .env.$(ENV)
ifeq ("$(wildcard $(ENV_FILE))","")
  $(error "$(ENV_FILE) が見つかりません。ENV=local|dev を指定し、対応する .env.* を作成してください")
endif
include $(ENV_FILE)
export

# AWS CLI の切替
ifeq ($(ENV),local)
  # LocalStack: 認証不要。プロファイル指定もしない
  AWSCLI = aws --endpoint-url=$(LOCALSTACK_ENDPOINT) --region $(AWS_REGION)
else
  AWSCLI = aws --profile $(AWS_PROFILE) --region $(AWS_REGION)
endif

# バケット名とオブジェクトキー
# local は自動命名、dev は .env.dev で BUCKET を既存共有バケット名に固定する想定
ifeq ($(ENV),local)
  BUCKET ?= $(APP_PREFIX)-$(ENV)-assets
  KEY_PREFIX ?=
else
  # dev は各自の名前空間(例: users/yourname/)。.env.dev で明示してもOK
  KEY_PREFIX ?= users/$(shell whoami)/
endif

OBJKEY      ?= package.txt
ARTIFACT    ?= artifact/$(OBJKEY)

# ===== ユーティリティ =====
.PHONY: help env prepare setup-local deploy list fetch list-namespace clean-namespace check-bucket teardown clean

help:
	@echo "Usage: make <target> ENV=local|dev"
	@echo
	@echo "General:"
	@echo "  env              使う設定を表示"
	@echo "  prepare          テスト用ファイル作成 (artifact/$(OBJKEY))"
	@echo "  deploy           S3にアップロード(localは自動作成、devは共有バケット必須)"
	@echo "  list             バケット内(local: 直下 / dev: KEY_PREFIX 配下)一覧"
	@echo "  fetch            アップロード済みオブジェクトをDLして表示"
	@echo "  list-namespace   自分の名前空間( KEY_PREFIX )配下の一覧(dev想定)"
	@echo "  clean-namespace  自分の名前空間だけ削除(確認あり・安全)"
	@echo "  setup-local      local 環境だけバケット作成"
	@echo "  teardown         local 環境だけバケットと中身を削除"
	@echo "  clean            生成物削除(artifact/)"

env:
	@echo "ENV=$(ENV)"
	@echo "AWS_REGION=$(AWS_REGION)"
ifeq ($(ENV),local)
	@echo "LOCALSTACK_ENDPOINT=$(LOCALSTACK_ENDPOINT)"
else
	@echo "AWS_PROFILE=$(AWS_PROFILE)"
endif
	@echo "BUCKET=$(BUCKET)"
	@echo "KEY_PREFIX=$(KEY_PREFIX)"
	@echo "OBJKEY=$(OBJKEY)"

# テスト用ファイル作成
$(ARTIFACT):
	@mkdir -p artifact
	@date +"generated at %F %T (%Z)" > $(ARTIFACT)
	@echo "hello from $(ENV)!" >> $(ARTIFACT)

prepare: $(ARTIFACT)

# ===== ガード類 =====
guard-bucket:
	@if [ -z "$(BUCKET)" ]; then \
	  echo "[error] BUCKET is empty"; exit 20; fi

guard-keyprefix:
	@if [ -z "$(KEY_PREFIX)" ] && [ "$(ENV)" != "local" ]; then \
	  echo "[error] KEY_PREFIX is empty (devでは必須)"; exit 21; fi
	@if [ -n "$(KEY_PREFIX)" ]; then \
	  case "$(KEY_PREFIX)" in */) ;; \
	    *) echo "[error] KEY_PREFIX must end with '/' (got '$(KEY_PREFIX)')"; exit 22;; esac; \
	  case "$(KEY_PREFIX)" in /*) \
	    echo "[error] KEY_PREFIX must be relative (no leading '/')"; exit 23;; \
	  esac; \
	fi

# ===== バケット存在/リージョンチェック(devのみ厳格) =====
check-bucket: guard-bucket
ifeq ($(ENV),local)
	@true
else
	@$(AWSCLI) s3api head-bucket --bucket $(BUCKET) >/dev/null 2>&1 || { \
	  echo "[error] bucket not found or no access: $(BUCKET)"; exit 30; }
	@br=$$($(AWSCLI) s3api get-bucket-location --bucket $(BUCKET) --output text 2>/dev/null || echo None); \
	if [ "$$br" != "None" ] && [ "$$br" != "$(AWS_REGION)" ]; then \
	  echo "[error] bucket region=$$br, CLI region=$(AWS_REGION) (mismatch)"; exit 31; \
	fi
endif

# ===== local だけ作成可 =====
setup-local: guard-bucket
ifeq ($(ENV),local)
	-$(AWSCLI) s3 mb s3://$(BUCKET) >/dev/null 2>&1 || true
	@echo "bucket ready: s3://$(BUCKET)"
else
	@echo "[error] setup-local は ENV=local のみ"; exit 2
endif

# アップロード(localは自動作成→アップ、devは存在チェック→アップ)
deploy: prepare guard-bucket guard-keyprefix
ifeq ($(ENV),local)
	-$(AWSCLI) s3 mb s3://$(BUCKET) >/dev/null 2>&1 || true
	$(AWSCLI) s3 cp $(ARTIFACT) s3://$(BUCKET)/$(OBJKEY)
	@echo "uploaded to s3://$(BUCKET)/$(OBJKEY)"
else
	$(MAKE) check-bucket
	$(AWSCLI) s3 cp $(ARTIFACT) s3://$(BUCKET)/$(KEY_PREFIX)$(OBJKEY)
	@echo "uploaded to s3://$(BUCKET)/$(KEY_PREFIX)$(OBJKEY)"
endif

# 一覧
list: guard-bucket guard-keyprefix
ifeq ($(ENV),local)
	$(AWSCLI) s3 ls s3://$(BUCKET)/
else
	$(MAKE) check-bucket
	@echo "[list] s3://$(BUCKET)/$(KEY_PREFIX)"
	$(AWSCLI) s3 ls s3://$(BUCKET)/$(KEY_PREFIX)
endif

# ダウンロード&内容確認
fetch: guard-bucket guard-keyprefix
ifeq ($(ENV),local)
	$(AWSCLI) s3 cp s3://$(BUCKET)/$(OBJKEY) artifact/$(OBJKEY).downloaded
else
	$(MAKE) check-bucket
	$(AWSCLI) s3 cp s3://$(BUCKET)/$(KEY_PREFIX)$(OBJKEY) artifact/$(OBJKEY).downloaded
endif
	@echo "------ downloaded content ------"
	@cat artifact/$(OBJKEY).downloaded || true
	@echo "--------------------------------"

# 名前空間の一覧/掃除(dev向け、安全版)
list-namespace: check-bucket guard-keyprefix
	@echo "[list] s3://$(BUCKET)/$(KEY_PREFIX)"
	$(AWSCLI) s3 ls s3://$(BUCKET)/$(KEY_PREFIX) --recursive || true

clean-namespace: check-bucket guard-keyprefix
	@echo "[plan] will remove objects under: s3://$(BUCKET)/$(KEY_PREFIX)"
	@$(AWSCLI) s3 ls s3://$(BUCKET)/$(KEY_PREFIX) --recursive | head -n 20 || true
	@echo "..."
	@cnt=$$($(AWSCLI) s3 ls s3://$(BUCKET)/$(KEY_PREFIX) --recursive | wc -l); \
	echo "[info] objects to delete: $$cnt";
	@read -p "Type 'YES' to delete ONLY your namespace: " ans; \
	[ "$$ans" = "YES" ] || { echo "[abort]"; exit 1; }
	$(AWSCLI) s3 rm s3://$(BUCKET)/$(KEY_PREFIX) --recursive
	@echo "[done] removed: s3://$(BUCKET)/$(KEY_PREFIX)"

# 破壊系は local 限定
teardown: guard-bucket
ifeq ($(ENV),local)
	-$(AWSCLI) s3 rm s3://$(BUCKET)/ --recursive || true
	-$(AWSCLI) s3 rb s3://$(BUCKET) --force || true
	@echo "removed: s3://$(BUCKET)"
else
	@echo "[warn] teardown は ENV=local 限定です(共有環境では実施しない)"
endif

clean:
	@rm -rf artifact
	@echo "cleaned: ./artifact"

.env.local(LocalStack 用)

ENV=local
LOCALSTACK_ENDPOINT=http://localhost:4566
AWS_REGION=ap-northeast-1
APP_PREFIX=my-app
# BUCKET は自動で my-app-local-assets になります

.env.dev(AWS / 共有バケット利用)

ENV=dev
AWS_REGION=ap-northeast-1
AWS_PROFILE=my-dev-profile         # あなたのAWSプロファイル名
BUCKET=our-shared-dev-assets       # 既存の共有バケット名(作らない)
KEY_PREFIX=users/myname/           # 自分の名前空間(末尾 / 必須)
Makefileが長くなりましたね。
今回はS3を例にしましたが、EC2へのデプロイやDB登録など、より複雑な運用にも応用できます。AWSの豊富なサービスに合わせて、Makefileを工夫していけるのが面白そうです。
読んでくださってありがとうございました。
それではまた!