付録B セルフチェックの解答と解説
第1章
1-1. NEXT_PUBLIC_が付いていない環境変数はサーバー専用として隔離され、サーバーランタイム(Route Handler、Server Actionなど)でのみ読み取ることができ、ブラウザへは渡されません。一方、NEXT_PUBLIC_プレフィックスが付いた変数は、ビルド時点に実際の文字列値がクライアントJavaScriptバンドルファイルの中に平文でインライン化され、世界中の誰もがブラウザの開発者ツールから見ることができます。ビルド時点に静的ファイルとして焼き付けられるため、ダッシュボードで値を変更したあとは必ず再デプロイ(Redeploy、新規ビルド)を実行しなければブラウザへ反映されません。
1-2. クライアントと関数の間の往復はリクエスト時の1回だけですが、サーバーレス関数が1つのリクエストを処理するとき、データベースとは接続確立(TCP+TLS+認証ハンドシェイク)および複数回の順次クエリ(認証確認、データ照会、記録保存など)をやり取りしながら多重往復(Multiple RTT)を行います。関数(iad1、米国東部)とDB(ソウル)の間の1回の往復遅延が約180msであるとすると、数回のクエリと接続確立だけでも1秒以上の累積ネットワーク遅延が発生するため、関数のリージョンをデータベースと同じソウル(icn1)に合わせなければなりません。
1-3. トランザクションモード(ポート6543)は、個別のトランザクション(BEGIN〜COMMIT)や単一クエリが実行される短い瞬間だけバックエンドのPostgres接続を借りて使い、即座にプールへ返却するため、寿命の短いインスタンスが急激に増えるサーバーレスアプリのランタイムクエリに使用することで接続枯渇を防げます。一方、セッションモード(ポート5432)や直接接続は、クライアントが接続を切るまでバックエンド接続を1対1で占有し、セッションレベルの機能(Prepared Statement、SET、Advisory Lockなど)を維持するため、データベーススキーママイグレーション(migrate)作業に分離して使用します。
1-4. USING句は、テーブルにすでに存在する既存の行のうち、どの行を読み取ったり(SELECT)、更新対象にしたり(UPDATE)、消したりできるか(DELETE)を絞り込むときに評価されます。一方、WITH CHECK句は、INSERTやUPDATEの結果としてテーブルへ新しく入る行、または変更されたあとの行の状態がポリシーを満たしているかを検証するときに評価されます。もしUPDATEポリシーでUSINGだけが本人所有として設定されていてWITH CHECK条件が緩い場合(例:true)、ユーザーが自分の行を選んで更新する際に、所有者ID(owner_id)や所属組織(org_id)、承認ステータスを他人のものや権限外の値へ上書きできてしまいます。
1-5. 1つ目はプロジェクトに発行された自動化バイパスシークレットを格納するx-vercel-protection-bypass: <SECRET>ヘッダーであり、2つ目は最初のアクセス後に続くページ・リソースリクエストでもバイパス状態を維持できるようブラウザへバイパスCookieをセットするx-vercel-set-bypass-cookie: true(クロスサイトテスト時はsamesitenone)ヘッダーです。
1-6. (1)バケット設定:契約書は機密性の高い個人文書であるため、URL流出時の無断閲覧を防ぐべく必ず非公開バケット(Private Bucket、public = false)として作成します。(2)アップロードフロー:Vercelサーバーレス関数のリクエストボディサイズ制限を回避するため、サーバーAPIでまずユーザーのログインおよび権限を検証したあと、短い有効期限を持つ署名付きアップロードURL(createSignedUploadUrl)を発行し、ブラウザがこのURLを用いてSupabase Storageへファイルを直接アップロードするようにします。(3)閲覧フロー:閲覧リクエスト時にサーバー(またはRLS)が契約書の所有者であるか確認したあと、短い有効期間(例:60秒)が設定された署名付きダウンロードURL(createSignedUrl)を発行し、本人のブラウザでのみ一時的に閲覧できるようにします。
第2章
2-1. ステートロックなしで2人が同時にterraform applyを実行すると、2つのプロセスが同時にステートファイルを読み取って各自の結果を上書きする競合状態(Race Condition)が発生し、片方が作成したリソースがステートファイルから消失したりJSON構造が破損したりして、重複作成・衝突エラーが起きます。AWS S3バックエンドでは、同時実行を防ぐためにステートロック(use_lockfile = trueまたはdynamodb_table)を有効にしなければならず、また誤ってステートファイルが上書きされたり破損したりしたときに以前の状態へ戻せるよう、S3バケットのバージョニング(Bucket Versioning、およびサーバーサイド暗号化encrypt = true)を有効にしなければなりません。
2-2. ~(Update in-place)は既存のクラウドリソースを削除せずに維持したまま設定属性のみをAPIでインプレース更新するという意味であり、-/+(Replace)は不変属性の変更などにより既存のリソースを完全に削除(-)したあとに新しいリソースを作成(+)するという意味です。本番データベースで-/+が実行されるとDBインスタンスと内部データが削除されてしまうため、リソースブロック内にlifecycle { prevent_destroy = true }を設定し、削除や置換が計画されたらplanの段階で即座にエラーとして遮断されるようにしなければなりません。
2-3. コード上でリソース識別子だけをaws_instance.appからaws_instance.api_serverに変えてapplyすると、Terraformは名前が変わったことを認識できず、既存のaws_instance.appインスタンスを完全に削除(-)してaws_instance.api_serverを新規作成(+)してしまうため、サービス停止とデータの初期化が発生します。停止なしにステートファイルのアドレスマッピングだけを移すには、コードにmoved { from = aws_instance.app, to = aws_instance.api_server }ブロックを追加しなければなりません。
2-4. sensitive = trueはterraform planとapplyを実行するときにターミナル画面(標準出力ログ)で値が見えないよう(sensitive value)として隠してくれるだけであり、Terraformが変更有無を比較するために保存するterraform.tfstate JSONファイルの中には依然としてシークレット値が平文(Plaintext)で記録されるからです。これを防ぐには、(1).gitignoreによって*.tfstateのGitコミットを根本から遮断し、(2)KMS暗号化と最小IAM権限が適用されたリモートバックエンド(S3など)にのみステートを保管し、(3)ステートファイルに値が残らないEphemeral変数・書き込み専用属性(password_wo)や外部Secrets Managerの参照を使用しなければなりません。
2-5. コンソールから手動で追加したルールはドリフト(Drift)状態にあるため、そのまま放置して次回のデプロイ(apply)を回すと、Terraformがコードにないルールだと判断して削除してしまいます。したがって、(1)まず.tfコードのセキュリティグループ定義に未明に手動追加したポートルールをまったく同じように記述して反映し(新規作成された別個のリソースであればimportブロックを使用)、(2)terraform planを実行してクラウドの実際の状態とコードの間にこれ以上変更事項がないこと(No changes. Your infrastructure matches the configuration.)を確認したうえで、Gitにコミット・マージします。
2-6. PRが開かれた時点ではまだコードレビューと承認を経ていない状態であるため、もしPRワークフローに本番書き込み(apply)権限を持つロールを与えてしまうと、悪意やミスを含むPRコードがレビュー前に実際のインフラを変更したりシークレットを奪取したりできてしまいます。したがって、PRワークフローには読み取り専用(plan)ロールのみを付与し、実際の変更権限を持つapplyロールはコードレビュー後にmainブランチへマージされたときにのみ作動するよう分離しなければなりません。applyロールのOIDC信頼ポリシーのConditionでは、対象サービスであるaud(sts.amazonaws.com)とともに、正確なリポジトリおよびブランチ/環境を指すsub(例:repo:組織/リポジトリ:ref:refs/heads/mainまたは保護されたenvironment:production)クレームを必ず検証しなければなりません。
第3章
3-1. DNS標準(RFC 1034/2181)上、あるドメイン名にCNAMEレコードが存在する場合、その名前には他のいかなる種類のレコードも一緒に存在することはできません。ところがルートドメイン(Zone Apex、example.com)には、ドメインゾーンを成立させる必須レコードであるSOAレコードとNSレコードが必ず存在しなければならないため、通常のCNAMEを入れることができません。Route 53では、ルートドメインにAliasレコードを設定し、内部的にALBの現在のIP(A/AAAA)を引き当てて標準応答として返す方式で解決します(CloudflareではCNAME Flatteningを使用します)。
3-2. 世界中の通信事業者およびパブリック再帰DNSリゾルバ群は、以前のクエリ時に受け取った旧サーバーのIPをその当時設定されていたTTL(86,400秒=24時間)の間自身のキャッシュに保管し、その時間が期限切れになるまでは権威ネームサーバーへ再び尋ねに来ないからです。移行当日にネームサーバー上でTTLを60秒に変えてIPを書き換えても、すでに24時間のキャッシュを握っているリゾルバ群は最大24時間にわたって旧サーバーのIPへトラフィックを送り続けます。したがって、最低でも既存のTTL(24時間)よりはるかに前にあらかじめTTLだけを60〜300秒へ引き下げ、既存のキャッシュがすべて期限切れになるのを待ってからIPを切り替えなければなりません。
3-3. ユーザーのブラウザでは**ERR_TOO_MANY_REDIRECTS(無限リダイレクトループ)エラーが発生し、サイトが開かなくなります。ユーザーはCloudflareへhttps://で接続しますが、FlexibleモードはCloudflareからオリジンサーバーへ向かう際に平文のhttp://(ポート80)へ変えてリクエストを送り、オリジンサーバーは平文HTTPリクエストが入ってきたため再びhttps://で接続し直せとして301/308リダイレクトを送り返す、という過程が果てしなく繰り返されるためです。また、エッジ〜オリジン区間が平文で露出するため、必ずオリジンサーバーに有効な証明書(公的CAまたはCloudflare Origin CA)をインストールし、Full (Strict)**モードを使用しなければなりません。
3-4. オレンジ色の雲(Proxied)は、DNSクエリに対してCloudflareエッジのIPを返すだけです。もしオリジンサーバーのファイアウォール(セキュリティグループ)が全世界(0.0.0.0/0)に開いている場合、攻撃者が過去のDNS履歴やスキャンで突き止めたオリジンサーバーの実際のIPアドレスへ直接Host: ドメインヘッダーを付けてHTTPリクエストを送ると、CloudflareエッジとWAFを一切経由せずに完全にバイパスできてしまいます。これを防ぐには、オリジンサーバー側で(1)インバウンドファイアウォールをCloudflareの公開IP帯域のみに制限するか、(2)**Authenticated Origin Pulls(mTLSクライアント証明書検証)によって自社のCloudflareゾーンが送ったリクエストのみを受け入れるか、(3)インバウンドポートをすべて閉じてCloudflare Tunnel(cloudflared)**でのみ接続しなければなりません。
3-5. パーソナライズAPI(/api/profile)を共有CDNにキャッシュすると、最初に呼び出したユーザーの個人情報やSet-Cookieセッションヘッダーがエッジに保存され、続いてアクセスした他のユーザーたちへそのまま露出するキャッシュ汚染(個人情報流出)事故が発生します。また、LLMストリーミング(/api/chat/stream、text/event-stream)をキャッシュしたり応答バッファリングを有効にしたりすると、トークンがリアルタイムに1文字ずつ流れず、ストリームが終わるまでエッジに溜め込まれてから一括で配信されたりタイムアウトが発生したりします。したがって、両方のパスともCDNキャッシュルールで**Bypass Cache(キャッシュバイパス)**に設定し、パーソナライズAPIにはCache-Control: private, no-storeを、SSEストリーミングにはCache-Control: no-cache, no-transform(およびバッファリング無効化)を指定しなければなりません。
3-6. 第1に、**自動リトライおよびマルチプロバイダーフォールバック(Universal Endpoint / Fallbacks)**機能を構成し、プライマリモデル(OpenAI)で503やタイムアウトが発生したら、コードの再デプロイなしにゲートウェイが即座に予備プロバイダー(例:Anthropic ClaudeまたはGoogle Gemini)へリクエストを自動迂回させ、サービス停止を防ぎます。第2に、**ゲートウェイレート制限(Rate Limiting)**を設定し、エージェントループの暴走によって時間あたり/分あたりの閾値を超える過度な呼び出しが発生したら、外部LLMに到達して料金を発生させる前にゲートウェイで遮断します。第3に、統合可観測性(Analytics/Logging)および決定論的プロンプトキャッシングを有効にし、モデル別の遅延時間・エラー率・コストをひと目で監視しつつ、繰り返される同一クエリはキャッシュから即座に応答してコストと負荷を減らします。
第4章
4-1. 従来型の境界型VPNは、認証を通過した端末を社内ネットワーク(サブネット)の中へ編入させ、基本的に同じネットワーク内の複数のサーバーやポートへアクセスできる広いネットワークレベルの信頼を付与します。一方、NIST SP 800-207に基づくZTNAは、ネットワークの位置によって信頼を与えず、セッションごとに身元と端末状態(Device Posture)を検証し、そのロールに許可された個別のアプリケーション・リソースと特定のポートにのみ最小権限のアクセスを開きます。そのため、端末1台がマルウェアに感染したとき、従来型VPNでは内部網を伝って隣接サーバーやDBへと広がる**ラテラルムーブメント(Lateral Movement、横展開)**の被害が発生します。
4-2. 通常のWebサーバーは外部のクライアントが内側へ入ってくるインバウンド接続を待たなければならないため、ファイアウォールのインバウンドポートを開ける必要があります。一方、cloudflared(Cloudflare Tunnel)は社内のプライベートサブネット内のデーモンが先に外側のCloudflareエッジサーバーへ向かってアウトバウンド(Outbound)暗号化接続を結んでおき、外部の社員がCloudflare Access認証を通過すると、すでに開いているそのアウトバウンドトンネルを通じてリクエストを内側へ受け取るため、社内ファイアウォールのインバウンドポートは1つも開ける必要がありません。
4-3. 192.168.1.0/24(および192.168.0.0/24)は、大多数の家庭用Wi-Fiルーターやカフェのルーターがデフォルト値として使用するRFC1918プライベートIP帯域です。社内VPCでも同じように192.168.1.0/24を使うと、在宅勤務者のローカルルーターネットワークと社内VPNのルーティング経路が正面衝突(Overlapping Subnet Collision)し、パケットが見当違いの場所へ飛ぶか、ローカルプリンター・ゲートウェイとの通信が切れてしまいます。解決策は、社内クラウドVPCの帯域を家庭用ルーターと重ならない帯域(例:10.240.0.0/16や172.28.0.0/16の精密なサブネット)として設計し、スプリットトンネリング(Includeモード)には実際に必要な社内CIDRやCGNAT(100.64.0.0/10)・ドメインのみを登録することです。
4-4. プライベートIP自体には外部から直接接続できなくても、外部のパブリックDNSに社内サーバー名(db.dev、payrollなど)と内部プライベートIP帯域を登録しておくと、攻撃者がDNS照会を通じて社内にどのような中核システムがあり、ネットワーク帯域がどう構成されているかという内部トポロジー情報を容易に収集できてしまいます。これを防ぐため、社内ドメインは外部のパブリックDNSから消し、社内のプライベートDNSにのみ登録したうえで、ZTNA/VPNクライアントが該当ドメインサフィックス(*.internal.corp)のクエリだけをトンネル内部のDNSリゾルバへ送るよう**スプリットホライズンDNS(Split-Horizon DNS/条件付きフォワーディング)**を構成します。
4-5. NODE_TLS_REJECT_UNAUTHORIZED=0は社内CAだけを許可するのではなく、TLS証明書検証自体を完全にオフにしてしまうため、外部ネットワークでハッカーが偽造証明書を用いて行う中間者攻撃(MITM)に対して無防備にさらされます。正しい方法は、TLS検証を維持したまま社内ルートCA証明書を信頼リストへ登録することです。Node.js(およびNodeベースのコーディングエージェント)には、既存の内蔵CAリストに社内CAを追加してくれるNODE_EXTRA_CA_CERTS="/path/to/company-root-ca.pem"を設定し、デフォルトバンドルを置換してしまうPython・OpenSSL系ツールには、既存の公的ルートCAバンドルと社内ルートCAを1つに結合した統合PEMファイルを作成してSSL_CERT_FILEおよびREQUESTS_CA_BUNDLEに指定しなければなりません。
4-6. (1)RDS接続の構成要素:AWS RDSにはVPN/ZTNAエージェントを直接インストールできないため、同じVPCのプライベートサブネットにサブネットルーター(Subnet Router)またはcloudflaredプライベートネットワークコネクタを冗長化(2台以上)して配置します。(2)ポート・対象制限の手段:外注開発者グループ(group:contractors)を送信元とし、ステージングRDSのIPと5432番ポートのみを宛先として許可する最小権限ACL(デフォルト拒否)を適用して、SSH(22)や他の社内サーバーへのラテラルムーブメントを遮断します。(3)退職時の自動回収:社内IdPとZTNA/VPNをSCIMプロトコルで連携させ、契約終了時にアカウントが即座に自動無効化され、アクティブなトンネルセッションとトークンが強制終了されるようにします。
第5章
5-1. デュアルキー(Dual-Key)無停止ローテーションの4段階は、(1)新キー(V2)を生成し、サーバーの検証コードが[V2, V1]の2つのキーを両方とも許可するよう先にデプロイ、(2)新規トークン署名およびWebhook送信設定をV2へ切り替え、(3)既存トークンの有効期間(TTL)の間待機し、ログでV1の使用量が0へ減るか確認、(4)検証リストからV1を削除して失効させる、という順序です。一方、外部APIキーが公開リポジトリへ流出した場合は、攻撃者が猶予期間中にそのキーを悪用してデータを奪取したり莫大な課金を引き起こしたりするおそれがあるため、新キーをデプロイした直後に猶予期間を置かず、露出した旧キーを永久失効(Revoke)させなければなりません。
5-2. RPO(目標復旧時点)は障害時に許容できる最大の過去データ消失時間幅であり、RTO(目標復旧時間)は障害発生からサービスの正常再開までに許容される最大の復旧所要時間です。毎日午前4時にだけスナップショットを取得する構造では、午前4時以降から午後2時の事故時点までの10時間に積み上がった変更履歴がスナップショットに存在しないため、復元時に10時間分のデータがすべて消失してしまいます。これを解決するには、ベースバックアップの上にすべての変更ログ(WAL、Write-Ahead Log)を継続アーカイブし、午後2時の事故クエリ実行直前(秒単位)へと巻き戻せる**ポイントインタイムリカバリ(PITR、Point-in-Time Recovery)**を使用しなければなりません。
5-3. ローリングデプロイ中は、まだ終了していない旧バージョンのアプリケーションインスタンスと新しく立ち上がった新バージョンのインスタンスが同時に同じデータベースへクエリを送ります。このときRENAME COLUMNやDROP COLUMNをいきなり実行すると、旧バージョンのインスタンスが既存カラムを見つけられずに即座に500エラーを出し、新バージョンで問題が起きても旧バージョンのコードへロールバックできなくなってしまいます。Expand-Contractパターンは、(1)Expand(拡張):既存カラムを残したまま新カラムをNullableで追加、(2)Dual-Write & Backfill:アプリが新旧両方のカラムへ書き込むようデプロイし過去データをバックフィル、(3)Cutover(切り替え):アプリが新カラムのみを読み書きするようデプロイ、(4)Contract(収縮):旧バージョンのインスタンスが完全に消えたあとに旧カラムを削除する、という順序で進めます。
5-4. 大多数の高速な静的・キャッシュ応答(例:10ms)が混ざっていると、少数のユーザーが経験する深刻なボトルネック(例:20秒の遅延)が平均値(Average)に薄められて正常であるかのように見えてしまうため、最も遅い下位5%・1%のリクエストの実際の体感遅延を示す**p95・p99(テールレイテンシ)**を見なければなりません。ログとトレースを収集する際は、(1)AuthorizationヘッダーとセッションCookie(Cookie/Set-Cookie)、(2)API秘密鍵とデータベース接続文字列のパスワード、(3)ユーザーのパスワード・クレジットカード番号・マイナンバー(住民登録番号)などの機密個人情報を必ずスクラビング(マスキング)しなければなりません。
5-5. 過負荷に陥った下位システムに対してすべてのサーバーインスタンスが待機なしに同時にリトライを浴びせると、元のトラフィックにリトライのトラフィックまで何倍にも重なり、下位システムが完全に復旧不能な状態へと崩壊する**リトライストーム(Retry Storm / Thundering Herd)と連鎖障害が発生します。これを防ぐには、リトライ間隔を徐々に延ばしながらランダムな時間偏差を混ぜる指数バックオフとジッター(Exponential Backoff with Jitter)を適用し、連続失敗が閾値を超えたら下位への呼び出しを即座に遮断してフォールバックを返すサーキットブレーカー(Circuit Breaker)**を設けなければなりません。
5-6. 障害が進行している状態で原因を分析し、新しいコードを書いてテスト・デプロイするのに何十分も費やせば、その間すべての決済顧客が被害を受け続けるためです(「緩和が最優先、分析は後」の原則違反)。最初の10分以内には、デバッグの代わりに即座に直前の正常なデプロイ版へロールバック(Instant Rollback)するか、問題の新規機能をフィーチャーフラグ(Kill Switch)でオフにして、エラー率をまず0%へ鎮めなければなりません。障害が終わったあとは、特定の個人を非難することなく「なぜテストやカナリアの段階でこのエラーが検出されなかったのか」というシステム的な原因と具体的な再発防止アクションアイテムを記録する**非難のない事後振り返り(Blameless Postmortem)**を作成します。