經過前端和後端的多租戶架構建立,今天我們要將整個系統部署到 AWS 雲端。這不是簡單的單一應用部署,而是要實作支援多租戶的生產級基礎設施。我們將使用 ECS 進行容器化部署、RDS 管理多個租戶資料庫、ElastiCache 提供 Redis 叢集,並建立完整的 CI/CD 流程。
我們的 AWS 架構採用微服務和多可用區設計,確保高可用性和擴展性:
┌─────────────────────── AWS Cloud ───────────────────────┐
│ ┌─── VPC (Multi-AZ) ───┐ │
│ │ │ ┌── CloudFront CDN ──┐ │
│ │ ┌── Public Subnet ──┤ │ │ │
│ │ │ │ │ ┌─ S3 Static ────┤ │
│ │ │ ┌─ ALB ─────────┐│ │ │ Web Assets │ │
│ │ │ │ ││ │ └─────────────────┤ │
│ │ │ └───────────────┘│ │ │ │
│ │ │ │ └────────────────────┘ │
│ │ └───────────────────┤ │
│ │ │ │
│ │ ┌── Private Subnet ─┤ ┌── ECS Cluster ─────┐ │
│ │ │ │ │ │ │
│ │ │ ┌─ ECS Service ──┤ │ ┌─ API Service ───┤ │
│ │ │ │ (Auto Scaling) ││ │ │ (Multi-AZ) │ │
│ │ │ └───────────────┘│ │ └─────────────────┤ │
│ │ │ │ │ │ │
│ │ │ ┌─ ECS Service ──┤ │ ┌─ Dashboard ─────┤ │
│ │ │ │ (Auto Scaling) ││ │ │ Service │ │
│ │ │ └───────────────┘│ │ └─────────────────┤ │
│ │ │ │ └────────────────────┘ │
│ │ └───────────────────┤ │
│ │ │ │
│ │ ┌── Database Subnet ┤ ┌── RDS Multi-AZ ────┐ │
│ │ │ │ │ │ │
│ │ │ ┌─ Master DB ────┤ │ ┌─ Tenant DB 1 ───┤ │
│ │ │ │ (PostgreSQL) ││ │ │ (PostgreSQL) │ │
│ │ │ └───────────────┘│ │ └─────────────────┤ │
│ │ │ │ │ │ │
│ │ │ ┌─ Tenant DBs ───┤ │ ┌─ Tenant DB N ───┤ │
│ │ │ │ (Auto-provisioned)││ │ (Auto-scaling) │ │
│ │ │ └───────────────┘│ │ └─────────────────┤ │
│ │ │ │ │ │ │
│ │ │ ┌─ ElastiCache ──┤ │ ┌─ Redis Cluster ─┤ │
│ │ │ │ (Redis Cluster)││ │ │ (Multi-AZ) │ │
│ │ │ └───────────────┘│ │ └─────────────────┤ │
│ │ └───────────────────┤ └────────────────────┘ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────────────┘
首先,我們建立 Docker 配置文件來容器化我們的應用:
# apps/kyo-otp-service/Dockerfile
FROM node:18-alpine AS builder
# 安裝 pnpm
RUN npm install -g pnpm
WORKDIR /app
# 複製 workspace 配置
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY tsconfig.base.json ./
# 複製所有 packages
COPY packages/ ./packages/
# 複製 API 服務源碼
COPY apps/kyo-otp-service/ ./apps/kyo-otp-service/
# 安裝依賴
RUN pnpm install --frozen-lockfile
# 建構共享套件
RUN pnpm --filter "@kyong/*" build
# 建構 API 服務
RUN pnpm --filter kyo-otp-service build
# 生產階段
FROM node:18-alpine AS runtime
# 建立非 root 使用者
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# 安裝 pnpm
RUN npm install -g pnpm
WORKDIR /app
# 複製建構結果
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/packages ./packages
COPY --from=builder --chown=nodejs:nodejs /app/apps/kyo-otp-service ./apps/kyo-otp-service
COPY --from=builder --chown=nodejs:nodejs /app/pnpm-workspace.yaml ./
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
USER nodejs
EXPOSE 3000
# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "apps/kyo-otp-service/dist/index.js"]
# apps/kyo-dashboard/Dockerfile
FROM node:18-alpine AS builder
RUN npm install -g pnpm
WORKDIR /app
# 複製 workspace 配置
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY tsconfig.base.json ./
# 複製所有 packages 和 dashboard
COPY packages/ ./packages/
COPY apps/kyo-dashboard/ ./apps/kyo-dashboard/
# 安裝依賴並建構
RUN pnpm install --frozen-lockfile
RUN pnpm --filter "@kyong/*" build
RUN pnpm --filter kyo-dashboard build
# Nginx 靜態伺服器
FROM nginx:alpine AS runtime
# 複製建構結果到 nginx
COPY --from=builder /app/apps/kyo-dashboard/dist /usr/share/nginx/html
# 複製 nginx 配置
COPY apps/kyo-dashboard/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# apps/kyo-dashboard/nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip 壓縮
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA 路由支援
location / {
try_files $uri $uri/ /index.html;
}
# API 代理
location /api/ {
proxy_pass http://api-service:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 靜態資源快取
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 安全標頭
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
}
}
我們使用 AWS CDK (Cloud Development Kit) 來定義基礎設施:
// infrastructure/lib/ecs-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
export class EcsStack extends cdk.Stack {
public readonly cluster: ecs.Cluster;
public readonly vpc: ec2.Vpc;
public readonly loadBalancer: elbv2.ApplicationLoadBalancer;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC 建立 - 跨三個可用區
this.vpc = new ec2.Vpc(this, 'KyoVpc', {
maxAzs: 3,
natGateways: 2, // 高可用性但成本控制
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 28,
name: 'Database',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
// ECS 集群建立
this.cluster = new ecs.Cluster(this, 'KyoCluster', {
vpc: this.vpc,
clusterName: 'kyo-production-cluster',
// 啟用容器洞察
containerInsights: true,
});
// 自動擴展群組 (使用 Spot 實例降低成本)
const autoScalingGroup = this.cluster.addCapacity('DefaultAutoScalingGroup', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
minCapacity: 2,
maxCapacity: 10,
desiredCapacity: 3,
// 混合 On-Demand 和 Spot 實例
spotPrice: '0.05',
spotInstanceDraining: cdk.Duration.minutes(5),
});
// 應用程式負載均衡器
this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'KyoLoadBalancer', {
vpc: this.vpc,
internetFacing: true,
loadBalancerName: 'kyo-production-alb',
});
// 建立安全群組
this.createSecurityGroups();
}
private createSecurityGroups() {
// ALB 安全群組
const albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
vpc: this.vpc,
description: 'Security group for Application Load Balancer',
allowAllOutbound: true,
});
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP traffic'
);
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS traffic'
);
// ECS 服務安全群組
const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
vpc: this.vpc,
description: 'Security group for ECS services',
allowAllOutbound: true,
});
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(3000),
'Allow traffic from ALB to API service'
);
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(80),
'Allow traffic from ALB to dashboard'
);
}
}
// infrastructure/lib/rds-stack.ts
export class RdsStack extends cdk.Stack {
public readonly masterDatabase: rds.DatabaseInstance;
public readonly subnetGroup: rds.SubnetGroup;
constructor(scope: Construct, id: string, vpc: ec2.Vpc, props?: cdk.StackProps) {
super(scope, id, props);
// 資料庫子網群組
this.subnetGroup = new rds.SubnetGroup(this, 'DatabaseSubnetGroup', {
vpc,
description: 'Subnet group for RDS databases',
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
});
// 主要資料庫 (管理租戶資訊)
this.masterDatabase = new rds.DatabaseInstance(this, 'MasterDatabase', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15_4,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
credentials: rds.Credentials.fromGeneratedSecret('postgres', {
secretName: 'kyo-master-db-credentials',
}),
vpc,
subnetGroup: this.subnetGroup,
databaseName: 'kyo_master',
allocatedStorage: 20,
storageType: rds.StorageType.GP2,
multiAz: true, // 高可用性
deletionProtection: true,
backupRetention: cdk.Duration.days(7),
monitoringInterval: cdk.Duration.minutes(1),
enablePerformanceInsights: true,
cloudwatchLogsExports: ['postgresql'],
});
// 租戶資料庫建立 Lambda 函數
this.createTenantProvisioningLambda(vpc);
// 自動備份和維護
this.setupBackupStrategy();
}
private createTenantProvisioningLambda(vpc: ec2.Vpc) {
// Lambda 函數用於動態建立租戶資料庫
const tenantProvisioningFunction = new lambda.Function(this, 'TenantProvisioning', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/tenant-provisioning'),
vpc,
environment: {
MASTER_DB_ENDPOINT: this.masterDatabase.instanceEndpoint.hostname,
MASTER_DB_SECRET_ARN: this.masterDatabase.secret!.secretArn,
},
timeout: cdk.Duration.minutes(5),
});
// 授權 Lambda 存取 RDS
this.masterDatabase.secret!.grantRead(tenantProvisioningFunction);
// API Gateway 觸發器
const api = new apigateway.RestApi(this, 'TenantProvisioningApi', {
restApiName: 'Tenant Provisioning Service',
});
const integration = new apigateway.LambdaIntegration(tenantProvisioningFunction);
api.root.addResource('provision-tenant').addMethod('POST', integration);
}
private setupBackupStrategy() {
// AWS Backup 計劃
const backupPlan = backup.BackupPlan.dailyWeeklyMonthly5YearRetention(
this,
'DatabaseBackupPlan'
);
// 備份選擇 - 包含所有 RDS 實例
backupPlan.addSelection('DatabaseBackupSelection', {
resources: [
backup.BackupResource.fromRdsDatabase(this.masterDatabase),
],
allowRestores: true,
});
// 跨區域備份 (災難恢復)
new backup.BackupVault(this, 'DisasterRecoveryVault', {
backupVaultName: 'kyo-disaster-recovery',
encryptionKey: kms.Key.fromLookup(this, 'BackupKey', {
aliasName: 'alias/aws/backup',
}),
});
}
}
// lambda/tenant-provisioning/index.ts
import { RDSClient, CreateDBInstanceCommand } from '@aws-sdk/client-rds';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
export async function handler(event: any) {
const { tenantId, plan, region } = JSON.parse(event.body);
try {
// 驗證請求
if (!tenantId || !plan) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing required parameters' }),
};
}
// 根據方案決定資料庫規格
const dbSpecs = getDbSpecsForPlan(plan);
// 建立租戶資料庫
const dbInstance = await createTenantDatabase(tenantId, dbSpecs);
// 初始化資料庫結構
await initializeTenantSchema(tenantId, dbInstance.endpoint);
// 記錄到主資料庫
await recordTenantInMaster(tenantId, {
databaseEndpoint: dbInstance.endpoint,
plan,
status: 'active',
createdAt: new Date(),
});
return {
statusCode: 200,
body: JSON.stringify({
success: true,
tenantId,
databaseEndpoint: dbInstance.endpoint,
message: 'Tenant database created successfully',
}),
};
} catch (error) {
console.error('Tenant provisioning failed:', error);
return {
statusCode: 500,
body: JSON.stringify({
error: 'Failed to provision tenant database',
details: error.message,
}),
};
}
}
function getDbSpecsForPlan(plan: string) {
const specs = {
free: {
instanceClass: 'db.t3.micro',
allocatedStorage: 10,
maxConnections: 20,
backupEnabled: false,
},
basic: {
instanceClass: 'db.t3.small',
allocatedStorage: 20,
maxConnections: 50,
backupEnabled: true,
},
premium: {
instanceClass: 'db.t3.medium',
allocatedStorage: 100,
maxConnections: 200,
backupEnabled: true,
},
enterprise: {
instanceClass: 'db.r5.large',
allocatedStorage: 500,
maxConnections: 1000,
backupEnabled: true,
multiAz: true,
},
};
return specs[plan] || specs.free;
}
async function createTenantDatabase(tenantId: string, specs: any) {
const rdsClient = new RDSClient({ region: process.env.AWS_REGION });
const dbInstanceIdentifier = `kyo-tenant-${tenantId}`;
const command = new CreateDBInstanceCommand({
DBInstanceIdentifier: dbInstanceIdentifier,
DBInstanceClass: specs.instanceClass,
Engine: 'postgres',
EngineVersion: '15.4',
MasterUsername: 'postgres',
MasterUserPassword: generateSecurePassword(),
AllocatedStorage: specs.allocatedStorage,
VpcSecurityGroupIds: [process.env.DB_SECURITY_GROUP_ID],
DBSubnetGroupName: process.env.DB_SUBNET_GROUP,
MultiAZ: specs.multiAz || false,
BackupRetentionPeriod: specs.backupEnabled ? 7 : 0,
StorageEncrypted: true,
MonitoringInterval: 60,
EnablePerformanceInsights: true,
DeletionProtection: specs.plan === 'enterprise',
Tags: [
{ Key: 'TenantId', Value: tenantId },
{ Key: 'Plan', Value: specs.plan },
{ Key: 'Environment', Value: 'production' },
],
});
const response = await rdsClient.send(command);
// 等待資料庫可用
await waitForDatabaseAvailable(rdsClient, dbInstanceIdentifier);
return {
identifier: dbInstanceIdentifier,
endpoint: response.DBInstance?.Endpoint?.Address,
};
}
// infrastructure/lib/cache-stack.ts
export class CacheStack extends cdk.Stack {
public readonly redisCluster: elasticache.CfnReplicationGroup;
constructor(scope: Construct, id: string, vpc: ec2.Vpc, props?: cdk.StackProps) {
super(scope, id, props);
// Redis 子網群組
const redisSubnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {
description: 'Subnet group for Redis cluster',
subnetIds: vpc.privateSubnets.map(subnet => subnet.subnetId),
});
// Redis 安全群組
const redisSecurityGroup = new ec2.SecurityGroup(this, 'RedisSecurityGroup', {
vpc,
description: 'Security group for Redis cluster',
allowAllOutbound: false,
});
redisSecurityGroup.addIngressRule(
ec2.Peer.ipv4(vpc.vpcCidrBlock),
ec2.Port.tcp(6379),
'Allow Redis access from VPC'
);
// Redis 叢集 - 跨可用區部署
this.redisCluster = new elasticache.CfnReplicationGroup(this, 'RedisCluster', {
replicationGroupDescription: 'Kyo production Redis cluster',
cacheNodeType: 'cache.r6g.large',
engine: 'redis',
engineVersion: '7.0',
numCacheClusters: 3, // 1 primary + 2 replica
port: 6379,
cacheSubnetGroupName: redisSubnetGroup.ref,
securityGroupIds: [redisSecurityGroup.securityGroupId],
// 高可用性配置
multiAzEnabled: true,
automaticFailoverEnabled: true,
// 備份配置
snapshotRetentionLimit: 7,
snapshotWindow: '03:00-05:00',
preferredMaintenanceWindow: 'sun:05:00-sun:07:00',
// 監控
notificationTopicArn: this.createCloudWatchTopic().topicArn,
// 加密
atRestEncryptionEnabled: true,
transitEncryptionEnabled: true,
// 標籤
tags: [
{ key: 'Environment', value: 'production' },
{ key: 'Service', value: 'kyo-cache' },
],
});
// CloudWatch 告警
this.createRedisAlarms();
}
private createRedisAlarms() {
// CPU 使用率告警
new cloudwatch.Alarm(this, 'RedisCpuAlarm', {
alarmName: 'Redis-High-CPU',
metric: new cloudwatch.Metric({
namespace: 'AWS/ElastiCache',
metricName: 'CPUUtilization',
dimensionsMap: {
CacheClusterId: this.redisCluster.ref,
},
}),
threshold: 80,
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.BREACHING,
});
// 記憶體使用率告警
new cloudwatch.Alarm(this, 'RedisMemoryAlarm', {
alarmName: 'Redis-High-Memory',
metric: new cloudwatch.Metric({
namespace: 'AWS/ElastiCache',
metricName: 'DatabaseMemoryUsagePercentage',
dimensionsMap: {
CacheClusterId: this.redisCluster.ref,
},
}),
threshold: 85,
evaluationPeriods: 2,
});
}
}
使用 GitHub Actions 建立自動化部署流程:
# .github/workflows/deploy.yml
name: Deploy to AWS
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: ap-northeast-1
ECR_REPOSITORY_API: kyo-api-service
ECR_REPOSITORY_DASHBOARD: kyo-dashboard-service
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm run build
- name: Run tests
run: pnpm run test
- name: Run linting
run: pnpm run lint
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
strategy:
matrix:
service: [api, dashboard]
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push API service
if: matrix.service == 'api'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file apps/kyo-otp-service/Dockerfile \
--tag $ECR_REGISTRY/$ECR_REPOSITORY_API:$IMAGE_TAG \
--tag $ECR_REGISTRY/$ECR_REPOSITORY_API:latest \
--push .
- name: Build and push Dashboard service
if: matrix.service == 'dashboard'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file apps/kyo-dashboard/Dockerfile \
--tag $ECR_REGISTRY/$ECR_REPOSITORY_DASHBOARD:$IMAGE_TAG \
--tag $ECR_REGISTRY/$ECR_REPOSITORY_DASHBOARD:latest \
--push .
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to ECS
env:
IMAGE_TAG: ${{ github.sha }}
run: |
# 更新 API 服務
aws ecs update-service \
--cluster kyo-production-cluster \
--service kyo-api-service \
--force-new-deployment
# 更新 Dashboard 服務
aws ecs update-service \
--cluster kyo-production-cluster \
--service kyo-dashboard-service \
--force-new-deployment
# 等待部署完成
aws ecs wait services-stable \
--cluster kyo-production-cluster \
--services kyo-api-service kyo-dashboard-service
notify:
needs: [test, build-and-push, deploy]
runs-on: ubuntu-latest
if: always()
steps:
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
// infrastructure/lib/monitoring-stack.ts
export class MonitoringStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// CloudWatch Dashboard
const dashboard = new cloudwatch.Dashboard(this, 'KyoDashboard', {
dashboardName: 'Kyo-Production-Metrics',
});
// ECS 服務監控
this.addEcsMetrics(dashboard);
// RDS 監控
this.addRdsMetrics(dashboard);
// ElastiCache 監控
this.addCacheMetrics(dashboard);
// 應用程式層級監控
this.addApplicationMetrics(dashboard);
// SNS 通知主題
const alertTopic = new sns.Topic(this, 'AlertTopic', {
topicName: 'kyo-production-alerts',
});
// 建立告警
this.createAlarms(alertTopic);
}
private addApplicationMetrics(dashboard: cloudwatch.Dashboard) {
// API 回應時間
const apiLatencyWidget = new cloudwatch.GraphWidget({
title: 'API Response Time',
left: [new cloudwatch.Metric({
namespace: 'Kyo/API',
metricName: 'ResponseTime',
statistic: 'Average',
})],
});
// 錯誤率
const errorRateWidget = new cloudwatch.GraphWidget({
title: 'Error Rate',
left: [new cloudwatch.Metric({
namespace: 'Kyo/API',
metricName: 'ErrorRate',
statistic: 'Sum',
})],
});
// 租戶活躍度
const tenantActivityWidget = new cloudwatch.GraphWidget({
title: 'Active Tenants',
left: [new cloudwatch.Metric({
namespace: 'Kyo/Tenants',
metricName: 'ActiveTenants',
statistic: 'Maximum',
})],
});
dashboard.addWidgets(
apiLatencyWidget,
errorRateWidget,
tenantActivityWidget
);
}
private createAlarms(alertTopic: sns.Topic) {
// API 高錯誤率告警
new cloudwatch.Alarm(this, 'HighErrorRate', {
alarmName: 'Kyo-High-Error-Rate',
metric: new cloudwatch.Metric({
namespace: 'Kyo/API',
metricName: 'ErrorRate',
statistic: 'Sum',
}),
threshold: 5, // 5% 錯誤率
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
}).addAlarmAction(new cloudwatchActions.SnsAction(alertTopic));
// 資料庫連線失敗告警
new cloudwatch.Alarm(this, 'DatabaseConnectionFailure', {
alarmName: 'Kyo-Database-Connection-Failure',
metric: new cloudwatch.Metric({
namespace: 'AWS/RDS',
metricName: 'DatabaseConnections',
statistic: 'Maximum',
}),
threshold: 80, // 80% 連線使用率
evaluationPeriods: 3,
}).addAlarmAction(new cloudwatchActions.SnsAction(alertTopic));
}
}
// 使用 Spot 實例降低 ECS 成本
const spotCapacityProvider = new ecs.AsgCapacityProvider(this, 'SpotCapacityProvider', {
autoScalingGroup: spotAutoScalingGroup,
enableManagedScaling: true,
enableManagedTerminationProtection: false,
spotInstanceDraining: cdk.Duration.minutes(2),
});
cluster.addAsgCapacityProvider(spotCapacityProvider);
// 混合容量策略
const service = new ecs.FargateService(this, 'ApiService', {
cluster,
taskDefinition,
capacityProviderStrategies: [
{
capacityProvider: 'FARGATE',
weight: 1,
base: 2, // 保證最少 2 個 Fargate 實例
},
{
capacityProvider: spotCapacityProvider.capacityProviderName,
weight: 4, // 80% 使用 Spot 實例
},
],
});
// 租戶資料庫自動縮放
const dbScalingTarget = new applicationautoscaling.ScalableTarget(this, 'DbScalingTarget', {
serviceNamespace: applicationautoscaling.ServiceNamespace.RDS,
scalableDimension: 'rds:db-instance:ReadReplicaCount',
resourceId: `db:${dbInstance.instanceIdentifier}`,
minCapacity: 1,
maxCapacity: 5,
});
// 基於 CPU 的自動縮放
dbScalingTarget.scaleToTrackMetric('CpuScaling', {
targetValue: 70,
predefinedMetric: applicationautoscaling.PredefinedMetric.RDS_READER_AVERAGE_CPU_UTILIZATION,
});
// WAF 保護
const webAcl = new wafv2.CfnWebACL(this, 'WebAcl', {
scope: 'CLOUDFRONT',
defaultAction: { allow: {} },
rules: [
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'CommonRuleSetMetric',
},
},
{
name: 'RateLimitRule',
priority: 2,
statement: {
rateBasedStatement: {
limit: 2000,
aggregateKeyType: 'IP',
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'RateLimitMetric',
},
},
],
});
// KMS 金鑰管理
const kmsKey = new kms.Key(this, 'KyoEncryptionKey', {
description: 'Kyo production encryption key',
enableKeyRotation: true,
keyRotation: kms.KeyRotation.ENABLED,
});
// 資料庫加密
const database = new rds.DatabaseInstance(this, 'Database', {
// ... 其他配置
storageEncrypted: true,
storageEncryptionKey: kmsKey,
});
// ECS 任務定義加密
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
// ... 其他配置
volumes: [{
name: 'encrypted-volume',
efsVolumeConfiguration: {
fileSystemId: efsFileSystem.fileSystemId,
transitEncryption: 'ENABLED',
authorizationConfig: {
accessPointId: accessPoint.accessPointId,
},
},
}],
});
// 跨區域資料複製
const crossRegionBackup = new backup.BackupPlan(this, 'CrossRegionBackup', {
backupPlanName: 'kyo-cross-region-backup',
backupPlanRules: [
backup.BackupPlanRule.monthly5Year(backup.BackupVault.fromBackupVaultName(
this,
'PrimaryVault',
'primary-backup-vault'
)),
],
});
// 自動故障轉移
const route53HealthCheck = new route53.HealthCheck(this, 'HealthCheck', {
type: route53.HealthCheckType.HTTPS,
resourcePath: '/health',
fqdn: 'api.kyo.app',
port: 443,
requestInterval: 30,
failureThreshold: 3,
});
#!/bin/bash
# scripts/disaster-recovery.sh
set -e
BACKUP_REGION="us-west-2"
PRIMARY_REGION="ap-northeast-1"
RECOVERY_TIME_OBJECTIVE="4h"
echo "Starting disaster recovery process..."
# 1. 檢查主要區域狀態
if aws elbv2 describe-target-health --target-group-arn $PRIMARY_ALB_ARN --region $PRIMARY_REGION; then
echo "Primary region is healthy, no action needed"
exit 0
fi
# 2. 啟動備援區域資源
echo "Primary region is down, activating backup region..."
# 啟動備援 ECS 服務
aws ecs update-service \
--cluster backup-cluster \
--service kyo-api-service \
--desired-count 3 \
--region $BACKUP_REGION
# 恢復資料庫
LATEST_SNAPSHOT=$(aws rds describe-db-snapshots \
--db-instance-identifier kyo-master \
--query 'DBSnapshots[0].DBSnapshotIdentifier' \
--output text \
--region $BACKUP_REGION)
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier kyo-master-recovery \
--db-snapshot-identifier $LATEST_SNAPSHOT \
--region $BACKUP_REGION
# 3. 更新 DNS 記錄指向備援區域
aws route53 change-resource-record-sets \
--hosted-zone-id $HOSTED_ZONE_ID \
--change-batch file://dns-failover.json
echo "Disaster recovery completed. Services are now running in backup region."
我們今天建立了完整的 AWS 生產級部署架構: