はじめに
ExcelのWBSを進捗管理ツールに置き換えた話の3本目です。
- ExcelのWBSを捨てずに、AWSサーバーレスの進捗管理ツールへ置き換えた
- Excelとの往復を壊さない取込の作り方(下見・全件主義・列の持ち方)
- 同じAWSアカウントに似た名前のスタックが同居しているときのデプロイの守り方(この記事)
- テストが通っても安心できないので、実装をわざと壊して確かめる仕組みを作った
- 作った進捗管理ツールに入力が集まらなかったので、入れてもらうための機能を足した
この記事は、デプロイ先を取り違えないための仕組みの話です。最後に、仕組みをすり抜けて本番の機能が巻き戻った件も書きます。
状況:名前がハッシュまで同じ
このツールの AWS アカウントには、個人で作っている家計簿アプリと飲食店探しアプリも同居しています。どれも CDK で作っていて、構成が似ています。
困ったのは、家計簿アプリと名前がそっくりになることです。
kakeistack-frontendbucketefe2e19c-xxxxxxxxxxxx
wbsstack-frontendbucketefe2e19c-xxxxxxxxxxxx ← efe2e19c まで同じ
KakeiStack-ApiFunctionCE271BD4-xxxxxxxxxxxx
WbsStack-ApiFunctionCE271BD4-xxxxxxxxxxxx ← CE271BD4 まで同じ
CDK は、論理 ID にコード上のパスから作ったハッシュを付けます。同じ名前の構成要素を同じ階層に置くと、別のアプリでもハッシュが一致します。
以前の手順書には、次のようなコマンドが書いてありました。
aws s3 sync dist/ s3://$(aws s3 ls | grep -i frontendbucket | awk '{print $3}')/ --delete
grep frontendbucket は両方のバケットに当たります。もし家計簿のバケットが先に出てきたら、--delete 付きで家計簿の画面を消していました。
名前で探さず、CloudFormation に聞く
grep の語を長くしても、ハッシュまで同じなので防ぎきれません。そこで、名前で探すのをやめました。
「WbsStack に属しているリソースはどれか」を CloudFormation に聞けば、同居している別のスタックのものは出てきません。
BUCKET=$(aws cloudformation describe-stack-resources --stack-name "$STACK" --region "$REGION" \
--query "StackResources[?ResourceType=='AWS::S3::Bucket' && starts_with(LogicalResourceId,'Frontend')].PhysicalResourceId" \
--output text)
BUCKET=$(only_one "フロントのバケット" "$BUCKET")
only_one は、ちょうど1件でなければ止める関数です。0件でも2件でも、そこで止めます。
出す前に確かめること
この確認を guard.sh という1本のスクリプトにまとめ、デプロイの前に必ず通すようにしました。
| 確かめること | 外れたとき |
|---|---|
| アカウント ID | 止める。別アカウントのアクセスキーだと、そこに WbsStack が新しく建ってしまう |
| リージョン | 環境の既定に頼らず、東京を明示する |
| スタックの状態 | 前のデプロイが終わっていなければ止める |
| 画面のバケット | スタックから引き、1件だけで、名前の頭が想定どおりであること |
| 取込用のバケットと同じでないか | 同じなら止める。--delete で取込ファイルを消してしまう |
| CloudFront の向き先 | いま引いたバケットを向いていること |
| Lambda | 全部 WbsStack- で始まること |
アカウント ID を最初に見るのは、取り違えたときにいちばん高くつくからです。別のアカウントのアクセスキーで cdk deploy すると、エラーにはならず、そのアカウントに同じスタックがもう1つ建ちます。
デプロイの流れは次のとおりです。どこかで外れたら、そこで止まります。
| 順番 | 中身 | AWS に触るか |
|---|---|---|
| 1. 検査 | テスト、ビルド、cdk synth など |
触らない |
| 2. 行き先の確認 | 上の表 | 読むだけ |
| 3. デプロイ |
cdk deploy WbsStack --exclusively、S3 への同期、キャッシュの破棄 |
書く |
| 4. 反映の確認 | 画面が200、API が401(未ログイン)を返すことなど | 読むだけ |
--exclusively は、指定したスタックだけをデプロイするオプションです。
確認が本当に止めるかを確かめる
確認のスクリプトを書いても、それが本当に止めるかは別の話です。条件を書き間違えていると、何でも通してしまいます。
そこで、期待値を1つずつ壊した写しを作り、10項目すべてで実際に止まることを確かめるスクリプトも置きました。
| 壊し方の例 | 期待する結果 |
|---|---|
| 期待するアカウント ID を別の値にする | 止まる |
| バケット名の頭を別の値にする | 止まる |
| 画面と取込のバケットを同じにする | 止まる |
このスクリプトは、デプロイ前の検査から毎回呼んでいます。
確かめた結果を引き継ぐ
1回のデプロイで、検査・行き先の確認・反映の確認と、同じ問い合わせを何度もしていました。GitHub Actions で測ると、4分55秒のうち40秒がこれでした。
そこで、一度確かめた結果を環境変数で次に渡し、2回目以降は近道を通るようにしました(8秒 → 1秒)。
ただし、アカウント ID だけは毎回見直します。途中でアクセスキーが入れ替わる可能性があるのと、取り違えの中でいちばん高くつくのがそこだからです。引き継いだ値が別のスタックを指していたら、近道に入らずに引き直します。
この近道には落とし穴がありました。「確認が本当に止めるか」を確かめるスクリプトの中でも、引き継いだ値が残っていると、壊した写しが近道を通って素通りします。そのスクリプトでは、引き継ぎの変数を必ず消してから動かしています。
スマホからデプロイする
外出先から直したいことがあるので、GitHub Actions からもデプロイできるようにしました。GitHub のアプリから Actions を開き、対象(両方・バックエンド・画面)を選んで実行します。master への push でも自動で走ります。
決めたことは3つです。
| 決めたこと | 理由 |
|---|---|
| ワークフローは手元と同じスクリプトを呼ぶだけ | 手元と Actions で手順を二重に持たない。片方だけ直して食い違うのを防ぐ |
| デプロイ専用の IAM ユーザーを作る | 権限を絞れる。権限だけ差し替えるオプションも用意し、アクセスキーを作り直さずに済むようにした |
| リポジトリは非公開にする | 設定ファイルに Teams の Webhook URL が入っている |
それでも本番が巻き戻った
ここまで守っていても、本番の機能が消えたことがあります。
| 日付 | 起きたこと |
|---|---|
| 9月3日 | 機能追加のブランチを、手元から本番に出した。プルリクエストはマージしていなかった |
| 9月10日 | 別の作業で master に push した。Actions が master の中身で本番に出し直した |
| 10月3日 | 本番の画面に、ブランチに積んでいた機能が無いことに気づいた |
ブランチに積んでいたのは、工数・先行タスク・コメント・当初計画との比較・招待のやり直しなどです。9月10日のデプロイで、これらが本番から消えていました。
行き先の確認は全部通っています。出した先は正しい WbsStack で、出したものが古かっただけです。検査も全部通ります。気づく手がかりがありませんでした。
気づいたのは、別の作業のついでにブランチの状態を見たときです。ブランチが master より11コミット進んだままで、プルリクエストは master と衝突していました。配信中の画面のコードを取ってきて、機能の文言を検索すると、確かに消えていました。
U=https://<配信のドメイン>
for f in $(curl -s $U/ | grep -o 'assets/[^"]*\.js'); do curl -s "$U/$f"; done > all.js
for w in 予定工数 先行タスク いまの予定を控える; do
grep -q "$w" all.js && echo "あり $w" || echo "なし $w"
done
直し方は、ブランチに master を取り込んで衝突を解き、プルリクエストをマージすることでした。マージで Actions が走り、両方の機能がそろった状態で出し直されました。
取り込むときに、別々に足した2つの機能がぶつかるところもありました。どちらも「案件の設定」を DynamoDB から読んでいて、合わせると同じ案件を1回の起動で2回読むようになっていました。テストが落ちたことで気づき、1回の読み取りを使い回す形に直しています。
いまは、引き継ぎの資料に次の決まりを足しました。
- ブランチから本番に出したら、その日のうちにプルリクエストをマージする
- マージできないなら、先に
masterをブランチに取り込んでから出す
push で自動デプロイする構成では、本番に出ていても master に入っていないものは、次の push で消えます。
おわりに
| 仕組み | 防いでいること |
|---|---|
| 名前で探さず CloudFormation に聞く | 似た名前の別スタックへの取り違え |
| アカウント ID を毎回確かめる | 別アカウントへのスタックの新設 |
| 確認を壊して止まるかを確かめる | 書いただけで実際には止めない確認 |
| 手元と Actions で同じスクリプトを使う | 手順の食い違い |
| 本番に出したらその日にマージする | 次の push での巻き戻り |
最後の1つは、スクリプトでは防げていません。
次の記事では、テストが本当に何かを確かめているのかを、実装をわざと壊して確かめる仕組みの話を書きます。