今天要學習 GitHub Environment 的設定,這就像為我們的農場建立不同的管理區域。還記得爸爸把農場分成試驗田、正式農田和種子保存區嗎?每個區域都有不同的管理規則和存取權限,GitHub Environment 就是這個概念!
GitHub Environment 是專案中的邏輯環境劃分:
# Repository Settings > Environments > development
name: development
protection_rules: [] # 無保護規則,快速部署
deployment_branch_policy:
protected_branches: false
custom_branch_policies: true
custom_branches:
- "feature/*"
- "develop"
variables:
API_BASE_URL: "https://dev-api.trading.example.com"
LOG_LEVEL: "DEBUG"
DEPLOYMENT_TIMEOUT: "300"
INSTANCE_TYPE: "t3.micro"
MIN_CAPACITY: "1"
MAX_CAPACITY: "2"
secrets:
AWS_ACCESS_KEY_ID: "AKIA..."
AWS_SECRET_ACCESS_KEY: "..."
BYBIT_API_KEY: "dev_api_key"
BYBIT_SECRET_KEY: "dev_secret"
DATABASE_URL: "postgresql://dev_user:pass@dev-db:5432/trading_dev"
REDIS_URL: "redis://dev-redis:6379/0"
# Repository Settings > Environments > staging
name: staging
protection_rules:
- type: wait_timer
wait_timer: 5 # 等待 5 分鐘
- type: required_reviewers
required_reviewers:
- "qa-team"
deployment_branch_policy:
protected_branches: false
custom_branch_policies: true
custom_branches:
- "develop"
- "release/*"
variables:
API_BASE_URL: "https://staging-api.trading.example.com"
LOG_LEVEL: "INFO"
DEPLOYMENT_TIMEOUT: "600"
INSTANCE_TYPE: "t3.small"
MIN_CAPACITY: "1"
MAX_CAPACITY: "3"
secrets:
AWS_ACCESS_KEY_ID: "AKIA..."
AWS_SECRET_ACCESS_KEY: "..."
BYBIT_API_KEY: "staging_api_key"
BYBIT_SECRET_KEY: "staging_secret"
DATABASE_URL: "postgresql://staging_user:pass@staging-db:5432/trading_staging"
REDIS_URL: "redis://staging-redis:6379/0"
# Repository Settings > Environments > production
name: production
protection_rules:
- type: required_reviewers
required_reviewers:
- "senior-developers"
- "devops-team"
- type: wait_timer
wait_timer: 10 # 等待 10 分鐘
- type: branch_policy
branch_policy:
protected_branches: true
deployment_branch_policy:
protected_branches: true
custom_branch_policies: false
variables:
API_BASE_URL: "https://api.trading.example.com"
LOG_LEVEL: "WARNING"
DEPLOYMENT_TIMEOUT: "900"
INSTANCE_TYPE: "c5.large"
MIN_CAPACITY: "2"
MAX_CAPACITY: "10"
secrets:
AWS_ACCESS_KEY_ID: "AKIA..."
AWS_SECRET_ACCESS_KEY: "..."
BYBIT_API_KEY: "prod_api_key"
BYBIT_SECRET_KEY: "prod_secret"
DATABASE_URL: "postgresql://prod_user:pass@prod-db:5432/trading_prod"
REDIS_URL: "redis://prod-redis:6379/0"
TELEGRAM_BOT_TOKEN: "..."
TELEGRAM_CHAT_ID: "..."
# .github/workflows/deploy-environments.yml
name: Multi-Environment Deployment
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- 'Dockerfile'
- 'requirements.txt'
pull_request:
branches: [ main ]
types: [ closed ]
jobs:
# 決定目標環境
determine-environment:
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.env.outputs.environment }}
should-deploy: ${{ steps.env.outputs.should-deploy }}
steps:
- name: Determine target environment
id: env
run: |
if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.action }}" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then
if [[ "${{ github.event.pull_request.base.ref }}" == "main" ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "should-deploy=true" >> $GITHUB_OUTPUT
fi
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "should-deploy=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT
echo "should-deploy=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" =~ refs/heads/feature/ ]]; then
echo "environment=development" >> $GITHUB_OUTPUT
echo "should-deploy=true" >> $GITHUB_OUTPUT
else
echo "should-deploy=false" >> $GITHUB_OUTPUT
fi
# 部署到目標環境
deploy:
runs-on: ubuntu-latest
needs: determine-environment
if: needs.determine-environment.outputs.should-deploy == 'true'
environment:
name: ${{ needs.determine-environment.outputs.environment }}
url: ${{ vars.API_BASE_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Set environment-specific variables
run: |
echo "Deploying to: ${{ needs.determine-environment.outputs.environment }}"
echo "API Base URL: ${{ vars.API_BASE_URL }}"
echo "Instance Type: ${{ vars.INSTANCE_TYPE }}"
echo "Min Capacity: ${{ vars.MIN_CAPACITY }}"
echo "Max Capacity: ${{ vars.MAX_CAPACITY }}"
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push Docker image
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: trading-bot-${{ needs.determine-environment.outputs.environment }}
IMAGE_TAG: ${{ github.sha }}
run: |
# 使用環境特定的 Dockerfile
if [ -f "Dockerfile.${{ needs.determine-environment.outputs.environment }}" ]; then
DOCKERFILE="Dockerfile.${{ needs.determine-environment.outputs.environment }}"
else
DOCKERFILE="Dockerfile"
fi
docker build \
--build-arg ENVIRONMENT=${{ needs.determine-environment.outputs.environment }} \
--build-arg LOG_LEVEL=${{ vars.LOG_LEVEL }} \
-f $DOCKERFILE \
-t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Update ECS service
env:
CLUSTER_NAME: trading-${{ needs.determine-environment.outputs.environment }}
SERVICE_NAME: trading-bot-service
TASK_DEFINITION_FAMILY: trading-bot-${{ needs.determine-environment.outputs.environment }}
run: |
# 下載當前 task definition
aws ecs describe-task-definition \
--task-definition $TASK_DEFINITION_FAMILY \
--query taskDefinition > task-def.json
# 更新映像檔 URI
jq '.containerDefinitions[0].image = "${{ steps.build-image.outputs.image }}"' task-def.json > new-task-def.json
# 註冊新的 task definition
aws ecs register-task-definition \
--cli-input-json file://new-task-def.json \
--query 'taskDefinition.taskDefinitionArn' \
--output text > task-def-arn.txt
# 更新服務
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--task-definition $(cat task-def-arn.txt)
# 等待部署完成
aws ecs wait services-stable \
--cluster $CLUSTER_NAME \
--services $SERVICE_NAME
- name: Verify deployment
run: |
# 健康檢查
for i in {1..10}; do
if curl -f "${{ vars.API_BASE_URL }}/health"; then
echo "✅ ${{ needs.determine-environment.outputs.environment }} 環境部署成功"
exit 0
fi
echo "等待服務啟動... ($i/10)"
sleep 30
done
echo "❌ ${{ needs.determine-environment.outputs.environment }} 環境部署驗證失敗"
exit 1
- name: Notify deployment
if: always()
run: |
if [[ "${{ job.status }}" == "success" ]]; then
STATUS_EMOJI="✅"
STATUS_TEXT="成功"
else
STATUS_EMOJI="❌"
STATUS_TEXT="失敗"
fi
# 只有生產環境才發送 Telegram 通知
if [[ "${{ needs.determine-environment.outputs.environment }}" == "production" ]]; then
curl -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-H "Content-Type: application/json" \
-d "{
\"chat_id\": \"${{ secrets.TELEGRAM_CHAT_ID }}\",
\"text\": \"$STATUS_EMOJI 部署到 ${{ needs.determine-environment.outputs.environment }} 環境 $STATUS_TEXT\\n\\n📦 版本: ${{ github.sha }}\\n🌐 環境: ${{ vars.API_BASE_URL }}\\n🕒 時間: $(date)\"
}"
fi
# src/config/environment.py
import os
from typing import Dict, Any
from dataclasses import dataclass
@dataclass
class EnvironmentConfig:
"""環境特定配置"""
# 基本設定
environment: str
debug: bool
log_level: str
# API 設定
api_base_url: str
api_timeout: int
# 交易設定
bybit_api_key: str
bybit_secret_key: str
bybit_testnet: bool
# 資料庫設定
database_url: str
redis_url: str
# AWS 設定
aws_region: str
s3_bucket: str
# 監控設定
enable_metrics: bool
metrics_port: int
@classmethod
def from_environment(cls) -> 'EnvironmentConfig':
"""從環境變數載入配置"""
environment = os.getenv('ENVIRONMENT', 'development')
return cls(
environment=environment,
debug=environment != 'production',
log_level=os.getenv('LOG_LEVEL', 'INFO'),
api_base_url=os.getenv('API_BASE_URL', 'http://localhost:8080'),
api_timeout=int(os.getenv('API_TIMEOUT', '30')),
bybit_api_key=os.getenv('BYBIT_API_KEY', ''),
bybit_secret_key=os.getenv('BYBIT_SECRET_KEY', ''),
bybit_testnet=environment != 'production',
database_url=os.getenv('DATABASE_URL', 'sqlite:///trading.db'),
redis_url=os.getenv('REDIS_URL', 'redis://localhost:6379'),
aws_region=os.getenv('AWS_REGION', 'us-east-1'),
s3_bucket=os.getenv('S3_BUCKET', f'trading-{environment}'),
enable_metrics=environment == 'production',
metrics_port=int(os.getenv('METRICS_PORT', '9090')),
)
def get_trading_config(self) -> Dict[str, Any]:
"""獲取交易相關配置"""
return {
'api_key': self.bybit_api_key,
'secret_key': self.bybit_secret_key,
'testnet': self.bybit_testnet,
'timeout': self.api_timeout,
}
# Dockerfile.development
FROM python:3.9-slim
WORKDIR /app
# 開發環境:安裝開發工具
RUN pip install --no-cache-dir pytest black flake8 mypy
COPY requirements.txt requirements-dev.txt ./
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY . .
# 開發環境:啟用調試模式
ENV ENVIRONMENT=development
ENV LOG_LEVEL=DEBUG
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
EXPOSE 8080
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]
# Dockerfile.production
FROM python:3.9-slim
WORKDIR /app
# 生產環境:最小化映像大小
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt && \
pip cache purge
COPY src/ ./src/
# 生產環境:安全性設定
RUN adduser --disabled-password --gecos '' --shell /bin/bash appuser && \
chown -R appuser:appuser /app
USER appuser
ENV ENVIRONMENT=production
ENV LOG_LEVEL=WARNING
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "4"]
# .github/workflows/database-migration.yml
name: Database Migration
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- development
- staging
- production
migration_type:
description: 'Migration type'
required: true
type: choice
options:
- schema-only
- data-only
- full-sync
jobs:
migrate:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install alembic psycopg2-binary
- name: Run database migration
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
MIGRATION_TYPE: ${{ github.event.inputs.migration_type }}
run: |
case $MIGRATION_TYPE in
"schema-only")
alembic upgrade head
;;
"data-only")
python scripts/migrate_data.py
;;
"full-sync")
alembic upgrade head
python scripts/migrate_data.py
;;
esac
- name: Verify migration
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
python scripts/verify_database.py
# monitoring/environments/production.yml
alerting:
rules:
- name: trading-bot-production
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
environment: production
annotations:
summary: "High error rate in production"
- alert: LowSuccessRate
expr: rate(trading_success_total[5m]) < 0.8
for: 10m
labels:
severity: warning
environment: production
annotations:
summary: "Low trading success rate"
# monitoring/environments/development.yml
alerting:
rules:
- name: trading-bot-development
rules:
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: info
environment: development
annotations:
summary: "Service down in development"
今天我們學習了 GitHub Environment 的完整設定,就像為農場建立了不同的管理區域。重要概念包括:
明天我們將學習如何管理 Production 和 Development 環境的差異和切換策略!
下一篇:Day 15 - Prod / Dev 環境管理