deployment with GitHub Actions, Stunnel for TLS database connections, and dynamic configuration injection.

This commit is contained in:
Haitao Pan 2026-01-20 21:05:30 +08:00
parent 7f6fe07f7f
commit 53f19c379f
4 changed files with 227 additions and 9 deletions

49
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Build and Deploy to Cloud Run
on:
push:
branches: [ "main" ]
env:
PROJECT_ID: your-project-id
REGION: asia-northeast1 # 既然你在日本,建议选东京或大阪
SERVICE_NAME: my-node-app
REPOSITORY: my-repo
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: 'read'
id-token: 'write' # WIF 身份验证必填
steps:
- name: Checkout
uses: actions/checkout@v4
# 1. 身份验证 (使用 Workload Identity Federation)
- name: Google Auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@your-project-id.iam.gserviceaccount.com'
# 2. 配置 Docker 认证
- name: Docker Auth
run: |-
gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet
# 3. 构建并推送镜像
- name: Build and Push Container
run: |-
DOCKER_TAG="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE_NAME }}:${{ github.sha }}"
docker build -t $DOCKER_TAG .
docker push $DOCKER_TAG
# 4. 部署到 Cloud Run
- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v2
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.REGION }}
image: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE_NAME }}:${{ github.sha }}

View File

@ -23,11 +23,14 @@ FROM ubuntu:24.04
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
&& apt-get install -y --no-install-recommends ca-certificates stunnel4 gettext-base \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/run/stunnel \
&& chown -R nobody:nogroup /var/run/stunnel
COPY --from=builder /src/account /usr/local/bin/account
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY config /app/config
RUN chmod +x /usr/local/bin/entrypoint.sh

View File

@ -0,0 +1,43 @@
# Google Cloud Run Configuration Guide
## Required Configuration
Before the `deploy.yml` workflow can run successfully, you must configure the following:
### 1. Update `deploy.yml` Environment Variables
Open the workflow file `.github/workflows/deploy.yml` and update the `env` section:
- `PROJECT_ID`: Set to your Google Cloud Project ID.
- `REGION`: Update if you want to use a region other than `asia-northeast1` (e.g., `asia-northeast1` for Tokyo).
- `SERVICE_NAME`: Confirm the Cloud Run service name.
- `REPOSITORY`: Confirm the Artifact Registry repository name.
### 2. Configure Workload Identity Federation (WIF)
You need to replace the placeholders in the `Google Auth` step or use GitHub Secrets (Recommended).
**Recommended Approach:**
1. Go to your GitHub Repository -> **Settings** -> **Secrets and variables** -> **Actions**.
2. Create a new Repository Secret named `WIF_PROVIDER`.
- Value should be the full provider path, e.g., `projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider`.
3. Create a new Repository Secret named `WIF_SERVICE_ACCOUNT` with your service account email, e.g., `my-service-account@your-project-id.iam.gserviceaccount.com`.
Then update the workflow file (`.github/workflows/deploy.yml`) to use these secrets:
```yaml
# 1. 身份验证 (使用 Workload Identity Federation)
- name: Google Auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
```
### 3. Grant Permissions
Ensure your Service Account has the following IAM roles in Google Cloud:
- `Artifact Registry Writer` (roles/artifactregistry.writer): To push container images.
- `Cloud Run Admin` (roles/run.admin): To deploy new revisions.
- `Service Account User` (roles/iam.serviceAccountUser): To act as the service account during deployment.

View File

@ -1,15 +1,116 @@
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="${CONFIG_PATH:-/etc/xcontrol/account.yaml}"
DEFAULT_CONFIG="/etc/xcontrol/account.yaml"
mkdir -p "$(dirname "${CONFIG_FILE}")"
# -----------------------------------------------------------------------------
# Stunnel Configuration
# -----------------------------------------------------------------------------
if [ ! -f "${CONFIG_FILE}" ]; then
cp "${DEFAULT_CONFIG}" "${CONFIG_FILE}"
if [ -n "${DB_TLS_HOST:-}" ] && [ -n "${DB_TLS_PORT:-}" ]; then
echo "Configuring Stunnel..."
STUNNEL_CONF="/etc/stunnel/stunnel.conf"
mkdir -p /etc/stunnel
# Write certificate files from env vars if present
if [ -n "${DB_CA:-}" ]; then
echo "${DB_CA}" > /etc/stunnel/ca.pem
fi
if [ -n "${DB_CERT:-}" ]; then
echo "${DB_CERT}" > /etc/stunnel/cert.pem
fi
if [ -n "${DB_KEY:-}" ]; then
echo "${DB_KEY}" > /etc/stunnel/key.pem
fi
# Generate stunnel.conf
cat > "${STUNNEL_CONF}" <<EOF
foreground = no
pid = /var/run/stunnel/stunnel.pid
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
debug = 5
output = /var/log/stunnel.log
[postgres]
client = yes
accept = 127.0.0.1:5432
connect = ${DB_TLS_HOST}:${DB_TLS_PORT}
verify = 2
EOF
if [ -f "/etc/stunnel/ca.pem" ]; then
echo "CAfile = /etc/stunnel/ca.pem" >> "${STUNNEL_CONF}"
fi
if [ -f "/etc/stunnel/cert.pem" ] && [ -f "/etc/stunnel/key.pem" ]; then
echo "cert = /etc/stunnel/cert.pem" >> "${STUNNEL_CONF}"
echo "key = /etc/stunnel/key.pem" >> "${STUNNEL_CONF}"
fi
echo "Starting Stunnel..."
stunnel "${STUNNEL_CONF}"
# Wait for stunnel to be up (simple check)
for i in {1..10}; do
if nc -z 127.0.0.1 5432; then
echo "Stunnel is up!"
break
fi
echo "Waiting for Stunnel..."
sleep 1
done
fi
# -----------------------------------------------------------------------------
# App Configuration
# -----------------------------------------------------------------------------
CONFIG_FILE="${CONFIG_PATH:-/etc/xcontrol/account.yaml}"
DEFAULT_CONFIG="/app/config/account.yaml" # Note: Adjusted path as config is likely copied to /app/config in Docker or we assume standard location
# If config not in /etc, maybe we need to copy it there or use what's available
# The original script assumed /etc/xcontrol/account.yaml or copied from DEFAULT_CONFIG
# But where is DEFAULT_CONFIG? In Dockerfile:
# COPY --from=builder /src/account /usr/local/bin/account -> Only binary
# We need to make sure config file exists.
# Looking at Dockerfile again:
# It assumes the app might have config embedded or user mounts it?
# Wait, original entrypoint had: DEFAULT_CONFIG="/etc/xcontrol/account.yaml"
# AND: `cp "${DEFAULT_CONFIG}" "${CONFIG_FILE}"` -- THIS LOGIC IS WEIRD if they are same path.
# Ah, lines 4-5:
# CONFIG_FILE="${CONFIG_PATH:-/etc/xcontrol/account.yaml}"
# DEFAULT_CONFIG="/etc/xcontrol/account.yaml"
# If CONFIG_PATH is set to something else, it copies from DEFAULT. But if DEFAULT is not there?
# The Dockerfile DOES NOT copy config files to /etc/xcontrol.
# Check Dockerfile again:
# It copies entrypoint.sh. It does NOT copy config folder.
# This means the original image probably expected config mounted or baked in separately?
# Or maybe the builder stage had it? No, builder stage copies . . but runtime only copies /src/account binary.
# The user might be mounting config at runtime.
# However, for Cloud Run, we want to bake a default config or generate it.
# Let's assume we need to provide a base config if one isn't there.
# I will blindly respect the original logic but adding my specific injection logic.
# In my plan I said "Inject DB_PASSWORD".
if [ ! -f "${CONFIG_FILE}" ]; then
# If we don't have a config file at all, we might be in trouble if we don't have a source.
# Let's hope the user mounts it or we can generate a minimal one.
# For now, let's assume the previous logic was correct for their env,
# OR we can improve it by creating a default one if missing.
if [ -f "/app/config/account.yaml" ]; then
mkdir -p "$(dirname "${CONFIG_FILE}")"
cp "/app/config/account.yaml" "${CONFIG_FILE}"
elif [ -f "/usr/local/bin/account.yaml" ]; then
mkdir -p "$(dirname "${CONFIG_FILE}")"
cp "/usr/local/bin/account.yaml" "${CONFIG_FILE}"
else
echo "Warning: No configuration file found to copy from."
fi
fi
# Modify Config if needed
if [ -n "${PORT:-}" ]; then
# Use sed for simpler replacement if format allows, but awk is robust
tmp_cfg=$(mktemp)
awk -v port="$PORT" '
/^server:/ {print; in_server=1; addr_written=0; next}
@ -23,7 +124,29 @@ if [ -n "${PORT:-}" ]; then
}
}
' "${CONFIG_FILE}" > "${tmp_cfg}"
CONFIG_FILE="${tmp_cfg}"
mv "${tmp_cfg}" "${CONFIG_FILE}"
fi
exec /usr/local/bin/accountsvc --config "${CONFIG_FILE}" "$@"
# Inject DB Password into DSN if DB_PASSWORD is set
if [ -n "${DB_PASSWORD:-}" ]; then
# Simply replacing 'password' in the DSN if it matches the default pattern,
# or appending if we want to be smarter.
# Default DSN: postgres://shenlan:password@127.0.0.1:5432/account?sslmode=disable
# We will try to replace ":password@" with ":${DB_PASSWORD}@"
# This is a bit brittle but simple.
sed -i "s|:password@|:${DB_PASSWORD}@|g" "${CONFIG_FILE}"
fi
# Inject Auth Secrets
if [ -n "${AUTH_PUBLIC_TOKEN:-}" ]; then
sed -i "s|publicToken: \".*\"|publicToken: \"${AUTH_PUBLIC_TOKEN}\"|g" "${CONFIG_FILE}"
fi
if [ -n "${AUTH_REFRESH_SECRET:-}" ]; then
sed -i "s|refreshSecret: \".*\"|refreshSecret: \"${AUTH_REFRESH_SECRET}\"|g" "${CONFIG_FILE}"
fi
if [ -n "${AUTH_ACCESS_SECRET:-}" ]; then
sed -i "s|accessSecret: \".*\"|accessSecret: \"${AUTH_ACCESS_SECRET}\"|g" "${CONFIG_FILE}"
fi
exec /usr/local/bin/account --config "${CONFIG_FILE}" "$@"