今天是我們系列的倒數第二天!我們要學習如何使用 GitHub Runner 自動建置 Docker 映像檔,這就像為我們的農產品建立自動化包裝工廠一樣。每當我們的程式碼有更新,系統就會自動打包成可部署的容器映像,就像農產品自動包裝一樣高效!
# 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.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/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 }})\"
}"
# 最佳化建置範例
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"]
# .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/
# .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