運用ランブック¶
前提: AWS CLI プロファイルの設定が必要です。AWS CLI セットアップ を参照してください。
以降のコマンドは事前に
aws sso login済みでAWS_PROFILEが設定されている前提です。# dev環境 export AWS_PROFILE=rikako-development-sso # shared環境(ECR操作時) export AWS_PROFILE=rikako-shared-sso
環境情報¶
| 項目 | dev環境 | prod環境 |
|---|---|---|
| Lambda (公開API) | rikako-api-development |
rikako-api-production |
| Lambda (管理API) | rikako-admin-api-development |
rikako-admin-api-production |
| Lambda (Slack 通知) | rikako-slack-notifier-development |
rikako-slack-notifier-production |
| 公開API エンドポイント | https://api.dev.rikako.org (API Gateway) |
https://api.rikako.org (API Gateway) |
| 管理画面 | https://admin.dev.rikako.org (Basic Auth) |
https://admin.rikako.org (Basic Auth) |
| 管理API | https://admin.dev.rikako.org/api |
https://admin.rikako.org/api |
| 画像CDN | https://image.dev.rikako.org/ |
https://image.rikako.org/ |
| コンテンツCDN | https://content.dev.rikako.org/ |
https://content.rikako.org/ |
| 画像S3 | rikako-images-development |
rikako-images-production |
| コンテンツS3 | rikako-content-development |
rikako-content-production |
| 管理画面S3 | rikako-admin-development |
rikako-admin-production |
| Neon プロジェクト ID | muddy-tree-64549662 (ap-southeast-1) |
fragrant-poetry-87067174 (ap-southeast-1) |
| Neon エンドポイント | ep-raspy-lab-a1wo0g6n |
ep-misty-unit-aoxkoz1d |
| ECR (shared) | 579039992557.dkr.ecr.ap-northeast-1.amazonaws.com/rikako-api |
同左 |
| CloudWatch Dashboard | rikako-dev |
rikako-prod |
| AWSアカウント | 197865631794 | 211125415945 |
| AWS Profile | rikako-development-sso |
rikako-production-sso |
Shared 環境(ECR を共有): AWS アカウント 579039992557 / プロファイル rikako-shared-sso。
1. デプロイ手順¶
公開API / 管理API¶
Dev: mainブランチへのマージで自動デプロイ(GitHub Actions)。
# 手動トリガー(Dev)
gh workflow run "Deploy API Dev" --repo takoikatakotako/rikako --ref main
gh workflow run "Deploy Admin API Dev" --repo takoikatakotako/rikako --ref main
# 状況確認
gh run list --repo takoikatakotako/rikako --limit 5
Prod: 手動 dispatch のみ(自動デプロイなし)。
gh workflow run "Deploy API Prod" --repo takoikatakotako/rikako --ref main
gh workflow run "Deploy Admin API Prod" --repo takoikatakotako/rikako --ref main
デプロイフロー¶
- Dockerイメージをビルド(
app/Dockerfile.lambda/app/Dockerfile.admin) - ECRにプッシュ(タグ:
dev/prod) aws lambda update-function-codeで Lambda 関数のイメージを更新aws lambda wait function-updatedで更新完了まで待機/healthエンドポイントでヘルスチェック
管理画面フロントエンド¶
mainブランチへのマージで admin/ 配下に変更がある場合に自動デプロイ。
# 手動デプロイ
gh workflow run "Deploy Admin Frontend Dev" --repo takoikatakotako/rikako --ref main
デプロイフロー¶
npm run buildでビルド- S3にsync(静的アセット: 1年キャッシュ、HTML: キャッシュなし)
- CloudFrontキャッシュを無効化
2. ロールバック手順¶
Lambda API(公開・管理共通)¶
ECRのイメージタグ dev は上書き可能。直前のイメージに戻すには:
# 直近のイメージダイジェストを確認
aws ecr describe-images \
--registry-id 579039992557 \
--repository-name rikako-api \
--query 'imageDetails | sort_by(@, &imagePushedAt) | [-5:].[imagePushedAt,imageDigest]' \
--output table
# 特定のダイジェストに戻す(公開API)
aws lambda update-function-code \
--function-name rikako-api-development \
--image-uri 579039992557.dkr.ecr.ap-northeast-1.amazonaws.com/rikako-api@sha256:<digest>
# 管理APIも同様
aws lambda update-function-code \
--function-name rikako-admin-api-development \
--image-uri 579039992557.dkr.ecr.ap-northeast-1.amazonaws.com/rikako-admin-api@sha256:<digest>
注意: ECRのライフサイクルポリシーにより最新5イメージのみ保持。古いイメージは自動削除される。
管理画面フロントエンド¶
gitで前のコミットに戻してデプロイワークフローを再実行する。
# 直前のコミットでデプロイ
gh workflow run "Deploy Admin Frontend Dev" --repo takoikatakotako/rikako --ref <commit-sha>
2.5 シークレット管理(SSM Parameter Store)¶
Lambda が読むシークレット(OPENAI_API_KEY / SLACK_WEBHOOK_URL / DATABASE_URL 等)は、Lambda 環境変数には ssm:/path 形式の参照のみを保存し、起動時に app/internal/secrets.Resolve または slack_notifier の _resolve_ssm が実値を取得する仕組み(Issue #199)。
管理対象パラメータ¶
| パス | 内容 | 登録方法 |
|---|---|---|
/rikako/<env>/openai-api-key |
OpenAI API キー | 手動 put-parameter |
/rikako/<env>/slack-contact-webhook-url |
お問い合わせ通知用 Slack Webhook | 手動 put-parameter |
/rikako/<env>/slack-alert-webhook-url |
CloudWatch アラート用 Slack Webhook | 手動 put-parameter |
/rikako/<env>/database-url |
Neon 接続文字列 | Terraform が neon_project.default.connection_uri を初期値として SecureString 登録。lifecycle.ignore_changes = [value] 指定のため以後の値はローテで上書き可(ローテ手順参照) |
/rikako/neon-api-key |
Neon API キー(Terraform Provider 用) | 手動 put-parameter |
初回登録¶
# 例: dev に OpenAI API key を登録
AWS_PROFILE=rikako-development-sso aws ssm put-parameter \
--name /rikako/development/openai-api-key \
--value 'sk-...' \
--type SecureString \
--region ap-northeast-1
ローテーション¶
SSM 値を put-parameter --overwrite で更新すれば、次回 Lambda cold start で新しい値が反映される(Lambda 関数の再デプロイ不要)。即時反映が必要な場合は Lambda コンソールから「最新バージョンを発行」または update-function-code で warm container を破棄する。
AWS_PROFILE=rikako-development-sso aws ssm put-parameter \
--name /rikako/development/openai-api-key \
--value 'sk-new-value...' \
--type SecureString \
--overwrite \
--region ap-northeast-1
確認¶
aws lambda get-function-configuration --query 'Environment.Variables' で シークレット実値が出ず ssm:/... リテラルだけが返ることを確認する。実値が露出していたら漏洩リスクがあるので即対応。
Neon DB パスワードのローテーション {#neon-db}¶
/rikako/<env>/database-url の Neon パスワードをローテする手順。
重要な前提 - Neon の role は DB を所有しているため、
terraform apply -replace=neon_role.defaultでの drop/recreate は HTTP 422ROLE_OWNS_OBJECTSで失敗する。ローテは必ず Neon API のreset_password(role を残しパスワードのみ再生成)で行う。 - SSMdatabase-urlはlifecycle.ignore_changes = [value]指定のため、put-parameter --overwriteした値を terraform が巻き戻さない。 - 環境ごとにアプリが使う role/DB が異なるので注意: - dev: rolerikako_owner/ DBrikako- prod: roleneondb_owner/ DBneondb(Neon デフォルト) - リセット直後から旧パスワードは無効になり、warm Lambda は cold start まで DB 接続に失敗する。
# 例: dev(prod の場合は PROFILE/PROJECT/BRANCH/ROLE/DB/SSM 名を読み替え)
export AWS_PROFILE=rikako-development-sso
API_KEY=$(aws ssm get-parameter --name /rikako/neon-api-key --with-decryption --query Parameter.Value --output text)
PROJECT_ID=muddy-tree-64549662
BRANCH_ID=br-calm-rice-a1123e1l
ROLE=rikako_owner
DB=rikako
HOST=$(cd terraform/environments/dev && terraform state show neon_project.default | grep 'database_host ' | sed -E 's/.*= "(.*)"/\1/')
# 1. パスワードを reset(新パスワードを取得、出力しない)
NEWPASS=$(curl -s -X POST \
"https://console.neon.tech/api/v2/projects/${PROJECT_ID}/branches/${BRANCH_ID}/roles/${ROLE}/reset_password" \
-H "Authorization: Bearer ${API_KEY}" -H "Accept: application/json" | jq -r '.role.password')
# 2. SSM を上書き
aws ssm put-parameter --name /rikako/development/database-url --type SecureString --overwrite \
--value "postgres://${ROLE}:${NEWPASS}@${HOST}/${DB}?sslmode=require"
# 3. Lambda を cold start(warm container に旧パスが残るため)
TS=$(date +%s)
for fn in rikako-api-development rikako-admin-api-development; do
aws lambda update-function-configuration --function-name "$fn" --description "db rotation $TS" >/dev/null
aws lambda wait function-updated --function-name "$fn"
done
# 4. 確認(DB を叩くエンドポイントが 200 を返すこと)
curl -s -o /dev/null -w "%{http_code}\n" https://api.dev.rikako.org/workbooks
prod は次のように読み替える: AWS_PROFILE=rikako-production-sso、PROJECT_ID は cd terraform/environments/prod && terraform state show neon_project.default の id、BRANCH_ID は同 default_branch_id、ROLE=neondb_owner、DB=neondb、SSM 名 /rikako/production/database-url、関数は rikako-api-production / rikako-admin-api-production。
管理画面 Basic 認証(CloudFront Function){#admin-basic-auth}¶
管理画面(admin.<env>.rikako.org / フロント・/api 共通)の Basic 認証は SSM の /rikako/admin-basic-auth-user / /rikako/admin-basic-auth-password を使う。
重要: Lambda シークレットと反映タイミングが違う Lambda のシークレット(2.5)は起動時に SSM を読むためローテだけで反映される。Basic 認証は違う。
terraform/environments/<env>/admin_frontend.tfがterraform apply時に SSM 値をbase64(user:password)して CloudFront Function(rikako-admin-spa-rewrite-<env>/rikako-admin-api-auth-rewrite-<env>)のコードに焼き込む。ランタイムでは SSM を見ない。 - したがって SSM をput-parameter --overwriteしただけでは 401 のまま。ローテ時は必ず対象 env でterraform apply(=関数の再デプロイ)まで行う。 - prod は自動 apply が無い(8. Terraform 操作)。SSM 更新後に prod apply を忘れると「SSM の値で 401」になる。逆に、後から誰かが prod apply すると現在の SSM 値に同期されて解消する。
疎通確認(シークレットを出力しない)¶
curl -u の資格情報は SSM から変数に入れて渡し、標準出力にはステータスコードだけ出す。
export AWS_PROFILE=rikako-production-sso # dev は rikako-development-sso
BASE=https://admin.rikako.org # dev は https://admin.dev.rikako.org
U=$(aws ssm get-parameter --name /rikako/admin-basic-auth-user --with-decryption --query Parameter.Value --output text)
P=$(aws ssm get-parameter --name /rikako/admin-basic-auth-password --with-decryption --query Parameter.Value --output text)
# 認証なし → 401(関数が機能している証拠)/ 認証あり → 200 を期待
echo "no-auth /api/health -> $(curl -s -o /dev/null -w '%{http_code}' $BASE/api/health)"
echo "auth /api/health -> $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" $BASE/api/health)"
echo "auth /api/users -> $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" $BASE/api/users)"
echo "auth / -> $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" $BASE/)"
ドリフト確認(SSM値 と デプロイ済み関数 の照合)¶
401 が出るときに「SSM 更新後の未 apply」かを切り分ける。値そのものは出さず、SHA-256 の先頭だけ比較する。
export AWS_PROFILE=rikako-production-sso
U=$(aws ssm get-parameter --name /rikako/admin-basic-auth-user --with-decryption --query Parameter.Value --output text)
P=$(aws ssm get-parameter --name /rikako/admin-basic-auth-password --with-decryption --query Parameter.Value --output text)
EXPECTED=$(printf '%s:%s' "$U" "$P" | base64)
echo "expected sha256: $(printf '%s' "$EXPECTED" | shasum -a 256 | cut -c1-12)"
for FN in rikako-admin-spa-rewrite-production rikako-admin-api-auth-rewrite-production; do
CODE=$(aws cloudfront get-function --name "$FN" --stage LIVE /dev/stdout 2>/dev/null)
DEP=$(printf '%s' "$CODE" | grep -oE "var CREDENTIALS = '[^']*'" | sed "s/var CREDENTIALS = '//; s/'\$//")
[ "$DEP" = "$EXPECTED" ] && R="MATCH" || R="MISMATCH → 対象 env で terraform apply が必要"
echo "$FN: deployed sha256 $(printf '%s' "$DEP" | shasum -a 256 | cut -c1-12) → $R"
done
MISMATCH なら cd terraform/environments/prod && terraform plan(差分が上記 2 関数の CREDENTIALS だけであることを確認)→ terraform apply。apply 中に資格情報が切り替わるため、旧資格情報で開いていた管理画面は再ログインが必要。
3. マイグレーション実行手順¶
GitHub Actions のワークフローで実行する。
# GitHub上で手動実行(推奨)
gh workflow run "Run Database Migration" \
--repo takoikatakotako/rikako \
-f environment=dev \
-f direction=up \
-f steps=all
パラメータ¶
| パラメータ | 説明 |
|---|---|
| environment | dev または prod |
| direction | up(適用)または down(ロールバック) |
| steps | all(全て)または数値(ステップ数) |
マイグレーションロールバック¶
# 1ステップ戻す
gh workflow run "Run Database Migration" \
--repo takoikatakotako/rikako \
-f environment=dev \
-f direction=down \
-f steps=1
ローカルでのマイグレーション¶
docker compose up -d postgres
docker run --rm -v $(pwd)/migrations:/migrations \
migrate/migrate -path=/migrations \
-database "postgres://rikako:password@host.docker.internal:5432/rikako?sslmode=disable" up
4. データインポート・同期手順¶
datasync(推奨)¶
YAMLデータを正としてDBと差分同期する。
cd app
# ローカルDB
go run ./cmd/datasync -data ../data plan # 差分確認
go run ./cmd/datasync -data ../data apply # 反映
# dev環境
go run ./cmd/datasync -data ../data -env dev plan
go run ./cmd/datasync -data ../data -env dev apply
詳細は データ同期 (datasync) を参照。
画像のS3アップロード¶
# アップロード
aws s3 sync data/images/ s3://rikako-images-development/ --exclude ".DS_Store"
# 確認
aws s3 ls s3://rikako-images-development/ | wc -l
importer(全件再投入)¶
全データを削除して再投入する場合に使用。
cd app && go run ./cmd/importer -data ../data
5. 障害対応フロー¶
Lambda障害¶
症状: APIがエラーを返す、タイムアウトする
# 1. ヘルスチェック(prod は api.rikako.org に置き換え)
curl -s https://api.dev.rikako.org/health
# 2. CloudWatchダッシュボードを確認
# https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#dashboards/dashboard/rikako-dev
# 3. Lambda最新ログを確認
aws logs tail /aws/lambda/rikako-api-development --since 30m
# 4. エラーのみ抽出
aws logs filter-log-events \
--log-group-name /aws/lambda/rikako-api-development \
--start-time $(date -v-1H +%s000) \
--filter-pattern "ERROR"
# 5. 必要に応じてロールバック(→ 2. ロールバック手順)
DB障害(Neon)¶
症状: API が 500 エラー、connection refused
# 1. Neonダッシュボードを確認
# https://console.neon.tech/
# 2. 接続テスト
go run ./cmd/datasync -data ../data -env dev plan
# 3. Neonのステータスページを確認
# https://neonstatus.com/
Neonが応答しない場合: - Neonのステータスページでリージョン障害を確認 - 障害が長引く場合はNeonサポートに連絡
画像配信障害¶
症状: 画像が表示されない、403/404エラー
# 1. CloudFront経由でアクセス確認(prod は image.rikako.org に置き換え)
curl -I https://image.dev.rikako.org/1.png
# 2. S3直接確認
aws s3 ls s3://rikako-images-development/1.png
# 3. CloudFrontキャッシュが古い場合は無効化
aws cloudfront create-invalidation \
--distribution-id E1LVBAGQ7YS8CR \
--paths "/*"
6. ログ調査手順¶
CloudWatch Logs¶
# 直近30分のログ(公開API)
aws logs tail /aws/lambda/rikako-api-development --since 30m --follow
# 直近30分のログ(管理API)
aws logs tail /aws/lambda/rikako-admin-api-development --since 30m --follow
# 特定パターンで検索
aws logs filter-log-events \
--log-group-name /aws/lambda/rikako-api-development \
--start-time $(date -v-1H +%s000) \
--filter-pattern "ERROR"
# 特定リクエストの調査(パスで絞り込み)
aws logs filter-log-events \
--log-group-name /aws/lambda/rikako-api-development \
--start-time $(date -v-1H +%s000) \
--filter-pattern "/questions"
公開API アクセスログ(API Gateway)¶
公開API(api.<env>.rikako.org = API Gateway HTTP API)のアクセスログは以下の Log Group に JSON で出力される(保持: dev 7日 / prod 30日)。
| env | Log Group |
|---|---|
| dev | /aws/vendedlogs/apigateway/rikako-api-development |
| prod | /aws/vendedlogs/apigateway/rikako-api-production |
記録項目は requestId / sourceIp / httpMethod / routeKey / path / status / responseLatency / integrationStatus / integrationErrorMessage / userAgent。Authorization・X-Device-ID・リクエスト本文は記録しない(機微情報を保存しないため。source IP は bot 識別のため既定で記録、不要なら access_log_include_source_ip = false で無効化可)。
4xx/5xx をステータス・パス別に集計(Logs Insights)¶
# prod は rikako-api-production / rikako-production-sso に読み替え
export AWS_PROFILE=rikako-development-sso
LOG_GROUP=/aws/vendedlogs/apigateway/rikako-api-development
QID=$(aws logs start-query \
--log-group-name "$LOG_GROUP" \
--start-time $(date -v-1d +%s) --end-time $(date +%s) \
--query-string 'filter status >= 400 | stats count(*) as cnt by status, path, userAgent | sort cnt desc | limit 50' \
--query queryId --output text)
sleep 5
aws logs get-query-results --query-id "$QID" --output table
主なクエリ例(--query-string に指定):
- ステータス別の全体内訳:
stats count(*) as cnt by status | sort cnt desc - 4xx をパス別に:
filter status >= 400 and status < 500 | stats count(*) as cnt by path, status | sort cnt desc - bot 由来の切り分け:
filter status >= 400 | stats count(*) as cnt by userAgent, path | sort cnt desc - 特定パスのレイテンシ:
filter path like /workbooks/ | stats avg(responseLatency), max(responseLatency) by path
Logs Insights はブラウザからも実行できる(CloudWatch → Logs Insights → 上記 Log Group を選択)。
CloudWatch Dashboard¶
ブラウザで確認:
https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#dashboards/dashboard/rikako-dev
確認できるメトリクス: - Invocations(呼び出し回数) - Duration(平均・p99レイテンシ) - Errors(エラー数) - Throttles(スロットリング数) - ConcurrentExecutions(同時実行数)
注意: ログ保持期間は7日間。それ以前のログは自動削除される。
7. スケーリング手順¶
Neon CU(コンピューティングユニット)変更¶
現在: dev は 0.25〜2 CU、prod は 0.25〜4 CU(auto-scaling、auto-suspend 無効)。
cd terraform/environments/dev # prod は environments/prod
# terraform で変更(neon.tf の default_endpoint_settings を編集)
terraform plan
terraform apply
neon.tf の該当箇所(neon_project の default_endpoint_settings ブロック):
resource "neon_project" "default" {
# ...
default_endpoint_settings {
autoscaling_limit_min_cu = 0.25 # 最小CU
autoscaling_limit_max_cu = 2 # 最大CU(prod は 4)
suspend_timeout_seconds = 0 # 常時稼働(auto-suspend 無効)
}
}
Lambda設定変更¶
メモリ・タイムアウトの変更は terraform/environments/dev/main.tf を編集:
module "lambda" {
memory_size = 512 # MB(現在値)
timeout = 30 # 秒(現在値)
}
cd terraform/environments/dev
terraform plan
terraform apply
Lambda同時実行数の制限¶
現在は制限なし(AWSアカウントデフォルト: 1000)。制限が必要な場合:
# 一時的に制限(Terraform外)
aws lambda put-function-concurrency \
--function-name rikako-api-development \
--reserved-concurrent-executions 100
# 制限解除
aws lambda delete-function-concurrency \
--function-name rikako-api-development
8. Terraform操作¶
# Plan/Apply(dev)
cd terraform/environments/dev
terraform plan
terraform apply
# Plan/Apply(shared)
cd terraform/environments/shared
terraform plan
terraform apply
注意: PRで
terraform/配下を変更するとGitHub Actionsで自動的にplanが実行され、結果がPRにコメントされる。
9. よくある操作¶
新しい問題を追加する¶
data/questions/{id}.ymlを作成- 必要なら
data/images/{id}.pngを追加 data/workbooks/*.ymlのquestionsに追加datasync planで確認 →datasync applyで反映- 画像があれば
aws s3 syncでS3にアップロード
アプリ別に強制アップデートを設定する(minimumVersion)¶
GET /status の minimumVersion / latestVersion は app_slug 別に上書きできる。iOS は全リクエストで X-App-Slug を送り、公開API Lambda は MINIMUM_VERSION_<SLUG> env があればそれを返す(無ければグローバル既定 MINIMUM_VERSION)。これにより アプリ単位で独立に強制アップデートを制御できる(例: 化学版だけ強制更新し、IT版は影響を受けない)。
- env 名は slug のハイフンを大文字+
_に正規化:high-school-chemistry→MINIMUM_VERSION_HIGH_SCHOOL_CHEMISTRY、it-passport→MINIMUM_VERSION_IT_PASSPORT - 設定は Terraform の Lambda 環境変数(
terraform/environments/<env>/main.tfの公開APIenvironment_variables)。グローバルのMINIMUM_VERSIONを上げると全アプリに効くため、片方だけ上げたいときは必ず slug 別 env を使う。
# 例: 化学版だけ 3.0.2 以上を必須にする(IT版は据え置き)
environment_variables = {
MINIMUM_VERSION = "1.0.0" # 既定(未指定アプリ用)
MINIMUM_VERSION_HIGH_SCHOOL_CHEMISTRY = "3.0.2"
# MINIMUM_VERSION_IT_PASSPORT は未設定 → 既定 1.0.0
}
Cognito ユーザー作成¶
aws cognito-idp admin-create-user \
--user-pool-id ap-northeast-1_DvsZzCoJw \
--username <email> \
--user-attributes Name=email,Value=<email>
CloudFrontキャッシュクリア¶
# 画像CDN
aws cloudfront create-invalidation \
--distribution-id E1LVBAGQ7YS8CR \
--paths "/*"
# 管理画面
aws cloudfront create-invalidation \
--distribution-id EIA13UUJ41NKO \
--paths "/*"