Skip to content

Support account lockout on repeated incorrect password entries #14

Support account lockout on repeated incorrect password entries

Support account lockout on repeated incorrect password entries #14

name: CodeDeploy - Production
on:
push:
branches:
- master
paths:
- 'gefapi/**'
- 'migrations/**'
- 'config/**'
- 'scripts/codedeploy/**'
- 'scripts/deployment/**'
- 'docker-compose.yml'
- 'docker-compose.prod.yml'
- 'Dockerfile'
- 'entrypoint.sh'
- 'appspec.yml'
- 'gunicorn.py'
- 'main.py'
- 'pyproject.toml'
- 'poetry.lock'
- 'run_db_migrations.py'
- '.github/workflows/codedeploy_production.yml'
workflow_dispatch:
inputs:
force_deploy:
description: 'Force deployment even if no changes detected'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'
env:
AWS_REGION: us-east-1
APPLICATION_NAME: trendsearth-api
DEPLOYMENT_GROUP: trendsearth-api-production
ECR_REPOSITORY: trendsearth-api
concurrency:
group: production-deployment
cancel-in-progress: false
# Required for OIDC authentication with AWS
permissions:
id-token: write
contents: read
jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
timeout-minutes: 30
environment: production
outputs:
image-uri: ${{ steps.build-push.outputs.image-uri }}
image-tag: ${{ steps.deployment-info.outputs.image-tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get deployment info
id: deployment-info
run: |
echo "commit-sha=${{ github.sha }}" >> $GITHUB_OUTPUT
echo "short-sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
echo "timestamp=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_OUTPUT
echo "image-tag=production-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
role-session-name: GitHubActionsProductionBuild
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Ensure ECR repository exists
id: ecr-repo
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
REPO="${{ env.ECR_REPOSITORY }}"
echo "repo-name=$REPO" >> $GITHUB_OUTPUT
if ! aws ecr describe-repositories --repository-names "$REPO" 2>/dev/null; then
echo "Creating ECR repository: $REPO"
aws ecr create-repository \
--repository-name "$REPO" \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability MUTABLE
else
echo "ECR repository exists: $REPO"
fi
- name: Build and push Docker image to ECR
id: build-push
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ steps.deployment-info.outputs.image-tag }}
run: |
IMAGE_URI="$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG"
docker buildx build \
--platform linux/amd64 \
--cache-from=type=gha,scope=trendsearth-api-production \
--cache-to=type=gha,mode=max,scope=trendsearth-api-production \
--tag "$IMAGE_URI" \
--tag "$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:production-latest" \
--tag "$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:latest" \
--push \
.
echo "image-uri=$IMAGE_URI" >> $GITHUB_OUTPUT
echo "✅ Docker image pushed to ECR: $IMAGE_URI"
deploy:
name: Deploy to Production via CodeDeploy
runs-on: ubuntu-latest
timeout-minutes: 20
environment: production
needs: build-and-push
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
role-session-name: GitHubActionsProductionDeploy
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Get deployment info
id: deployment-info
run: |
echo "commit-sha=${{ github.sha }}" >> $GITHUB_OUTPUT
echo "short-sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
echo "timestamp=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_OUTPUT
# Get AWS account ID for S3 bucket name
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "s3-bucket=trendsearth-api-deployments-${ACCOUNT_ID}" >> $GITHUB_OUTPUT
- name: Get image references
id: images
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
echo "api-image=${{ needs.build-and-push.outputs.image-uri }}" >> $GITHUB_OUTPUT
echo "ecr-registry=$ECR_REGISTRY" >> $GITHUB_OUTPUT
echo "✅ API image: ${{ needs.build-and-push.outputs.image-uri }}"
- name: Create environment file for deployment
env:
ECR_REGISTRY: ${{ steps.images.outputs.ecr-registry }}
API_IMAGE: ${{ steps.images.outputs.api-image }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}
# GEE Configuration
EE_SERVICE_ACCOUNT_JSON: ${{ secrets.EE_SERVICE_ACCOUNT_JSON }}
GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID }}
GEE_ENDPOINT: ${{ vars.GEE_ENDPOINT || 'https://earthengine-highvolume.googleapis.com' }}
GOOGLE_OAUTH_CLIENT_ID: ${{ vars.GOOGLE_OAUTH_CLIENT_ID }}
GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
GOOGLE_OAUTH_REDIRECT_URI: ${{ vars.PRODUCTION_GOOGLE_OAUTH_REDIRECT_URI }}
# Rollbar
ROLLBAR_SCRIPT_TOKEN: ${{ secrets.ROLLBAR_SCRIPT_TOKEN }}
ROLLBAR_SERVER_TOKEN: ${{ secrets.ROLLBAR_SERVER_TOKEN }}
API_PUBLIC_URL: ${{ vars.PRODUCTION_API_PUBLIC_URL }}
# S3 Configuration
SCRIPTS_S3_BUCKET: ${{ vars.SCRIPTS_S3_BUCKET }}
SCRIPTS_S3_PREFIX: ${{ vars.PRODUCTION_SCRIPTS_S3_PREFIX || 'api-files/scripts/production' }}
PARAMS_S3_BUCKET: ${{ vars.PARAMS_S3_BUCKET }}
PARAMS_S3_PREFIX: ${{ vars.PRODUCTION_PARAMS_S3_PREFIX || 'api-files/params/production' }}
# Docker Configuration
REGISTRY_URL: ${{ vars.REGISTRY_URL }}
DOCKER_SUBNET: ${{ vars.DOCKER_SUBNET || '10.10.0.0/16' }}
EXECUTION_SUBNET: ${{ vars.EXECUTION_SUBNET || '10.11.0.0/24' }}
# Email
SPARKPOST_API_KEY: ${{ secrets.SPARKPOST_API_KEY }}
API_ENVIRONMENT_USER: ${{ secrets.API_ENVIRONMENT_USER }}
API_ENVIRONMENT_USER_PASSWORD: ${{ secrets.API_ENVIRONMENT_USER_PASSWORD }}
CORS_ORIGINS: ${{ vars.PRODUCTION_CORS_ORIGINS || 'https://trends.earth,https://api.trends.earth' }}
# Rate Limiting
RATE_LIMITING_ENABLED: ${{ vars.RATE_LIMITING_ENABLED || 'true' }}
RATE_LIMIT_DEFAULT_LIMITS: ${{ vars.RATE_LIMIT_DEFAULT_LIMITS || '1000 per hour,100 per minute' }}
RATE_LIMIT_API_LIMITS: ${{ vars.RATE_LIMIT_API_LIMITS || '300 per hour,20 per minute' }}
RATE_LIMIT_AUTH_LIMITS: ${{ vars.RATE_LIMIT_AUTH_LIMITS || '60 per minute,600 per hour' }}
RATE_LIMIT_PASSWORD_RESET_LIMITS: ${{ vars.RATE_LIMIT_PASSWORD_RESET_LIMITS || '3 per hour,1 per minute' }}
RATE_LIMIT_USER_CREATION_LIMITS: ${{ vars.RATE_LIMIT_USER_CREATION_LIMITS || '100 per hour' }}
RATE_LIMIT_EXECUTION_RUN_LIMITS: ${{ vars.RATE_LIMIT_EXECUTION_RUN_LIMITS || '10 per minute,40 per hour,150 per day' }}
RATE_LIMIT_TRUSTED_PROXY_COUNT: ${{ vars.RATE_LIMIT_TRUSTED_PROXY_COUNT || '1' }}
RATE_LIMIT_INTERNAL_NETWORKS: ${{ vars.RATE_LIMIT_INTERNAL_NETWORKS || '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' }}
run: |
# Generate prod.env with secrets from GitHub
# This file is included in the deployment package and copied to the server
# Values are NOT quoted - Docker env_file reads quotes as literal characters
# The CodeDeploy scripts use safe_source_env() to handle special chars in values
{
echo "# Generated by GitHub Actions"
echo "# Commit: ${{ github.sha }}"
echo ""
echo "# ECR Images (pre-built in CI)"
echo "ECR_REGISTRY=$ECR_REGISTRY"
echo "API_IMAGE=$API_IMAGE"
echo ""
echo "# Environment"
echo "ENVIRONMENT=production"
echo "DEBUG=False"
echo "TESTING=false"
echo ""
echo "# Flask/API Configuration"
echo "SECRET_KEY=$SECRET_KEY"
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY"
echo "API_ENVIRONMENT_USER=$API_ENVIRONMENT_USER"
echo "API_ENVIRONMENT_USER_PASSWORD=$API_ENVIRONMENT_USER_PASSWORD"
echo ""
echo "# Database Configuration"
echo "DATABASE_URL=$DATABASE_URL"
echo ""
echo "# Redis Configuration (uses stack Redis service)"
echo "REDIS_URL=redis://redis:6379/0"
echo ""
echo "# Rate Limiting"
echo "RATE_LIMITING_ENABLED=$RATE_LIMITING_ENABLED"
echo "RATE_LIMIT_STORAGE_URI=redis://redis:6379/1"
echo "DEFAULT_LIMITS=$RATE_LIMIT_DEFAULT_LIMITS"
echo "API_LIMITS=$RATE_LIMIT_API_LIMITS"
echo "AUTH_LIMITS=$RATE_LIMIT_AUTH_LIMITS"
echo "PASSWORD_RESET_LIMITS=$RATE_LIMIT_PASSWORD_RESET_LIMITS"
echo "USER_CREATION_LIMITS=$RATE_LIMIT_USER_CREATION_LIMITS"
echo "EXECUTION_RUN_LIMITS=$RATE_LIMIT_EXECUTION_RUN_LIMITS"
echo "TRUSTED_PROXY_COUNT=$RATE_LIMIT_TRUSTED_PROXY_COUNT"
echo "INTERNAL_NETWORKS=$RATE_LIMIT_INTERNAL_NETWORKS"
echo ""
echo "# Google Earth Engine"
echo "EE_SERVICE_ACCOUNT_JSON=$EE_SERVICE_ACCOUNT_JSON"
echo "GOOGLE_PROJECT_ID=$GOOGLE_PROJECT_ID"
echo "GEE_ENDPOINT=$GEE_ENDPOINT"
echo "GOOGLE_OAUTH_CLIENT_ID=$GOOGLE_OAUTH_CLIENT_ID"
echo "GOOGLE_OAUTH_CLIENT_SECRET=$GOOGLE_OAUTH_CLIENT_SECRET"
echo "GOOGLE_OAUTH_REDIRECT_URI=$GOOGLE_OAUTH_REDIRECT_URI"
echo ""
echo "# Rollbar Error Tracking"
echo "ROLLBAR_SCRIPT_TOKEN=$ROLLBAR_SCRIPT_TOKEN"
echo "ROLLBAR_SERVER_TOKEN=$ROLLBAR_SERVER_TOKEN"
echo ""
echo "# API URLs"
echo "API_PUBLIC_URL=$API_PUBLIC_URL"
echo "API_INTERNAL_URL=http://api:3000"
echo ""
echo "# S3 Configuration (uses EC2 instance role for credentials)"
echo "SCRIPTS_S3_BUCKET=$SCRIPTS_S3_BUCKET"
echo "SCRIPTS_S3_PREFIX=$SCRIPTS_S3_PREFIX"
echo "PARAMS_S3_BUCKET=$PARAMS_S3_BUCKET"
echo "PARAMS_S3_PREFIX=$PARAMS_S3_PREFIX"
echo ""
echo "# Docker Configuration"
echo "REGISTRY_URL=$REGISTRY_URL"
echo "DOCKER_SUBNET=$DOCKER_SUBNET"
echo "EXECUTION_SUBNET=$EXECUTION_SUBNET"
echo ""
echo "# Email Configuration (SparkPost)"
echo "SPARKPOST_API_KEY=$SPARKPOST_API_KEY"
echo ""
echo "# CORS Configuration"
echo "CORS_ORIGINS=$CORS_ORIGINS"
echo ""
echo "# Deployment info"
echo "GIT_REVISION=${{ github.sha }}"
echo "GIT_BRANCH=${{ github.ref_name }}"
echo "DEPLOYMENT_ENVIRONMENT=production"
} > prod.env
echo "✅ Created prod.env with $(wc -l < prod.env) lines"
- name: Update appspec.yml for production
run: |
# Modify appspec.yml to use production-specific destination
# This prevents race conditions when staging and production deploy simultaneously
sed -i 's|destination: /opt/trendsearth-api|destination: /opt/trendsearth-api-production|g' appspec.yml
echo "Updated appspec.yml destination to /opt/trendsearth-api-production"
cat appspec.yml
- name: Create deployment package
id: package
run: |
# Create a deployment archive
REVISION_FILE="production-${{ steps.deployment-info.outputs.timestamp }}-${{ steps.deployment-info.outputs.short-sha }}.zip"
echo "revision-file=$REVISION_FILE" >> $GITHUB_OUTPUT
# Include only files needed for deployment (not source code - that's in the Docker image)
zip -r "$REVISION_FILE" \
appspec.yml \
prod.env \
docker-compose.yml \
docker-compose.prod.yml \
scripts/codedeploy/ \
config/db/ \
entrypoint.sh \
-x "*.pyc" \
-x "__pycache__/*" \
-x ".git/*" \
-x ".venv/*" \
-x "*.log"
echo "Created deployment package: $REVISION_FILE ($(du -h $REVISION_FILE | cut -f1))"
- name: Upload deployment package to S3
id: upload
run: |
BUCKET="${{ steps.deployment-info.outputs.s3-bucket }}"
KEY="production/${{ steps.package.outputs.revision-file }}"
aws s3 cp "${{ steps.package.outputs.revision-file }}" "s3://$BUCKET/$KEY"
echo "s3-key=$KEY" >> $GITHUB_OUTPUT
echo "Uploaded to s3://$BUCKET/$KEY"
- name: Stop any in-progress deployments
run: |
echo "🔍 Checking for in-progress deployments..."
# Get list of active deployments
ACTIVE_DEPLOYMENTS=$(aws deploy list-deployments \
--application-name ${{ env.APPLICATION_NAME }} \
--deployment-group-name ${{ env.DEPLOYMENT_GROUP }} \
--include-only-statuses "InProgress" "Queued" "Created" \
--query 'deployments' \
--output text 2>/dev/null || echo "")
if [ -n "$ACTIVE_DEPLOYMENTS" ] && [ "$ACTIVE_DEPLOYMENTS" != "None" ]; then
for DEPLOYMENT_ID in $ACTIVE_DEPLOYMENTS; do
echo "⏹️ Stopping deployment: $DEPLOYMENT_ID"
aws deploy stop-deployment \
--deployment-id "$DEPLOYMENT_ID" \
--auto-rollback-enabled 2>/dev/null || true
done
# Wait for deployments to stop
echo "⏳ Waiting for deployments to stop..."
sleep 15
echo "✅ Previous deployments stopped"
else
echo "✅ No active deployments found"
fi
- name: Create CodeDeploy deployment
id: deploy
run: |
BUCKET="${{ steps.deployment-info.outputs.s3-bucket }}"
KEY="${{ steps.upload.outputs.s3-key }}"
DEPLOYMENT_ID=$(aws deploy create-deployment \
--application-name ${{ env.APPLICATION_NAME }} \
--deployment-group-name ${{ env.DEPLOYMENT_GROUP }} \
--s3-location bucket=$BUCKET,key=$KEY,bundleType=zip \
--deployment-config-name CodeDeployDefault.OneAtATime \
--description "Production deployment from ${{ github.ref_name }}" \
--file-exists-behavior OVERWRITE \
--query 'deploymentId' \
--output text)
echo "deployment-id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
echo "🚀 Created deployment: $DEPLOYMENT_ID"
- name: Wait for deployment to complete
run: |
DEPLOYMENT_ID="${{ steps.deploy.outputs.deployment-id }}"
echo "⏳ Waiting for deployment $DEPLOYMENT_ID to complete..."
# Wait for deployment (max 30 minutes)
aws deploy wait deployment-successful \
--deployment-id "$DEPLOYMENT_ID" \
--cli-read-timeout 1800 || {
# Get deployment info if failed
STATUS=$(aws deploy get-deployment --deployment-id "$DEPLOYMENT_ID" \
--query 'deploymentInfo.status' --output text)
echo "❌ Deployment failed with status: $STATUS"
# Get deployment events for debugging
echo "📋 Deployment events:"
aws deploy list-deployment-targets --deployment-id "$DEPLOYMENT_ID" \
--query 'targetIds' --output text | while read TARGET_ID; do
echo "Target: $TARGET_ID"
aws deploy get-deployment-target \
--deployment-id "$DEPLOYMENT_ID" \
--target-id "$TARGET_ID" \
--query 'deploymentTarget.instanceTarget.lifecycleEvents[*].{Event:lifecycleEventName,Status:status,Message:diagnostics.message}' \
--output table
done
exit 1
}
echo "✅ Deployment completed successfully!"
- name: Verify deployment
run: |
echo "🔍 Verifying deployment..."
# Get deployment status
DEPLOYMENT_ID="${{ steps.deploy.outputs.deployment-id }}"
aws deploy get-deployment \
--deployment-id "$DEPLOYMENT_ID" \
--query 'deploymentInfo.{Status:status,CreateTime:createTime,CompleteTime:completeTime}' \
--output table
- name: Deployment notification
if: always()
run: |
if [ "${{ job.status }}" = "success" ]; then
echo "✅ Production deployment successful!"
echo "🌐 Site: https://api.trends.earth"
echo "📦 Commit: ${{ github.sha }}"
echo "🚀 Deployment ID: ${{ steps.deploy.outputs.deployment-id }}"
else
echo "❌ Production deployment failed!"
echo "📦 Commit: ${{ github.sha }}"
echo "🚀 Deployment ID: ${{ steps.deploy.outputs.deployment-id }}"
exit 1
fi
- name: Notify Rollbar of deployment
if: success()
uses: rollbar/github-deploy-action@2.1.2
with:
environment: 'production'
version: ${{ github.sha }}
status: 'succeeded'
local_username: ${{ github.actor }}
env:
ROLLBAR_ACCESS_TOKEN: ${{ secrets.ROLLBAR_SERVER_TOKEN }}
continue-on-error: true
- name: Notify Rollbar of deployment failure
if: failure()
uses: rollbar/github-deploy-action@2.1.2
with:
environment: 'production'
version: ${{ github.sha }}
status: 'failed'
local_username: ${{ github.actor }}
env:
ROLLBAR_ACCESS_TOKEN: ${{ secrets.ROLLBAR_SERVER_TOKEN }}
continue-on-error: true