Skip to content

CD - Deploy to On-Premise #18

CD - Deploy to On-Premise

CD - Deploy to On-Premise #18

Workflow file for this run

name: CD - Deploy to On-Premise
on:
workflow_dispatch:
inputs:
image_tag:
description: 'Docker image tag to deploy (leave empty to use commit SHA)'
required: false
default: ''
type: string
deploy_targets:
description: 'Services to deploy (all, nest, spring, web, or comma-separated)'
required: false
default: 'all'
type: string
skip_build:
description: 'Skip building Docker images? (use existing images)'
required: false
default: false
type: boolean
operation:
description: 'Deployment operation'
required: false
default: 'app'
type: choice
options:
- app
- monitoring
- all
env:
AWS_REGION: us-east-1
NODE_VERSION: '24.3.0'
JAVA_VERSION: '21'
jobs:
# ============================================
# Build and Push Docker Images
# ============================================
build-images:
name: Build & Push Images
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/production' && !inputs.skip_build && (inputs.operation == 'app' || inputs.operation == 'all') }}
outputs:
image_tag: ${{ steps.set-tag.outputs.image_tag }}
ecr_registry: ${{ steps.login-ecr.outputs.registry }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set image tag
id: set-tag
run: |
TAG="${{ inputs.image_tag }}"
if [ -z "$TAG" ]; then
# Use GitHub SHA (short) as default
TAG="prod-${GITHUB_SHA::8}"
fi
echo "image_tag=${TAG}" >> $GITHUB_OUTPUT
echo "🏷️ Image Tag: ${TAG}"
echo "πŸ“ Full SHA: ${GITHUB_SHA}"
- 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: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push NestJS API image
uses: docker/build-push-action@v5
with:
context: .
file: apps/api/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/api:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/api:prod-latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Spring Boot API image
uses: docker/build-push-action@v5
with:
context: .
file: apps/api-springboot/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/api-springboot:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/api-springboot:prod-latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Web image
uses: docker/build-push-action@v5
with:
context: .
file: apps/web/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/web:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/web:prod-latest
build-args: |
NEXT_PUBLIC_API_URL=${{ secrets.ONPREMISE_API_URL }}
NEXT_PUBLIC_SPRINGBOOT_API_URL=${{ secrets.ONPREMISE_API_URL }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Log Consumer image
uses: docker/build-push-action@v5
with:
context: .
file: apps/log-consumer/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer:prod-latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push AI API image
uses: docker/build-push-action@v5
with:
context: apps/ai-agent
file: apps/ai-agent/ai-api/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/ai-api:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/ai-api:prod-latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push AI Indexer image
uses: docker/build-push-action@v5
with:
context: apps/ai-agent
file: apps/ai-agent/ai-indexer/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer:prod-latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push AI Worker image
uses: docker/build-push-action@v5
with:
context: apps/ai-agent
file: apps/ai-agent/ai-worker/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker:${{ steps.set-tag.outputs.image_tag }}
${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker:prod-latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image build summary
run: |
echo "## 🐳 Docker Images Built" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Service | Tag | Registry |" >> $GITHUB_STEP_SUMMARY
echo "|---------|-----|----------|" >> $GITHUB_STEP_SUMMARY
echo "| NestJS API | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/api |" >> $GITHUB_STEP_SUMMARY
echo "| Spring Boot API | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/api-springboot |" >> $GITHUB_STEP_SUMMARY
echo "| Web | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/web |" >> $GITHUB_STEP_SUMMARY
echo "| Log Consumer | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer |" >> $GITHUB_STEP_SUMMARY
echo "| AI API | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/ai-api |" >> $GITHUB_STEP_SUMMARY
echo "| AI Indexer | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer |" >> $GITHUB_STEP_SUMMARY
echo "| AI Worker | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker |" >> $GITHUB_STEP_SUMMARY
# ============================================
# Deploy to On-Premise
# ============================================
deploy-on-premise:
name: Deploy to On-Premise
runs-on: ubuntu-latest
needs: [build-images]
if: github.ref == 'refs/heads/production' && always() && (needs.build-images.result == 'success' || inputs.skip_build)
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 }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Ansible and dependencies
run: |
pip install ansible boto3 botocore
- name: Install Ansible collections
run: |
ansible-galaxy collection install amazon.aws
- name: Create Ansible vault password file
run: |
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ansible/.vault_pass
chmod 600 ansible/.vault_pass
- name: Create secrets.yml
run: |
echo "${{ secrets.ANSIBLE_SECRETS_YML }}" | base64 -d > ansible/on-premise/environments/production/inventory/group_vars/on_premise_prod/secrets.yml
chmod 600 ansible/on-premise/environments/production/inventory/group_vars/on_premise_prod/secrets.yml
- name: Verify SSM connectivity
run: |
echo "πŸ” Looking up SSM instance..."
INSTANCE_ID=$(aws ssm describe-instance-information \
--filters "Key=tag:Name,Values=flowly-onpremise-prod" \
--query "InstanceInformationList[0].InstanceId" \
--output text)
if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "None" ]; then
echo "❌ Could not find on-premise instance with tag Name=flowly-onpremise-prod"
exit 1
fi
echo "βœ… Found instance: ${INSTANCE_ID}"
echo "SSM_INSTANCE_ID=${INSTANCE_ID}" >> $GITHUB_ENV
- name: Test SSM connectivity
working-directory: ansible/on-premise/environments/production
env:
SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }}
run: |
echo "πŸ”Œ Testing SSM connection..."
ansible on_premise_prod \
-i inventory/hybrid.yml \
-m ping \
--one-line || {
echo "❌ SSM connection failed"
echo "Instance ID: ${SSM_INSTANCE_ID}"
exit 1
}
echo "βœ… SSM connection successful"
- name: Deploy Application
if: ${{ inputs.operation == 'app' || inputs.operation == 'all' }}
working-directory: ansible/on-premise/environments/production
env:
SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }}
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
IMAGE_TAG: ${{ needs.build-images.outputs.image_tag || inputs.image_tag }}
DEPLOY_TARGETS: ${{ inputs.deploy_targets }}
run: |
echo "πŸš€ Deploying application..."
echo "Image Tag: ${IMAGE_TAG}"
echo "Targets: ${DEPLOY_TARGETS}"
ansible-playbook \
-i inventory/hybrid.yml \
../../playbooks/deploy.yml \
-e "target_env=on_premise_prod" \
-e "image_tag=${IMAGE_TAG}" \
-e "deploy_targets=${DEPLOY_TARGETS}" \
--vault-password-file ../../../.vault_pass \
--tags "app,deploy" \
-v
- name: Deploy Monitoring
if: ${{ inputs.operation == 'monitoring' || inputs.operation == 'all' }}
working-directory: ansible/on-premise/environments/production
env:
SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }}
run: |
echo "πŸ“Š Deploying monitoring stack..."
ansible-playbook \
-i inventory/hybrid.yml \
../../playbooks/deploy.yml \
-e "target_env=on_premise_prod" \
-e "deploy_targets=none" \
--vault-password-file ../../../.vault_pass \
--tags "monitoring" \
-v
# Ensure monitoring containers are up and running with latest configs
ansible on_premise_prod \
-i inventory/hybrid.yml \
-m shell \
-a "cd /opt/flowly && docker compose up -d prometheus grafana" \
-v
- name: Verify deployment
working-directory: ansible/on-premise/environments/production
env:
SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }}
run: |
echo "πŸ₯ Checking deployment status..."
# Get container status
ansible on_premise_prod \
-i inventory/hybrid.yml \
-m shell \
-a "cd /opt/flowly && docker compose ps" \
-o
- name: Health check
working-directory: ansible/on-premise/environments/production
env:
SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }}
run: |
echo "🩺 Running health checks via Gateway..."
# Check health endpoints via Gateway (Nginx)
ansible on_premise_prod \
-i inventory/hybrid.yml \
-m shell \
-a "curl -f http://localhost/api/health && curl -f http://localhost/actuator/health" \
--one-line || {
echo "⚠️ Warning: Health checks failed (may still be starting)"
}
- name: Cleanup vault password
if: always()
run: |
rm -f /tmp/.vault_pass
- name: Deployment summary
if: always()
run: |
echo "## πŸš€ On-Premise Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Environment:** on-premise (production)" >> $GITHUB_STEP_SUMMARY
echo "**Operation:** \`${{ inputs.operation }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Image Tag:** \`${{ needs.build-images.outputs.image_tag || inputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Targets:** \`${{ inputs.deploy_targets }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Instance:** \`${{ env.SSM_INSTANCE_ID }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Access" >> $GITHUB_STEP_SUMMARY
echo "- SSH: \`aws ssm start-session --target ${{ env.SSM_INSTANCE_ID }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Logs: \`./deploy.sh logs <service>\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Health Check URLs (via Gateway/SSH)" >> $GITHUB_STEP_SUMMARY
echo "- NestJS API: \`http://localhost/api/health\`" >> $GITHUB_STEP_SUMMARY
echo "- Spring Boot: \`http://localhost/actuator/health\`" >> $GITHUB_STEP_SUMMARY
echo "- Prometheus: \`http://localhost/prometheus/-/healthy\`" >> $GITHUB_STEP_SUMMARY
echo "- Grafana: \`http://localhost/grafana/api/health\`" >> $GITHUB_STEP_SUMMARY