跳轉到

MatchRPG 連假共享 Dev VM:EC2 + SSM + ECR + Cloudflare Tunnel 零 SSH 部署方案

文檔資訊

  • 分類: architecture
  • 難度: advanced
  • 預估閱讀時間: 12 分鐘
  • 標籤: aws, ec2, ssm, ecr, terraform, cloudflare-tunnel, powershell, docker-compose, shared-dev, windows

摘要

為解決連假期間無法維持本地 docker-compose 環境供團隊測試的問題,設計並實作一套臨時共享 Dev VM 方案:以 Terraform 建立 EC2(t3.large),使用 AWS SSM Session Manager 取代 SSH(無需固定 IP 或開放 port),將本地 Docker image 直接 push 到 ECR(確保 binary 完全一致),透過 S3 傳輸設定檔與 Cloudflare Tunnel credentials,在 VM 上重現完全一致的 docker-compose 環境,保留相同的 Cloudflare Tunnel domain(local-*.hanwellspring.com)。

關鍵學習

  • Windows 環境下 PowerShell 驅動 bash subprocess 有嚴重路徑問題(反斜線被吃掉),應全程用 PowerShell 避免跨 shell 的 PATH 與路徑問題

  • SSM send-command 的 --parameters 若包含複雜 bash 指令,用 file:// 前綴傳 JSON 檔案比 inline 更可靠

  • PowerShell ConvertTo-Json 對單一字串不會自動序列化成陣列,需明確用 @($Commands) 強制包裝

  • PowerShell 多重賦值 $local, $ecr = @('a','b') 會讓 $ecr 變成單元素陣列,改用 $pair[0]; $pair[1]

  • pg_dump 二進位格式透過 PowerShell > redirect 會被轉成 UTF-16,必須用 cmd /c 'docker exec ... > file' 保持二進位安全

  • ECR token 12 小時過期,docker-compose 中不應設 pull_policy: always,否則 VM 重啟後無法拉 image

  • docker-compose 若引用 Dockerfile 路徑,VM 上必須有對應的 Dockerfile,需一起打包到 tar bundle

技術細節

架構設計

核心原則:零程式碼差異 - 不重新編譯、不 clone repo,直接 docker tag 本地 image 後 push 到 ECR,確保 binary 完全一致 - 透過 docker-compose.override.yml 將服務的 build 替換為 ECR image 參照 - 保留相同 Cloudflare Tunnel ID 與 credentials,domain 不變

Terraform 資源(shared-dev-vm)

# 關鍵設計:outbound-only Security Group,靠 SSM 管理
resource "aws_security_group" "shared_dev" {
  description = "Shared dev VM - outbound only (SSM, Docker pull, Cloudflare Tunnel)"
  # 無 ingress rule
}

resource "aws_instance" "shared_dev" {
  instance_type        = "t3.large"
  iam_instance_profile = aws_iam_instance_profile.shared_dev.name
  # IAM 含:SSM managed policy + ECR pull + S3 read
  root_block_device {
    volume_size = 30
    volume_type = "gp3"
    encrypted   = true
  }
}

PowerShell 腳本設計

deploy.ps1 核心流程: 1. 讀取 terraform output 取得 instance ID 2. 等待 SSM agent 上線(輪詢 aws ssm describe-instance-information) 3. 等待 user-data.sh 完成(檢查 .vm-ready sentinel file) 4. ECR login(aws ecr get-login-password | docker login) 5. docker tag matchrpg-twirpserver <ecr>/matchrpg-game-server:shared-dev 然後 push 6. 打包 configs/gamedata/Dockerfiles 成 tar.gz 7. 產生 docker-compose.override.yml 指向 ECR image 8. 產生 setup.sh 並上傳到 S3 9. 透過 SSM 執行 setup.sh(使用 file:// 參數避免 quoting 問題) 10. 清理 S3

SSM 呼叫模式(避免 quoting 地獄):

function Invoke-Remote {
    param([string[]]$Commands)
    $tmpJson = [System.IO.Path]::GetTempFileName() + '.json'
    @{ commands = @($Commands) } | ConvertTo-Json | Set-Content $tmpJson
    aws ssm send-command `
        --instance-ids $InstanceId `
        --document-name AWS-RunShellScript `
        --parameters "file://$tmpJson" `
        --region $Region | Out-Null
    # 輪詢等待完成...
}

pg_dump 二進位安全:

# 錯誤:PowerShell > 會轉成 UTF-16
docker exec postgres pg_dump -Fc matchrpg > matchrpg.dump

# 正確:cmd /c 保持二進位
cmd /c "docker exec postgres pg_dump -Fc matchrpg > $dumpPath"

What Changed

從「開發者本地 docker-compose + Cloudflare Tunnel」演進為「AWS EC2 共享 Dev VM」方案。

MatchRPG_Infra/terraform/environments/shared-dev-vm/ 新增完整 Terraform 模組,建立 EC2 instance(t3.large, 30GB gp3, Amazon Linux 2023),配備 SSM + ECR + S3 的 IAM 權限,Security Group 只開 outbound(無 inbound port,完全靠 SSM 管理)。

同目錄新增三支 PowerShell 腳本:deploy.ps1(一鍵部署)、sync-db.ps1(資料庫遷移)、teardown.ps1(銷毀環境)。原本的 bash 腳本因 Windows PowerShell 路徑與 PATH 問題全數廢棄改用 PS1。

So What

這個方案解決了一個常見的團隊開發痛點:開發者的本地環境是團隊唯一的整合測試環境,但開發者有時無法持續在線(出差、連假)。

方案的核心價值在於**零環境差異**:不重新 build image、不 clone repo、不需要固定 IP、不需要 SSH key,任何有 AWS IAM 權限的人都能透過 SSM 管理 VM。Cloudflare Tunnel 保留相同 domain,團隊成員的設定不需任何修改。

Trade-offs

  • 成本 vs 便利性:t3.large 持續運行每月約 $60-80(ap-east-1),但設計為臨時用途,連假後 teardown.ps1 銷毀
  • SSM 延遲 vs SSH 方便性:SSM send-command 有 polling 延遲(數秒到數十秒),但換來零 inbound port、無需管理 SSH key
  • ECR tag 汙染 vs 方便:共用同一個 ECR repo,用 shared-dev tag 與 dev/prod 區隔,有一定混用風險
  • Windows-only 腳本 vs 跨平台:全改 PowerShell 後,Mac/Linux 的團隊成員無法直接執行(但此場景下只有 Windows 機器需要執行)

Try It Fast

# 1. 建立 VM(只需一次)
cd terraform/environments/shared-dev-vm
terraform init
terraform apply

# 2. 部署完整環境(push images + 啟動 stack)
.\deploy.ps1

# 3. 同步本地 DB 到 VM
.\sync-db.ps1

# 4. 驗證
curl https://local-api.hanwellspring.com/health

# 5. 連假結束後銷毀
.\teardown.ps1

Recommendation

  1. 將 ECR 的 shared-dev image 使用獨立的 ECR repository 命名(如 matchrpg-game-server-shared-dev),避免與 dev/prod tag 混用造成誤拉
  2. sync-db.ps1 的 SSM remote command 加入更詳細的 stderr 輸出,方便偵錯 pg_restore 失敗原因
  3. 考慮在 user-data.sh 中加入 ECR login cron job(每 10 小時執行一次),避免 12 小時 token 過期問題
  4. teardown.ps1 執行前自動提示確認 ECR shared-dev tag 的清理,避免遺留過時 image

本文檔由 Semi-Brain 自動生成

Session ID: 9632bcf1-7666-464a-9364-4b237ee0291b

分析信心度: 88%