# ROLAC — Infrastructure Design **平台:** Microsoft Azure (Nonprofit $2,000 Credit) **文件版本:** v0.1 (2026-05-24) --- ## 目錄 1. [Azure 資源規劃](#1-azure-資源規劃) 2. [VM 容器架構](#2-vm-容器架構) 3. [Docker Compose 服務清單](#3-docker-compose-服務清單) 4. [網路與 DNS](#4-網路與-dns) 5. [Nginx 反向代理路由](#5-nginx-反向代理路由) 6. [CI/CD Pipeline (Jenkins + Gitea)](#6-cicd-pipeline-jenkins--gitea) 7. [Azure Storage Account 使用規劃](#7-azure-storage-account-使用規劃) 8. [備份策略](#8-備份策略) 9. [成本估算 (Monthly)](#9-成本估算-monthly) 10. [部署步驟清單](#10-部署步驟清單) --- ## 1. Azure 資源規劃 | 資源 | 規格 | 用途 | 估計月費 | |------|------|------|----------| | **Azure VM** | Standard B2s (2 vCPU / 4 GB RAM) | 主機:所有 Docker 容器 | ~$30–35/月 | | **Azure Storage Account** | LRS, Cool tier | Blob 儲存(照片/PDF/媒體) | ~$2–5/月 | | **Azure Static Public IP** | Standard | 固定對外 IP | ~$4/月 | | **Azure DNS Zone** *(可選)* | — | 管理 DNS 記錄 | ~$0.5/月 | | **OS Disk** | Premium SSD 64 GB | VM 系統碟 | ~$10/月 | | **資料磁碟** | Standard SSD 128 GB | Docker volumes(DB、Gitea、Jenkins) | ~$11/月 | > **總估算: ~$57–65/月**,年約 $700,在 $2,000 Credit 範圍內可運行約 **30 個月**。 > 若流量低,可降級至 **B1ms (1 vCPU / 2 GB)** 節省至 ~$15/月。 --- ## 2. VM 容器架構 ``` Azure VM (Ubuntu 22.04 LTS) │ ├── /data/ ← 資料磁碟掛載點 │ ├── postgres/ ← PostgreSQL data │ ├── gitea/ ← Gitea repos & config │ ├── jenkins/ ← Jenkins home │ └── nginx/certs/ ← SSL 憑證 │ └── Docker Engine │ ├── [nginx] :80, :443 ← 反向代理 + SSL 終止 ├── [angular-app] :4200 ← Angular SPA (nginx serve) ├── [rolac-api] :5000 ← ASP.NET Core API ├── [postgres] :5432 ← PostgreSQL 14+ ├── [gitea] :3000, :22 ← 原始碼管理 └── [jenkins] :8080 ← CI/CD ``` --- ## 3. Docker Compose 服務清單 ```yaml # docker-compose.yml (簡化草圖) version: '3.9' services: nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/certs:/etc/letsencrypt depends_on: - angular-app - rolac-api angular-app: image: rolac/frontend:latest # 由 Jenkins build expose: - "80" restart: unless-stopped rolac-api: image: rolac/api:latest # 由 Jenkins build expose: - "5000" environment: - ConnectionStrings__Default=Host=postgres;... - Azure__StorageAccount__ConnectionString=... - Jwt__Secret=${JWT_SECRET} depends_on: - postgres restart: unless-stopped postgres: image: postgres:16-alpine volumes: - /data/postgres:/var/lib/postgresql/data environment: - POSTGRES_DB=rolac - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} expose: - "5432" restart: unless-stopped gitea: image: gitea/gitea:latest ports: - "2222:22" # Git SSH expose: - "3000" # Web UI (透過 nginx 代理) volumes: - /data/gitea:/data restart: unless-stopped jenkins: image: jenkins/jenkins:lts-jdk21 expose: - "8080" # Web UI (透過 nginx 代理) volumes: - /data/jenkins:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock # Jenkins 可操作 Docker restart: unless-stopped ``` > **Secrets 管理:** 使用 `.env` 檔搭配 `docker-compose --env-file`,**不得** commit 到 Gitea。 --- ## 4. 網路與 DNS ``` Internet │ ▼ Azure Public IP (Static) │ ▼ Azure NSG (Network Security Group) ├── Port 80 (HTTP → 轉 443) ├── Port 443 (HTTPS) └── Port 2222 (Git SSH) │ ▼ Ubuntu VM → Docker → Nginx ``` **DNS 記錄 — rolac.org** | 子域名 | 完整網址 | 類型 | 指向 | 用途 | |--------|----------|------|------|------| | `@` | rolac.org | A | VM Public IP | 教會公開網站(根域名) | | `www` | www.rolac.org | CNAME | rolac.org | 同上(重定向) | | `app` | app.rolac.org | A | VM Public IP | Angular 後台管理入口 | | `api` | api.rolac.org | A | VM Public IP | ASP.NET Core REST API | | `git` | git.rolac.org | A | VM Public IP | Gitea 原始碼管理 | | `ci` | ci.rolac.org | A | VM Public IP | Jenkins CI/CD | --- ## 5. Nginx 反向代理路由 ```nginx # /nginx/conf.d/rolac.conf (草圖) # HTTP → HTTPS redirect server { listen 80; server_name *.rolac.org rolac.org; return 301 https://$host$request_uri; } # 教會公開網站 (Angular SSR or static) server { listen 443 ssl; server_name rolac.org www.rolac.org; location / { proxy_pass http://angular-app:80; } } # 後台管理 App server { listen 443 ssl; server_name app.rolac.org; location / { proxy_pass http://angular-app:80; } } # API server { listen 443 ssl; server_name api.rolac.org; location / { proxy_pass http://rolac-api:5000; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; } } # Gitea server { listen 443 ssl; server_name git.rolac.org; location / { proxy_pass http://gitea:3000; } } # Jenkins server { listen 443 ssl; server_name ci.rolac.org; location / { proxy_pass http://jenkins:8080; } } ``` **SSL 憑證 (Let's Encrypt)** ```bash certbot certonly --webroot -w /data/nginx/www \ -d rolac.org \ -d www.rolac.org \ -d app.rolac.org \ -d api.rolac.org \ -d git.rolac.org \ -d ci.rolac.org ``` --- ## 6. CI/CD Pipeline (Jenkins + Gitea) ### 流程圖 ``` 開發者 push → Gitea │ │ Webhook (HTTP POST) ▼ Jenkins Job │ ┌───────────┼───────────┐ ▼ ▼ ▼ Build Test Lint Docker dotnet ng lint image test │ ▼ Push image (Web) ← rolac.org & app.rolac.org 用 docker compose up -d │ ▼ (main branch only) Capacitor Build ← 產出 iOS .ipa / Android .apk npx cap build ios npx cap build android │ ▼ Health check 通知結果 (Email) ``` > **注意:** Capacitor build (iOS) 需要 macOS 環境,Jenkins 跑在 Linux VM 上只能產 Android APK;iOS `.ipa` 需在 Mac 上另外 build 或使用 Xcode Cloud。 ### Jenkinsfile 草圖 ```groovy pipeline { agent any environment { API_IMAGE = "rolac/api:${BUILD_NUMBER}" WEB_IMAGE = "rolac/frontend:${BUILD_NUMBER}" } stages { stage('Checkout') { steps { checkout scm } } stage('Test API') { steps { sh 'dotnet test ./backend/ROLAC.Tests' } } stage('Build Images') { parallel { stage('API') { steps { sh 'docker build -t ${API_IMAGE} -t rolac/api:latest ./backend' } } stage('Frontend (Web)') { steps { sh 'docker build -t ${WEB_IMAGE} -t rolac/frontend:latest ./frontend' } } } } stage('Deploy Web') { when { branch 'main' } steps { sh 'docker compose -f /opt/rolac/docker-compose.yml up -d --no-deps rolac-api angular-app' } } stage('Build Android APK') { when { branch 'main' } steps { dir('frontend') { sh 'npm ci' sh 'npm run build -- --configuration production' sh 'npx cap sync android' sh 'cd android && ./gradlew assembleRelease' } archiveArtifacts artifacts: 'frontend/android/app/build/outputs/apk/release/*.apk' } } } post { failure { mail to: 'admin@rolac.org', subject: "Build FAILED: ${env.JOB_NAME} #${BUILD_NUMBER}" } success { mail to: 'admin@rolac.org', subject: "Build OK: ${env.JOB_NAME} #${BUILD_NUMBER}" } } } ``` ### Branch 策略 | Branch | 說明 | 部署 | |--------|------|------| | `main` | 正式版本 | 自動部署 Production | | `develop` | 開發整合 | 自動部署 Staging (同 VM,不同 port) | | `feature/*` | 功能開發 | 僅跑 Build + Test,不部署 | | `hotfix/*` | 緊急修復 | 審核後 merge 至 main | --- ## 7. Azure Storage Account 使用規劃 ### Container (Blob) 結構 ``` rolac-storage (Storage Account) ├── members/ │ ├── photos/ ← 教友大頭照 │ └── documents/ ← 個人相關文件 ├── receipts/ │ └── {year}/ ← 年度奉獻收據 PDF ├── cms/ │ ├── images/ ← 網站上傳圖片 │ └── attachments/ ← 公告附件 └── reports/ └── exports/ ← 匯出的報表 PDF/CSV ``` ### 存取控制 | Container | 存取層級 | 說明 | |-----------|----------|------| | `members/` | **Private** | 僅 API 透過 SAS Token 存取 | | `receipts/` | **Private** | API 產生限時下載連結給教友 | | `cms/images/` | **Blob (Public Read)** | 網站圖片可公開存取 | | `reports/` | **Private** | 僅管理員 API 存取 | ### C# 整合 ```csharp // 使用 Azure.Storage.Blobs SDK // 上傳教友照片範例 var blobClient = new BlobContainerClient(connectionString, "members"); await blobClient.UploadBlobAsync($"photos/{memberId}.jpg", fileStream); // 產生限時 SAS URL (收據下載) var sasUri = blobClient.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddHours(1)); ``` --- ## 8. 備份策略 ### PostgreSQL 備份 ```bash # crontab (每日 02:00 UTC 備份) 0 2 * * * docker exec postgres pg_dump -U rolac_user rolac \ | gzip > /data/backups/rolac_$(date +%Y%m%d).sql.gz # 保留 30 天 find /data/backups -name "*.sql.gz" -mtime +30 -delete ``` ### 備份上傳至 Azure Blob ```bash # 使用 azcopy 或 Azure CLI 將備份上傳 azcopy copy "/data/backups/rolac_$(date +%Y%m%d).sql.gz" \ "https://rolacstorage.blob.core.windows.net/backups/" ``` ### 備份範圍 | 資料 | 方式 | 頻率 | 保留 | |------|------|------|------| | PostgreSQL DB | pg_dump → Azure Blob | 每日 | 30 天 | | Gitea repos | volume backup → Azure Blob | 每日 | 14 天 | | Jenkins config | volume backup | 每週 | 4 週 | | Azure Blob 本身 | Azure Soft Delete | 自動 | 7 天 | --- ## 9. 成本估算 (Monthly) | 項目 | 規格 | 月費 (USD) | |------|------|-----------| | Azure VM | Standard_B2s | $30 | | OS Disk | Premium SSD 64 GB | $10 | | 資料磁碟 | Standard SSD 128 GB | $11 | | Static Public IP | Standard | $4 | | Storage Account | LRS 50 GB | $2 | | 出站流量 | ~10 GB/月 | $1 | | **合計** | | **~$58/月** | > 💡 **$2,000 nonprofit credit → 約 34 個月 (接近 3 年)** > 如規模成長,可升級 VM 規格但仍在 credit 範圍內。 --- ## 10. 部署步驟清單 ### Phase 0 — Azure 環境建置 - [ ] 申請 Microsoft Nonprofit Azure 帳號 - [ ] 建立 Resource Group: `rg-rolac-prod` - [ ] 建立 Azure VM (Ubuntu 22.04, Standard_B2s) - [ ] 建立資料磁碟 (128 GB) 並掛載至 `/data` - [ ] 設定 NSG (開放 80, 443, 2222) - [ ] 建立 Static Public IP 並綁定 - [ ] 建立 Storage Account (`rolacstorage`) - [ ] 設定 DNS A 記錄指向 VM IP ### Phase 0 — VM 環境初始化 - [ ] 安裝 Docker Engine + Docker Compose - [ ] 安裝 Certbot (Let's Encrypt) - [ ] 建立目錄結構 (`/data/postgres`, `/data/gitea`, `/data/jenkins`) - [ ] 啟動 Gitea 容器,建立組織與 Repo - [ ] 啟動 Jenkins 容器,安裝 Plugins (Git, Docker, Pipeline) - [ ] 設定 Gitea Webhook → Jenkins - [ ] 申請 SSL 憑證 (certbot) - [ ] 啟動 Nginx 容器,配置反向代理 ### Phase 1+ — 每次 Release - [ ] Push code to Gitea `main` branch - [ ] Jenkins 自動觸發 Build → Test → Deploy - [ ] 確認 Health Check endpoint 回應正常 - [ ] 必要時執行 EF Core Migration: `dotnet ef database update`