こんにちは、さるまりんです🐒
開発をしていると、ローカル(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 listとmake 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 listとmake 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を工夫していけるのが面白そうです。
読んでくださってありがとうございました。
それではまた!