diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..ff74c9c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 }} diff --git a/Dockerfile b/Dockerfile index adc62f6..98d4a6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docs/google-cloud-run-howto.md b/docs/google-cloud-run-howto.md new file mode 100644 index 0000000..a25dbc4 --- /dev/null +++ b/docs/google-cloud-run-howto.md @@ -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. diff --git a/entrypoint.sh b/entrypoint.sh index 774fe10..203d05f 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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}" <> "${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}" "$@" +