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>
This commit is contained in:
Chris Chen
2026-05-24 20:54:10 -07:00
commit 9b28fbcfb6
11 changed files with 4215 additions and 0 deletions
+462
View File
@@ -0,0 +1,462 @@
# 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`