iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0

小明的自動化工廠

今天是我們系列的倒數第二天!我們要學習如何使用 GitHub Runner 自動建置 Docker 映像檔,這就像為我們的農產品建立自動化包裝工廠一樣。每當我們的程式碼有更新,系統就會自動打包成可部署的容器映像,就像農產品自動包裝一樣高效!

Docker 映像建置策略

多階段建置架構

多階段建置架構

完整的 Dockerfile

生產級 Dockerfile

# Dockerfile
# Multi-stage build for trading bot

#######################################
# Stage 1: Builder (開發和建置環境)
#######################################
FROM python:3.9-slim as builder

# 設定工作目錄
WORKDIR /app

# 安裝系統依賴和建置工具
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    make \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

# 升級 pip 並安裝建置依賴
RUN pip install --upgrade pip setuptools wheel

# 複製依賴文件
COPY requirements.txt requirements-dev.txt ./

# 安裝 Python 依賴到本地目錄
RUN pip install --user --no-cache-dir -r requirements.txt

# 複製原始碼
COPY . .

# 執行測試和程式碼品質檢查
RUN python -m pytest tests/ --tb=short
RUN python -m flake8 src/
RUN python -m black --check src/

# 編譯和優化(如果需要)
RUN python -m py_compile -q src/**/*.py

#######################################
# Stage 2: Runtime (生產運行環境)
#######################################
FROM python:3.9-slim as runtime

# 建置參數
ARG BUILD_DATE
ARG VERSION
ARG VCS_REF

# 標籤資訊
LABEL maintainer="trading-team@example.com" \
      org.opencontainers.image.title="Trading Bot" \
      org.opencontainers.image.description="Quantitative Trading Bot for Cryptocurrency" \
      org.opencontainers.image.version="${VERSION}" \
      org.opencontainers.image.created="${BUILD_DATE}" \
      org.opencontainers.image.revision="${VCS_REF}" \
      org.opencontainers.image.vendor="Trading Team"

# 建立非 root 使用者
RUN groupadd -r trading && useradd -r -g trading trading

# 安裝運行時系統依賴
RUN apt-get update && apt-get install -y \
    curl \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# 設定工作目錄
WORKDIR /app

# 從 builder 階段複製 Python 依賴
COPY --from=builder /root/.local /home/trading/.local

# 複製應用程式程式碼
COPY --from=builder /app/src ./src
COPY --from=builder /app/config ./config

# 建立必要目錄並設定權限
RUN mkdir -p /app/logs /app/data \
    && chown -R trading:trading /app \
    && chmod +x /app/src/main.py

# 切換到非 root 使用者
USER trading

# 設定環境變數
ENV PATH="/home/trading/.local/bin:${PATH}" \
    PYTHONPATH="/app/src" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    USER=trading

# 健康檢查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# 暴露埠號
EXPOSE 8080

# 預設命令
CMD ["python", "src/main.py"]

開發環境 Dockerfile

# Dockerfile.dev
FROM python:3.9-slim

WORKDIR /app

# 安裝開發工具
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    make \
    curl \
    git \
    vim \
    htop \
    && rm -rf /var/lib/apt/lists/*

# 安裝 Python 依賴
COPY requirements.txt requirements-dev.txt ./
RUN pip install --upgrade pip \
    && pip install -r requirements-dev.txt

# 安裝開發工具
RUN pip install \
    jupyter \
    ipython \
    debugpy

# 複製程式碼
COPY . .

# 設定開發環境
ENV PYTHONPATH="/app/src" \
    FLASK_ENV=development \
    DEBUG=1

# 暴露除錯埠號
EXPOSE 8080 5678 8888

CMD ["python", "src/main.py"]

GitHub Actions 映像建置工作流程

完整的 CI/CD Pipeline

# .github/workflows/build-and-deploy.yml
name: Build and Deploy Trading Bot

on:
  push:
    branches: [ main, develop ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: trading-bot
  ECS_SERVICE: trading-bot-service
  ECS_CLUSTER: trading-cluster

jobs:
  # 程式碼品質檢查
  code-quality:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.9'
        cache: 'pip'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements-dev.txt

    - name: Lint with flake8
      run: |
        flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics
        flake8 src/ tests/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

    - name: Check code formatting
      run: black --check src/ tests/

    - name: Type checking
      run: mypy src/

    - name: Security check
      run: bandit -r src/

  # 單元測試
  test:
    runs-on: ubuntu-latest
    needs: code-quality
    
    services:
      redis:
        image: redis:6.2
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: trading_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.9'
        cache: 'pip'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements-dev.txt

    - name: Run tests
      env:
        DATABASE_URL: postgresql://postgres:testpass@localhost:5432/trading_test
        REDIS_URL: redis://localhost:6379
        BYBIT_API_KEY: test_key
        BYBIT_SECRET_KEY: test_secret
      run: |
        pytest tests/ \
          --cov=src/ \
          --cov-report=xml \
          --cov-report=html \
          --junitxml=test-results.xml \
          -v

    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml
        flags: unittests
        name: codecov-umbrella

  # Docker 映像建置
  build:
    runs-on: ubuntu-latest
    needs: [code-quality, test]
    outputs:
      image: ${{ steps.image.outputs.image }}
      digest: ${{ steps.build.outputs.digest }}

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - 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: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./Dockerfile
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        platforms: linux/amd64,linux/arm64
        build-args: |
          BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
          VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
          VCS_REF=${{ github.sha }}

    - name: Generate SBOM
      uses: anchore/sbom-action@v0
      with:
        image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
        format: spdx-json
        output-file: sbom.spdx.json

    - name: Scan image for vulnerabilities
      uses: anchore/scan-action@v3
      id: scan
      with:
        image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
        fail-build: false
        severity-cutoff: high

    - name: Upload scan results
      uses: github/codeql-action/upload-sarif@v2
      if: always()
      with:
        sarif_file: ${{ steps.scan.outputs.sarif }}

    - name: Image output
      id: image
      run: |
        echo "image=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}" >> $GITHUB_OUTPUT

  # 部署到開發環境
  deploy-dev:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment: development

    steps:
    - 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 development
      run: |
        aws ecs update-service \
          --cluster trading-dev-cluster \
          --service trading-bot-dev-service \
          --force-new-deployment \
          --task-definition trading-bot-dev:LATEST

    - name: Wait for deployment
      run: |
        aws ecs wait services-stable \
          --cluster trading-dev-cluster \
          --services trading-bot-dev-service

  # 部署到生產環境
  deploy-prod:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment: production

    steps:
    - name: Checkout code
      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 }}
        role-to-assume: ${{ secrets.PROD_DEPLOY_ROLE_ARN }}
        role-session-name: GitHubActions-ProdDeploy

    - name: Download current task definition
      run: |
        aws ecs describe-task-definition \
          --task-definition ${{ env.ECS_SERVICE }} \
          --query taskDefinition > task-definition.json

    - name: Update task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: task-definition.json
        container-name: trading-bot
        image: ${{ needs.build.outputs.image }}

    - name: Deploy to production
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def.outputs.task-definition }}
        service: ${{ env.ECS_SERVICE }}
        cluster: ${{ env.ECS_CLUSTER }}
        wait-for-service-stability: true
        wait-for-minutes: 10

    - name: Verify deployment
      run: |
        # 等待服務穩定
        sleep 30
        
        # 獲取 ALB DNS 名稱
        ALB_DNS=$(aws elbv2 describe-load-balancers \
          --names trading-bot-alb \
          --query 'LoadBalancers[0].DNSName' \
          --output text)
        
        # 健康檢查
        for i in {1..10}; do
          if curl -f "https://$ALB_DNS/health"; then
            echo "✅ Production deployment successful"
            exit 0
          fi
          echo "Waiting for service to be ready... ($i/10)"
          sleep 30
        done
        
        echo "❌ Production deployment verification failed"
        exit 1

  # 發送通知
  notify:
    runs-on: ubuntu-latest
    needs: [deploy-dev, deploy-prod]
    if: always()

    steps:
    - name: Notify success
      if: ${{ needs.deploy-prod.result == 'success' || needs.deploy-dev.result == 'success' }}
      run: |
        ENVIRONMENT=${{ github.ref == 'refs/heads/main' && 'production' || 'development' }}
        
        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\": \"🚀 Trading Bot 部署成功!\\n\\n🌍 環境: $ENVIRONMENT\\n📦 版本: ${{ github.sha }}\\n🕒 時間: $(date)\\n🔗 [查看詳情](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\"
             }"

    - name: Notify failure
      if: ${{ needs.deploy-prod.result == 'failure' || needs.deploy-dev.result == 'failure' }}
      run: |
        ENVIRONMENT=${{ github.ref == 'refs/heads/main' && 'production' || 'development' }}
        
        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\": \"❌ Trading Bot 部署失敗!\\n\\n🌍 環境: $ENVIRONMENT\\n📦 版本: ${{ github.sha }}\\n🕒 時間: $(date)\\n🔗 [查看詳情](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\"
             }"

映像最佳化策略

1. 多階段建置最佳化

# 最佳化建置範例
FROM python:3.9-alpine as base

# 設定共用環境變數
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

#######################################
# Dependencies stage
#######################################
FROM base as dependencies

# 安裝建置依賴
RUN apk add --no-cache \
    gcc \
    musl-dev \
    libffi-dev \
    openssl-dev

# 安裝 Python 依賴
COPY requirements.txt .
RUN pip install --user --no-warn-script-location -r requirements.txt

#######################################
# Development stage
#######################################
FROM dependencies as development

# 安裝開發依賴
COPY requirements-dev.txt .
RUN pip install --user --no-warn-script-location -r requirements-dev.txt

# 複製程式碼
COPY . /app
WORKDIR /app

# 開發模式命令
CMD ["python", "src/main.py", "--debug"]

#######################################
# Testing stage
#######################################
FROM development as testing

# 執行測試
RUN python -m pytest tests/ --tb=short
RUN python -m flake8 src/
RUN python -m black --check src/

#######################################
# Production stage
#######################################
FROM base as production

# 僅複製運行時依賴
COPY --from=dependencies /root/.local /root/.local

# 建立應用使用者
RUN adduser --disabled-password --gecos '' --shell /bin/sh --uid 1000 app

# 複製應用程式
COPY src/ /app/src/
COPY config/ /app/config/

# 設定權限
RUN chown -R app:app /app
USER app

WORKDIR /app

# 生產模式命令
CMD ["python", "src/main.py"]

2. 映像大小優化

# .dockerignore
.git
.github
.pytest_cache
.coverage
__pycache__
*.pyc
*.pyo
*.pyd
.Python
build
develop-eggs
dist
downloads
eggs
.eggs
lib
lib64
parts
sdist
var
wheels
*.egg-info/
.installed.cfg
*.egg

# 測試和開發文件
tests/
docs/
*.md
LICENSE

# IDE 文件
.vscode/
.idea/
*.swp
*.swo

# 日誌文件
*.log
logs/

# 暫存文件
.tmp/
temp/

3. 安全掃描集成

# .github/workflows/security-scan.yml
name: Security Scan

on:
  schedule:
    - cron: '0 2 * * *'  # 每日凌晨 2 點執行
  push:
    branches: [ main ]

jobs:
  container-scan:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Build image for scanning
      run: |
        docker build -t trading-bot:scan .

    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'trading-bot:scan'
        format: 'sarif'
        output: 'trivy-results.sarif'

    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

    - name: Run Hadolint
      uses: hadolint/hadolint-action@v3.1.0
      with:
        dockerfile: Dockerfile
        format: sarif
        output-file: hadolint-results.sarif

    - name: Upload Hadolint scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: hadolint-results.sarif

映像版本管理

語義化版本標籤

# 標籤策略
tags: |
  # 分支標籤
  type=ref,event=branch
  # PR 標籤
  type=ref,event=pr
  # 語義化版本
  type=semver,pattern={{version}}
  type=semver,pattern={{major}}.{{minor}}
  type=semver,pattern={{major}}
  # SHA 標籤
  type=sha,prefix={{branch}}-
  # 最新標籤
  type=raw,value=latest,enable={{is_default_branch}}
  # 日期標籤
  type=raw,value={{date 'YYYYMMDD'}}

映像清理策略

# ECR 生命週期政策
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Keep last 10 production images",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["main-"],
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": {
        "type": "expire"
      }
    },
    {
      "rulePriority": 2,
      "description": "Delete untagged images older than 1 day",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 1
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}

效能監控

建置時間優化

# 快取策略優化
- name: Build with enhanced caching
  uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: |
      type=gha
      type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
    cache-to: |
      type=gha,mode=max
      type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

建置指標收集

- name: Collect build metrics
  run: |
    echo "BUILD_START_TIME=$(date +%s)" >> $GITHUB_ENV
    
- name: Calculate build duration
  run: |
    BUILD_END_TIME=$(date +%s)
    BUILD_DURATION=$((BUILD_END_TIME - BUILD_START_TIME))
    echo "Build duration: ${BUILD_DURATION} seconds"
    
    # 發送指標到監控系統
    curl -X POST "${{ secrets.METRICS_ENDPOINT }}" \
         -H "Content-Type: application/json" \
         -d "{
           \"metric\": \"build_duration\",
           \"value\": $BUILD_DURATION,
           \"tags\": {
             \"repository\": \"${{ github.repository }}\",
             \"branch\": \"${{ github.ref_name }}\"
           }
         }"

小結

今天我們學習了如何使用 GitHub Runner 建置高品質的 Docker 映像檔,就像建立了一個全自動化的農產品包裝工廠。重要的概念包括:

映像建置最佳實踐:

  • 多階段建置減少映像大小
  • 安全掃描確保映像安全
  • 適當的標籤管理策略
  • 有效的快取策略

CI/CD 集成:

  • 自動化品質檢查
  • 多環境部署策略
  • 完整的測試覆蓋
  • 及時的通知機制

安全和監控:

  • 容器安全掃描
  • 漏洞管理
  • 建置指標監控
  • 映像生命週期管理

效能優化:

  • 建置時間最佳化
  • 映像大小控制
  • 快取策略優化
  • 並行建置支援

明天是我們系列的最後一天,我們將整合 Telegram 通知功能,完成整個量化交易系統的最後一塊拼圖!


下一篇:Day 31 - Telegram Notification


上一篇
Day 29: Containerized Service
下一篇
Day 31: Telegram Notification
系列文
小資族的量化交易 10131
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言