iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0

小明的分層農場管理

今天要學習 GitHub Environment 的設定,這就像為我們的農場建立不同的管理區域。還記得爸爸把農場分成試驗田、正式農田和種子保存區嗎?每個區域都有不同的管理規則和存取權限,GitHub Environment 就是這個概念!

什麼是 GitHub Environment?

GitHub Environment 是專案中的邏輯環境劃分:

  • 隔離配置:每個環境有獨立的變數和機密
  • 存取控制:設定誰可以部署到特定環境
  • 保護規則:部署前的審查和等待機制
  • 部署追蹤:記錄每個環境的部署歷史

環境架構設計

三層環境策略

三層環境策略

Environment 配置詳解

Development 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"

Staging Environment

# 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"

Production Environment

# 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: "..."

Environment 在 Workflow 中的使用

環境特定的部署工作流程

# .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

# 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 的完整設定,就像為農場建立了不同的管理區域。重要概念包括:

  1. 環境隔離:開發、測試、生產環境的獨立配置
  2. 存取控制:不同環境的保護規則和審查機制
  3. 動態配置:根據環境自動調整應用程式行為
  4. 部署策略:環境特定的部署流程和驗證
  5. 監控差異:各環境的監控和告警設定

明天我們將學習如何管理 Production 和 Development 環境的差異和切換策略!


下一篇:Day 15 - Prod / Dev 環境管理


上一篇
Day 13: Github Actions CI/CD
下一篇
Day 15: Prod / Dev 環境管理
系列文
小資族的量化交易 10115
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言