Skip to content

thoughtparametersllc/flaskapp-deploy-action

Repository files navigation

Flask App Deploy to Google App Engine

A comprehensive GitHub Action to deploy Flask applications to Google App Engine with full support for:

  • Google App Engine deployment with automatic scaling
  • Firestore database setup and migrations
  • Cloudflare DNS integration with SSL/TLS
  • Traffic splitting for blue-green deployments
  • Custom domains with automatic HTTPS
  • Version management for seamless upgrades
  • VPC connectivity for private resources
  • Environment variables and secrets management

🚀 Quick Start

New to this action? Check out the Quick Start Guide to get deployed in 15 minutes!

Features

🚀 App Engine Deployment

  • Automatic deployment to Google App Engine
  • Support for multiple services and versions
  • Configurable scaling (automatic, basic, or manual)
  • Multiple Python runtime versions (3.11, 3.12)
  • Instance class selection (F1, F2, F4, etc.)
  • Custom entrypoint support (Gunicorn pre-configured)

🗄️ Database Management

  • Firestore database initialization
  • Database migration support
  • Firestore index deployment
  • Sample data seeding
  • Both Native and Datastore modes

🔐 Secret Management

  • Google Secret Manager integration
  • Secure storage for API keys, passwords, and sensitive configuration
  • Automatic secret access from Flask applications
  • IAM-based access control
  • Secret versioning and rotation support

🌐 DNS & SSL

  • Cloudflare DNS integration
  • Automatic SSL/TLS configuration
  • Custom domain mapping
  • HTTPS enforcement (all traffic)
  • Multiple SSL modes (flexible, full, strict)

📊 Traffic Management

  • Version-based deployments
  • Traffic splitting for gradual rollouts
  • Blue-green deployment support
  • Zero-downtime deployments
  • Version cleanup options

🔒 Security & Networking

  • VPC connector support
  • Cloud Armor integration (optional)
  • Service account authentication
  • Secure-only traffic (HTTPS)
  • Environment variable encryption

Quick Start

Basic Deployment

name: Deploy to App Engine

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    
    steps:
      - uses: thoughtparametersllc/flaskapp-deploy-action@v1
        with:
          project_id: my-gcp-project
          service_account_key: ${{ secrets.GCP_SA_KEY }}

Advanced Deployment with All Features

name: Deploy with Firestore and Custom Domain

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    
    steps:
      - uses: thoughtparametersllc/flaskapp-deploy-action@v1
        with:
          # GCP Configuration
          project_id: my-gcp-project
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          region: us-central
          
          # App Engine Configuration
          service_name: api
          runtime: python311
          instance_class: F2
          scaling_type: automatic
          max_instances: 20
          min_instances: 2
          
          # Firestore Configuration
          enable_firestore: true
          run_migrations: true
          migration_script: migrate.py
          
          # Cloudflare & Custom Domain
          cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
          custom_domain: api.example.com
          enable_ssl: true
          ssl_mode: full
          
          # Environment Variables
          env_variables: '{"FLASK_ENV":"production"}'

Inputs

GCP Configuration

Input Description Required Default
project_id Google Cloud Project ID Yes -
service_account_key GCP Service Account JSON key Yes -
region GCP region for App Engine No us-central

App Engine Configuration

Input Description Required Default
service_name App Engine service name No default
runtime Python runtime (python311, python312) No python311
instance_class Instance class (F1, F2, F4, etc.) No F1
scaling_type Scaling type (automatic, basic, manual) No automatic
max_instances Maximum number of instances No 10
min_instances Minimum number of instances No 1

Application Configuration

Input Description Required Default
app_yaml_path Path to custom app.yaml No (generated)
requirements_file Path to requirements.txt No requirements.txt
entrypoint Custom entrypoint command No (gunicorn)

Firestore Configuration

Input Description Required Default
enable_firestore Enable Firestore setup No true
firestore_mode Firestore mode (native or datastore) No native
firestore_location Firestore location No us-central
run_migrations Run migration script No false
migration_script Path to migration script No migrate.py

Traffic Management

Input Description Required Default
promote_version Promote version to receive all traffic No true
traffic_split Traffic split config (e.g., "v1=0.9,v2=0.1") No -
stop_previous_version Stop previous version after deploy No false

Cloudflare Configuration

Input Description Required Default
cloudflare_api_token Cloudflare API token No -
cloudflare_zone_id Cloudflare Zone ID No -
custom_domain Custom domain to map No -
enable_ssl Enable SSL/TLS configuration No true
ssl_mode SSL mode (flexible, full, strict) No full

Additional Options

Input Description Required Default
env_variables Environment vars in JSON format No {}
vpc_connector VPC connector name No -
enable_cloud_armor Enable Cloud Armor No false

Outputs

Output Description
service_url URL of the deployed service
version_id Deployed version ID

Prerequisites

1. Google Cloud Project Setup

  1. Create a GCP project or use an existing one

  2. Enable required APIs:

    gcloud services enable appengine.googleapis.com
    gcloud services enable cloudbuild.googleapis.com
    gcloud services enable firestore.googleapis.com
    gcloud services enable secretmanager.googleapis.com
  3. Initialize App Engine (required once per project):

    # Initialize App Engine in your preferred region
    gcloud app create --region=us-central
    
    # Available regions: us-central, us-east1, us-west2, europe-west, asia-northeast1, etc.
    # Full list: https://cloud.google.com/appengine/docs/locations
  4. Create a service account with required permissions:

    gcloud iam service-accounts create github-deploy \
      --display-name="GitHub Actions Deploy"
    
    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="serviceAccount:github-deploy@PROJECT_ID.iam.gserviceaccount.com" \
      --role="roles/appengine.appAdmin"
    
    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="serviceAccount:github-deploy@PROJECT_ID.iam.gserviceaccount.com" \
      --role="roles/cloudbuild.builds.editor"
    
    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="serviceAccount:github-deploy@PROJECT_ID.iam.gserviceaccount.com" \
      --role="roles/datastore.owner"
    
    # Grant Secret Manager access (if using secrets)
    # Note: Use secret-level permissions for better security. This grants
    # read-only access at the project level for simplicity.
    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="serviceAccount:github-deploy@PROJECT_ID.iam.gserviceaccount.com" \
      --role="roles/secretmanager.secretAccessor"
  5. Grant App Engine default service account access to secrets:

    # This allows your deployed app to access Secret Manager
    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
      --role="roles/secretmanager.secretAccessor"
  6. Create and download service account key:

    gcloud iam service-accounts keys create key.json \
      --iam-account=github-deploy@PROJECT_ID.iam.gserviceaccount.com
  7. Add the key to GitHub Secrets as GCP_SA_KEY

2. Cloudflare Setup (Optional)

For custom domain with Cloudflare DNS:

  1. Get your Cloudflare API Token:

    • Go to Cloudflare Dashboard → My Profile → API Tokens
    • Create token with Zone.DNS and Zone.SSL and Certificates permissions
  2. Get your Zone ID from the Cloudflare Dashboard

  3. Add to GitHub Secrets:

    • CLOUDFLARE_API_TOKEN
    • CLOUDFLARE_ZONE_ID

Flask Application Structure

Your Flask application should follow this structure:

your-app/
├── main.py                 # Main application file (entry point)
├── requirements.txt        # Python dependencies
├── app.yaml               # App Engine config (optional)
├── migrate.py             # Migration script (optional)
├── firestore.indexes.json # Firestore indexes (optional)
├── .gcloudignore         # Files to exclude from deployment
└── static/               # Static files (optional)

Required Files

main.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

@app.route('/health')
def health():
    return {'status': 'healthy'}

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080)

requirements.txt

Flask>=3.0.0
gunicorn>=21.2.0
google-cloud-firestore>=2.14.0  # If using Firestore

Optional Files

app.yaml (will be generated if not provided)

runtime: python311
service: default
instance_class: F1

automatic_scaling:
  min_instances: 1
  max_instances: 10

entrypoint: gunicorn -b :$PORT main:app

See the complete example for a full working application.

Usage Examples

Basic Deployment

- uses: thoughtparametersllc/flaskapp-deploy-action@v1
  with:
    project_id: my-project
    service_account_key: ${{ secrets.GCP_SA_KEY }}

With Firestore Database

- uses: thoughtparametersllc/flaskapp-deploy-action@v1
  with:
    project_id: my-project
    service_account_key: ${{ secrets.GCP_SA_KEY }}
    enable_firestore: true
    run_migrations: true

With Custom Domain and SSL

- uses: thoughtparametersllc/flaskapp-deploy-action@v1
  with:
    project_id: my-project
    service_account_key: ${{ secrets.GCP_SA_KEY }}
    custom_domain: api.example.com
    cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
    ssl_mode: full

Blue-Green Deployment

# Step 1: Deploy new version without promoting
- uses: thoughtparametersllc/flaskapp-deploy-action@v1
  with:
    project_id: my-project
    service_account_key: ${{ secrets.GCP_SA_KEY }}
    promote_version: false

# Step 2: Gradually shift traffic
- uses: thoughtparametersllc/flaskapp-deploy-action@v1
  with:
    project_id: my-project
    service_account_key: ${{ secrets.GCP_SA_KEY }}
    traffic_split: v1=0.9,v2=0.1

Multi-Service Deployment

strategy:
  matrix:
    service: [api, admin, worker]

steps:
  - uses: thoughtparametersllc/flaskapp-deploy-action@v1
    with:
      project_id: my-project
      service_account_key: ${{ secrets.GCP_SA_KEY }}
      service_name: ${{ matrix.service }}

How It Works

Deployment Process

  1. Authentication: Authenticates with Google Cloud using service account
  2. Validation: Validates all input parameters
  3. App Configuration: Generates or uses provided app.yaml
  4. Database Setup: Initializes Firestore (if enabled)
  5. Migrations: Runs database migrations (if enabled)
  6. Deployment: Deploys to App Engine
  7. Traffic Management: Configures traffic splitting (if specified)
  8. Domain Mapping: Maps custom domain (if provided)
  9. DNS Configuration: Updates Cloudflare DNS (if configured)
  10. SSL Setup: Configures SSL/TLS settings
  11. Verification: Verifies deployment success

Gunicorn Configuration

App Engine automatically uses Gunicorn as the WSGI server. The default configuration is:

gunicorn -b :$PORT main:app

You can customize this with the entrypoint input:

entrypoint: gunicorn -b :$PORT main:app --workers 4 --threads 8 --timeout 60

Note: App Engine handles Gunicorn configuration professionally. The platform:

  • Automatically binds to the correct port via $PORT
  • Manages worker processes based on instance class
  • Handles graceful shutdowns and restarts
  • Provides health checking and monitoring

SSL/TLS with Cloudflare

When using Cloudflare with a custom domain:

  1. Full Mode (Recommended): Encrypts traffic from visitors to Cloudflare and from Cloudflare to App Engine
  2. Strict Mode: Requires valid SSL certificate on App Engine (automatically provided by Google)
  3. Flexible Mode: Only encrypts traffic from visitors to Cloudflare (not recommended)

The action automatically:

  • Creates CNAME record pointing to ghs.googlehosted.com
  • Enables proxy (orange cloud) for CDN and DDoS protection
  • Sets SSL/TLS mode
  • Enables "Always Use HTTPS"
  • Enables "Automatic HTTPS Rewrites"

Traffic Splitting for Seamless Upgrades

App Engine supports running multiple versions simultaneously:

# Deploy new version without sending traffic
promote_version: false

# Gradually shift traffic
traffic_split: v1=0.9,v2=0.1  # 90% to v1, 10% to v2

This enables:

  • Canary deployments
  • A/B testing
  • Zero-downtime upgrades
  • Easy rollback

Database Migrations

Create a migrate.py script for database initialization:

from google.cloud import firestore

def run_migrations():
    db = firestore.Client()
    
    # Create collections
    items_ref = db.collection('items')
    
    # Add initial data
    items_ref.add({
        'name': 'Sample Item',
        'created_at': firestore.SERVER_TIMESTAMP
    })
    
    print("Migrations completed!")

if __name__ == '__main__':
    run_migrations()

Enable migrations in the action:

run_migrations: true
migration_script: migrate.py

Monitoring and Logs

View Logs

# Stream logs
gcloud app logs tail -s SERVICE_NAME

# View recent logs
gcloud app logs read -s SERVICE_NAME --limit=50

View in Console

Health Checks

Implement a /health endpoint in your Flask app:

@app.route('/health')
def health():
    return {'status': 'healthy'}

Security Best Practices

  1. Service Account: Use minimal required permissions
  2. Secrets: Store sensitive data in Secret Manager, not environment variables
  3. HTTPS Only: Always use secure: always in app.yaml handlers
  4. VPC: Use VPC connector for private resource access
  5. Cloud Armor: Enable for DDoS protection and security rules
  6. Input Validation: Validate all user inputs
  7. Dependencies: Keep requirements.txt updated
  8. IAM: Follow principle of least privilege

Using Google Secret Manager

Google Secret Manager provides a secure and convenient method for storing API keys, passwords, certificates, and other sensitive data.

Setup Secrets

# Enable Secret Manager API
gcloud services enable secretmanager.googleapis.com --project=PROJECT_ID

# Create secrets
echo -n "my-database-password" | gcloud secrets create db-password \
  --data-file=- \
  --replication-policy="automatic" \
  --project=PROJECT_ID

echo -n "my-api-key" | gcloud secrets create external-api-key \
  --data-file=- \
  --replication-policy="automatic" \
  --project=PROJECT_ID

# List all secrets
gcloud secrets list --project=PROJECT_ID

# View secret metadata
gcloud secrets describe db-password --project=PROJECT_ID

Grant Access to App Engine

# Grant the App Engine service account access to specific secrets
gcloud secrets add-iam-policy-binding db-password \
  --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor" \
  --project=PROJECT_ID

# Repeat for each secret your app needs to access
gcloud secrets add-iam-policy-binding external-api-key \
  --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor" \
  --project=PROJECT_ID

Access Secrets in Flask Application

Add google-cloud-secret-manager to your requirements.txt:

Flask>=3.0.0
gunicorn>=21.2.0
google-cloud-secret-manager>=2.16.0

Then access secrets in your code:

import os
import requests
from google.cloud import secretmanager

def access_secret(secret_id, version_id="latest"):
    """
    Access a secret from Google Secret Manager
    
    Args:
        secret_id: The ID of the secret to access
        version_id: The version (default: "latest")
    
    Returns:
        The secret value as a string, or None if error occurs
    """
    try:
        client = secretmanager.SecretManagerServiceClient()
        project_id = os.environ.get('GOOGLE_CLOUD_PROJECT') or os.environ.get('GCP_PROJECT')
        
        # Fallback to metadata server if running in GCP
        if not project_id:
            try:
                metadata_server = "http://metadata.google.internal/computeMetadata/v1/"
                response = requests.get(
                    metadata_server + 'project/project-id',
                    headers={'Metadata-Flavor': 'Google'},
                    timeout=5
                )
                response.raise_for_status()
                project_id = response.text
            except requests.exceptions.RequestException:
                return None
        
        name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
        response = client.access_secret_version(request={"name": name})
        return response.payload.data.decode('UTF-8')
    except Exception as e:
        # Use proper logging in production (app.logger.error)
        import logging
        logging.error(f"Error accessing secret: {e}")
        return None

# Use in your Flask app
from flask import Flask
app = Flask(__name__)

# Lazy-load secrets on demand to avoid startup failures and support rotation
DATABASE_PASSWORD = None
API_KEY = None

def get_database_password():
    """
    Lazily retrieve and cache the database password.
    """
    global DATABASE_PASSWORD
    if DATABASE_PASSWORD is None:
        DATABASE_PASSWORD = access_secret('db-password')
    return DATABASE_PASSWORD

def get_api_key():
    """
    Lazily retrieve and cache the external API key.
    """
    global API_KEY
    if API_KEY is None:
        API_KEY = access_secret('external-api-key')
    return API_KEY

@app.route('/api/data')
def get_data():
    api_key = get_api_key()
    if not api_key:
        return {'error': 'API key not configured'}, 500
    
    # Use the API key to make external requests
    headers = {'Authorization': f'Bearer {api_key}'}
    # ... make your API call
    return {'status': 'success'}

Note: See the complete implementation for a production-ready example with full error handling.

Secret Management Best Practices

  1. Never commit secrets to git - Use Secret Manager instead of environment variables or config files
  2. Never log secret values - Be careful not to log or return secrets in API responses
  3. Use specific permissions - Grant access only to the secrets each service needs
  4. Rotate secrets regularly - Update secret versions periodically
  5. Use secret versions - Keep old versions for rollback capability
  6. Audit access - Monitor who accesses secrets using Cloud Logging

Managing Secret Versions

# Add a new version to an existing secret
echo -n "new-password-value" | gcloud secrets versions add db-password \
  --data-file=- \
  --project=PROJECT_ID

# List all versions
gcloud secrets versions list db-password --project=PROJECT_ID

# Access a specific version
gcloud secrets versions access 2 --secret=db-password --project=PROJECT_ID

# Disable a secret version
gcloud secrets versions disable 1 --secret=db-password --project=PROJECT_ID

# Destroy a secret version (irreversible)
gcloud secrets versions destroy 1 --secret=db-password --project=PROJECT_ID

See the example application for a complete working implementation with Secret Manager.

Cost Optimization

Free Tier

App Engine offers a generous free tier:

  • F1/F2 instances: 28 instance hours/day
  • 1 GB egress/day
  • Shared memcache

Cost-Saving Tips

  1. Use F1 instances for development
  2. Set min_instances: 0 for low-traffic apps
  3. Use Firestore Native mode (better free tier)
  4. Enable automatic scaling
  5. Clean up old versions regularly

Estimated Costs

For a typical small application:

  • F1 instance (1 min instance): ~$40/month
  • Firestore: Usually free tier
  • Bandwidth: Usually free tier
  • Total: ~$40-60/month

Troubleshooting

Deployment Fails

# Check service account permissions
gcloud projects get-iam-policy PROJECT_ID

# Verify APIs are enabled
gcloud services list --enabled

Firestore Issues

# Check Firestore status
gcloud firestore databases describe --project=PROJECT_ID

# View Firestore indexes
gcloud firestore indexes list

DNS Not Resolving

  1. Verify domain mapping: gcloud app domain-mappings list
  2. Check Cloudflare DNS records in dashboard
  3. Wait for DNS propagation (up to 48 hours)
  4. Verify SSL/TLS mode is compatible

Service Not Starting

# View recent logs
gcloud app logs tail -s SERVICE_NAME

# Check service status
gcloud app services describe SERVICE_NAME

# List versions
gcloud app versions list --service=SERVICE_NAME

Secret Manager Issues

Error: Permission denied when accessing secrets

# Verify the App Engine service account has access
gcloud secrets get-iam-policy SECRET_NAME --project=PROJECT_ID

# Grant access if missing
gcloud secrets add-iam-policy-binding SECRET_NAME \
  --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor" \
  --project=PROJECT_ID

Error: Secret not found

# List all secrets to verify it exists
gcloud secrets list --project=PROJECT_ID

# Create the secret if it doesn't exist
echo -n "secret-value" | gcloud secrets create SECRET_NAME \
  --data-file=- \
  --project=PROJECT_ID

Error: Unable to access metadata server

  • This error occurs when running locally without proper credentials
  • Set GOOGLE_CLOUD_PROJECT or GCP_PROJECT environment variable
  • Ensure GOOGLE_APPLICATION_CREDENTIALS points to your service account key

Examples

See the examples directory for complete working examples:

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details.

License

This project is licensed under the terms specified in the LICENSE file.

Support

Related Resources

About

A GitHub action to deploy Flask apps and configure them and start/restart the service

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •