Files
ROLAC/docs/INFRASTRUCTURE.md
T
Chris Chen 9b28fbcfb6 Initial commit: monorepo scaffold for ROLAC
- Add .gitignore covering C#/.NET and Angular/Node
- Add placeholder structure for API (C#) and APP (Angular)
- Add project docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:54:10 -07:00

463 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 容器 | ~$3035/月 |
| **Azure Storage Account** | LRS, Cool tier | Blob 儲存(照片/PDF/媒體) | ~$25/月 |
| **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 volumesDB、Gitea、Jenkins | ~$11/月 |
> **總估算: ~$5765/月**,年約 $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 APKiOS `.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`