【IT】個人開発DjangoをHerokuで本番運用へ(第2回(AWS編②):アップロード専用IAM/疎通テスト/Heroku連携
皆さま、こんにちは
前回に引き続き、
個人開発したDjangoアプリをHerokuで本番運用に向けて環境構築していきます。
ゴール
・S3 に書き込みだけできる 最小権限の IAM ユーザーを用意(削除権限なし)
・Heroku の環境変数に アクセスキーを安全に登録
・boto3 で 単発アップロードと マルチパートアップロードの疎通を確認
・運用の鍵ローテーションとトラブル時の切り分け手順を持つ
前提(第1回の設計)
・バケット:汎用 / ACL 無効 / バージョニング無効 / SSE-S3(AES256)
・ライフサイクル:1 ルール(`<prefix>/` フィルタ)で 5日削除 + 未完了MPU 7日中止
・“落穂拾い”Lambda:7日超の完成済みオブジェクトを削除(削除権限は Lambda のみ)
連載ナビ(5回構成+序章)
第2回(AWS②)(本稿):アップロード専用IAM/疎通テスト/Heroku連携(この記事)
第3回(Heroku①):Config Vars/Release/静的ファイル最小運用
第4回(Heroku②):日次自動バックアップ(Scheduler + `pg_dump` + S3)
第5回(運用):復旧テスト&アラート
※連載回数や内容を変更することがございます。予めご了承ください。
Step 1:アップロード専用 IAM ユーザーを作る(削除なし・最小権限)
1-1. 設計方針(再確認)
アップロード担当(Heroku側):Put だけ。Delete 不可。
削除担当(Lambda):第1回で作成済み。Delete 権限は Lambda ロールのみに付与。
これで誤削除リスクを IAM レベルで遮断します。
1-2. 付与するアクション(最小限)
アップロード(特に マルチパートアップロード)には Put 以外にいくつかの補助アクションが必要です。
`s3:PutObject`(必須)
`s3:AbortMultipartUpload`(MPU途中の中止権限)
`s3:ListBucketMultipartUploads`(バケット側の“進行中MPUの一覧”参照)
`s3:ListMultipartUploadParts`(オブジェクト側の“MPUの部品一覧”参照)
`s3:ListBucket`(prefix 限定で直近アップロードの確認に使用/任意)
※ Get は不要(読み出しは不要なので付けない)
1-3. ポリシー(バケット+特定プレフィックスに限定)
置換:`<YOUR_S3_BUCKET>`, `<YOUR_PREFIX>`(例:`heroku/<APP_NAME>`)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucketLimitedToPrefix",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:ListBucketMultipartUploads"],
"Resource": "arn:aws:s3:::<YOUR_S3_BUCKET>",
"Condition": {
"StringLike": { "s3:prefix": ["<YOUR_PREFIX>/*"] }
}
},
{
"Sid": "PutAndMultipartOnlyWithinPrefix",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::<YOUR_S3_BUCKET>/<YOUR_PREFIX>/*"
}
]
}🔒 付けないもの:`s3:DeleteObject` / `s3:GetObject` / `s3:PutObjectAcl` など。
※ バケットポリシーは不要(非公開のまま。IAM のみで制御)。
1-4. 作成手順(コンソール派)
IAM > ポリシー > 作成 → 上記 JSON を貼付&保存(例:`S3PutOnly-<APP_NAME>`)。
IAM > ユーザー > ユーザーの作成
ユーザー名:`heroku-uploader-<APP_NAME>`
アクセスタイプ:アクセスキー - プログラムによるアクセス
次へ → 「ポリシーを直接アタッチ」にて 1 のポリシーを選択 → 作成。
ユーザー詳細画面で アクセスキーを作成 → CSV を安全な場所に保管。
1-5. 作成手順(CLI派|任意)
# 事前変数
BUCKET="<YOUR_S3_BUCKET>"
PREFIX="<YOUR_PREFIX>"
APP="yourapp"
# ポリシーJSONを policy.json として保存後:
aws iam create-policy --policy-name S3PutOnly-$APP --policy-document file://policy.json
aws iam create-user --user-name heroku-uploader-$APP
aws iam attach-user-policy \
--user-name heroku-uploader-$APP \
--policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/S3PutOnly-$APP
aws iam create-access-key --user-name heroku-uploader-$APP
# => AccessKeyId / SecretAccessKey を控える(CSV推奨)Step 2:Heroku へ環境変数を登録(Config Vars)
Heroku Dashboard → Settings → Reveal Config Vars で以下をセット。

✅ 引用符は不要。`AES256` などはそのまま入力。
✅ 値の変更はデプロイなしで即反映(dyno が再起動されることはあります)。
Step 3:疎通テスト(単発アップロード → マルチパートアップロード)
第1回のバックアップスクリプト(`scripts/s3_backup.sh`)を動かす前に、まずは最小確認を行います。
3-1. 単発アップロード(`PutObject` のみを検証)
Heroku の one-off dyno で1行テスト:
heroku run --app <yourapp> -- python - <<'PY'
import os, boto3, datetime
s3 = boto3.client("s3", region_name=os.getenv("AWS_DEFAULT_REGION"))
bucket = os.getenv("S3_BUCKET"); prefix = os.getenv("S3_PREFIX")
key = f"{prefix}/probes/putobject-{datetime.datetime.utcnow().isoformat()}.txt"
s3.put_object(Bucket=bucket, Key=key, Body=b"hello", ServerSideEncryption=os.getenv("S3_SSE","AES256"))
print("OK:", key)
exit()
PY成功 → S3 に `.../probes/putobject-*.txt` が出来る。
失敗 → 環境変数/権限/リージョンのいずれかが原因。
3-2. マルチパートアップロード(MPU 系権限を検証)
5MB 超なら boto3 の `S3Transfer` が MPU を使用します。強制するテスト:
heroku run --app <yourapp> -- python - <<'PY'
import os, io, boto3, datetime
from boto3.s3.transfer import TransferConfig
size = 8 * 1024 * 1024 # 8MB
body = io.BytesIO(b"x" * size)
cfg = TransferConfig(multipart_threshold=5*1024*1024, multipart_chunksize=5*1024*1024)
s3 = boto3.client("s3", region_name=os.getenv("AWS_DEFAULT_REGION"))
bucket = os.getenv("S3_BUCKET"); prefix=os.getenv("S3_PREFIX")
key = f"{prefix}/probes/mpu-{datetime.datetime.utcnow().isoformat()}.bin"
s3.upload_fileobj(body, bucket, key, ExtraArgs={"ServerSideEncryption": os.getenv("S3_SSE","AES256")}, Config=cfg)
print("OK (multipart):", key)
exit()
PY成功 → `PutObject` / `ListMultipartUploadParts` / `ListBucketMultipartUploads` が機能。
途中で中断しても、ライフサイクルの“未完了MPU 7日中止”が後始末。
🧹 テストファイルの扱い
削除は Lambda の役割なので、アップロード用 IAM には権限がありません。probes/ 配下は少量のまま放置でOK**(5日で自動削除)**。気になる場合は S3 コンソールで手動削除。
Step 4:バックアップスクリプトの“本番一歩手前”実行
ここで `scripts/s3_backup.sh` を新規作成し、Heroku の One-off dyno から動作確認します。
4-1. ファイル作成:`scripts/s3_backup.sh`
# scripts/s3_backup.sh
#!/usr/bin/env bash
set -euo pipefail
export PATH="/app/.heroku/python/bin:/app/.apt/usr/bin:$PATH"
for d in /app/.apt/usr/lib/postgresql/*/bin; do
[ -d "$d" ] && export PATH="$d:$PATH"
done
APP_NAME="mailsession" # ログ表示用
BUCKET="${S3_BUCKET:?S3_BUCKET missing}"
PREFIX="${S3_PREFIX:-heroku/${APP_NAME}}"
SSE="${S3_SSE:-AES256}"
KMS_KEY_ID="${S3_KMS_KEY_ID:-}"
STAMP="$(date -u +%Y%m%d-%H%M%S)"
OUT="/tmp/${APP_NAME}-${STAMP}.dump"
export PGSSLMODE=require # Heroku PG は TLS 前提
echo "[backup] Start pg_dump to ${OUT}"
pg_dump -Fc --no-acl --no-owner "$DATABASE_URL" -f "$OUT"
S3_URI="s3://${BUCKET}/${PREFIX}/daily-${APP_NAME}-${STAMP}.dump"
echo "[backup] Upload to ${S3_URI}"
if [[ "$SSE" == "aws:kms" && -n "$KMS_KEY_ID" ]]; then
aws s3 cp "$OUT" "$S3_URI" --sse aws:kms --sse-kms-key-id "$KMS_KEY_ID"
else
aws s3 cp "$OUT" "$S3_URI" --sse AES256
fi
rm -f "$OUT"
echo "[backup] Done."4-2. `requirements.txt` に追記
boto34-3. One-off dyno で実行
# まず push(または GitHub 連携で自動デプロイ後)
git add scripts/s3_backup.sh requirements.txt
git commit -m "Add pg_dump_to_s3 script"
git push heroku main
# 実行
heroku run --app <yourapp> -- python scripts/s3_backup.sh4-4. うまくいかない時の主な原因
`Unable to locate credentials` → `AWS_*` が未設定/スペルミス。
`AccessDenied` → IAM ポリシーの範囲(バケット/プレフィックス)がズレている。
`Invalid SSE` → `S3_SSE` が `AES256` 以外(本連載は KMS なし前提)。
`pg_dump` 失敗 → `DATABASE_URL` / Postgres アドオンの到達性。
S3 キー場所違い → `S3_PREFIX` の末尾スラッシュ抜け等(想定は `.../<APP_NAME>/daily-...`)。
Step 5:鍵ローテーション & 運用の小ワザ
5-1. ローテーション
新しいアクセスキーを発行(IAM → 当該ユーザー → アクセスキーを作成)。
Heroku の Config Vars を新値に更新(旧値は保持したままでもよい)。
One-off で疎通確認(Step 3 の 1&2 を再実施)。
問題なければ 旧アクセスキーを無効化 → 削除。
5-2. 事故防止の工夫
権限分離:Delete は Lambda のみ(本稿の前提)。
Prefix の固定:`S3_PREFIX=heroku/<APP_NAME>` のようにアプリ毎に分離。
不要な Get を付与しない:読み出し権限は付けない。
ログの確認:アップロード試験・本番実行はCloudWatch(Lambda)とS3 のオブジェクト一覧で確認。
Heroku 変数の“見える化”:`/admin/` を持つなら管理者ページに「現在の S3 先」を read-only 表示するのも有効。
よくある質問(Q&A)
Q. Heroku から AWS に “IP 制限” を掛けたい
A. Heroku の送信元 IP は安定しません。IAM ポリシーの `aws:SourceIp` 条件は実務上つらいです。最小権限化 + プレフィックス限定 + 短期鍵ローテーションでリスクを抑えるのが現実解です。
Q. `awscli` を入れて `aws s3 cp` を使いたい
A. Heroku で apt による `awscli` 導入は失敗しやすい&不要。boto3 で完結しましょう。
Q. KMS(SSE-KMS)を使いたい
A. 本連載の範囲外(将来回)。KMS を使う場合は `SSEKMSKeyId` や `kms:Encrypt` 等の権限・キー ポリシー設計が必要です。
Q. Heroku Scheduler での“毎日バックアップ”は?
A. 第4回で詳述します。現時点は One-off での疎通が通ることがゴール。
この回の成果物(テンプレまとめ)
IAM ポリシー:`S3PutOnly-<APP_NAME>`(上記 JSON)
IAM ユーザー:`heroku-uploader-<APP_NAME>`(アクセスキー発行)
Heroku Config Vars:`AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION / S3_BUCKET / S3_PREFIX / S3_SSE`
疎通テスト:単発(`put_object`)+ MPU(`upload_fileobj` + `TransferConfig`)
次回予告(第3回:Heroku 編①)
Heroku アプリの Config Vars 整理/Release/静的ファイル(Whitenoise) の最小構成を再点検
`DEBUG=false` / `ALLOWED_HOSTS` / `CSRF_TRUSTED_ORIGINS` の本番向け落とし穴
管理者の最初の `migrate` / `createsuperuser` を“正しいフラグ順”で
では
次回に!
前回の記事
過去の記事
