feat(deploy): add dual-mode deploy switch (EC2 monolith + EKS)

- deploy.sh: single script to switch between EC2 and EKS modes
  - ec2: docker-compose with ECR images + nginx SSL reverse proxy
  - eks: terraform apply + helm install (for demos/grading)
  - eks-down: terraform destroy (stop costs)
- docker-compose.prod.yml: ECR image overrides + nginx service
- nginx/nginx.conf: reverse proxy with SSL, SSE streaming support
- deploy-ec2.yml: auto-deploy to EC2 after images are built
- Remove old single-server deploy.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Manmohan Sharma 2026-04-16 12:57:57 -07:00
parent 9095cf01a8
commit b766dcf703
No known key found for this signature in database
5 changed files with 445 additions and 29 deletions

70
.github/workflows/deploy-ec2.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: Deploy to EC2 (Monolith)
on:
workflow_dispatch: # Manual trigger from GitHub UI
workflow_run: # Auto-trigger after images are built
workflows: ["Build & Push Dev Images"]
types: [completed]
branches: [master, main]
concurrency:
group: deploy-ec2
cancel-in-progress: false
jobs:
deploy:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
- name: Get ECR login password
id: ecr
run: |
echo "password=$(aws ecr get-login-password --region ${{ vars.AWS_REGION || 'us-west-2' }})" >> $GITHUB_OUTPUT
echo "registry=${{ secrets.AWS_ACCOUNT_ID || '883107058766' }}.dkr.ecr.${{ vars.AWS_REGION || 'us-west-2' }}.amazonaws.com" >> $GITHUB_OUTPUT
- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -e
cd /home/ubuntu
# Login to ECR
echo "${{ steps.ecr.outputs.password }}" | \
docker login --username AWS --password-stdin ${{ steps.ecr.outputs.registry }}
# Clone or update repo
if [ -d samosachaat ]; then
cd samosachaat
git fetch origin master
git reset --hard origin/master
else
git clone https://github.com/manmohan659/nanochat.git samosachaat
cd samosachaat
fi
# Set image source
export ECR_REGISTRY=${{ steps.ecr.outputs.registry }}
export IMAGE_TAG=dev-latest
# Pull and deploy
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Run migrations (wait for postgres)
sleep 8
docker compose exec -T chat-api alembic upgrade head 2>/dev/null || true
echo "Deploy complete!"
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps

View File

@ -1,29 +0,0 @@
name: Deploy samosaChaat to EC2
on:
push:
branches: [master]
paths:
- 'nanochat/**'
- 'scripts/chat_web.py'
- 'scripts/chat_cli.py'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd /home/ubuntu/nanochat
git fetch origin master
git reset --hard origin/master
sudo systemctl restart samosachaat.service
echo "Deploy complete - samosaChaat restarted"

264
deploy.sh Executable file
View File

@ -0,0 +1,264 @@
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# samosaChaat Deploy Switch
#
# Usage:
# ./deploy.sh ec2 Deploy monolith to EC2 via docker-compose
# ./deploy.sh ec2-down Stop services on EC2
# ./deploy.sh eks Provision EKS + deploy via Helm (demo/grading)
# ./deploy.sh eks-down Tear down EKS (save $$$)
# ./deploy.sh status Show what's currently running
###############################################################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AWS_ACCOUNT="883107058766"
AWS_REGION="us-west-2"
ECR_REGISTRY="${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com"
EC2_HOST="52.10.243.118"
EC2_USER="ubuntu"
EC2_KEY="$HOME/.ssh/samosachaat.pem" # adjust if your key is elsewhere
DOMAIN="samosachaat.art"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[samosaChaat]${NC} $1"; }
warn() { echo -e "${YELLOW}[samosaChaat]${NC} $1"; }
err() { echo -e "${RED}[samosaChaat]${NC} $1" >&2; }
#─── EC2 MONOLITH ─────────────────────────────────────────────────────────────
ec2_deploy() {
log "Deploying to EC2 monolith at ${EC2_HOST}..."
# 1. Login to ECR locally to get credentials
log "Logging into ECR..."
aws ecr get-login-password --region ${AWS_REGION} | \
ssh -i "${EC2_KEY}" -o StrictHostKeyChecking=no ${EC2_USER}@${EC2_HOST} \
"docker login --username AWS --password-stdin ${ECR_REGISTRY}" 2>/dev/null
# 2. Sync repo to EC2
log "Syncing code to EC2..."
ssh -i "${EC2_KEY}" ${EC2_USER}@${EC2_HOST} bash -s << 'REMOTE_SCRIPT'
set -e
cd /home/ubuntu
# Clone or update repo
if [ -d samosachaat ]; then
cd samosachaat
git fetch origin master
git reset --hard origin/master
else
git clone https://github.com/manmohan659/nanochat.git samosachaat
cd samosachaat
fi
# Ensure .env exists
if [ ! -f .env ]; then
cp .env.example .env
echo "⚠️ Created .env from template — edit it with real values!"
fi
REMOTE_SCRIPT
# 3. Copy .env from local if it exists
if [ -f "${SCRIPT_DIR}/.env" ]; then
log "Syncing .env to EC2..."
scp -i "${EC2_KEY}" "${SCRIPT_DIR}/.env" ${EC2_USER}@${EC2_HOST}:/home/ubuntu/samosachaat/.env
fi
# 4. Pull images and start services
log "Pulling images and starting services..."
ssh -i "${EC2_KEY}" ${EC2_USER}@${EC2_HOST} bash -s << REMOTE_DEPLOY
set -e
cd /home/ubuntu/samosachaat
# Set ECR registry in environment
export ECR_REGISTRY=${ECR_REGISTRY}
export IMAGE_TAG=dev-latest
# Pull latest images
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
# Start everything
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Run DB migrations
echo "Running database migrations..."
sleep 5 # wait for postgres to be ready
docker compose exec -T chat-api alembic upgrade head 2>/dev/null || \
echo "Migrations skipped (may need manual .env setup)"
echo ""
docker compose ps
REMOTE_DEPLOY
# 5. Setup SSL if not already done
log "Checking SSL..."
ssh -i "${EC2_KEY}" ${EC2_USER}@${EC2_HOST} bash -s << 'SSL_CHECK'
if [ ! -d /etc/letsencrypt/live/samosachaat.art ]; then
echo "Setting up SSL with certbot..."
sudo apt-get update -qq && sudo apt-get install -y -qq certbot > /dev/null 2>&1
sudo certbot certonly --standalone --non-interactive \
--agree-tos -m manmohan659@gmail.com \
-d samosachaat.art -d www.samosachaat.art \
--pre-hook "docker compose -f /home/ubuntu/samosachaat/docker-compose.yml -f /home/ubuntu/samosachaat/docker-compose.prod.yml stop nginx" \
--post-hook "docker compose -f /home/ubuntu/samosachaat/docker-compose.yml -f /home/ubuntu/samosachaat/docker-compose.prod.yml start nginx"
else
echo "SSL already configured."
fi
SSL_CHECK
echo ""
log "EC2 deploy complete!"
log " App: https://${DOMAIN}"
log " Grafana: https://${DOMAIN}/grafana/"
log " EC2: ${EC2_HOST}"
}
ec2_down() {
log "Stopping services on EC2..."
ssh -i "${EC2_KEY}" ${EC2_USER}@${EC2_HOST} \
"cd /home/ubuntu/samosachaat && docker compose -f docker-compose.yml -f docker-compose.prod.yml down"
log "EC2 services stopped."
}
#─── EKS CLUSTER ──────────────────────────────────────────────────────────────
eks_deploy() {
local ENV="${1:-dev}"
log "Provisioning EKS cluster (${ENV})... This takes ~15-20 minutes."
cd "${SCRIPT_DIR}/terraform/environments/${ENV}"
# Init & apply Terraform
log "Running terraform init..."
terraform init
log "Running terraform apply..."
terraform apply -auto-approve
# Get cluster info
local CLUSTER_NAME=$(terraform output -raw eks_cluster_name 2>/dev/null || echo "samosachaat-${ENV}")
log "Configuring kubectl for ${CLUSTER_NAME}..."
aws eks update-kubeconfig --name "${CLUSTER_NAME}" --region ${AWS_REGION}
# Install ALB Ingress Controller
log "Installing ALB Ingress Controller..."
helm repo add eks https://aws.github.io/eks-charts 2>/dev/null || true
helm repo update
helm upgrade --install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName="${CLUSTER_NAME}" \
--set serviceAccount.create=true \
--set serviceAccount.name=aws-load-balancer-controller \
--wait --timeout 5m 2>/dev/null || warn "ALB controller may need IRSA setup"
# Deploy observability stack
log "Deploying observability stack..."
helm dependency build "${SCRIPT_DIR}/helm/observability" 2>/dev/null || true
helm upgrade --install observability "${SCRIPT_DIR}/helm/observability" \
--namespace monitoring --create-namespace \
--wait --timeout 10m 2>/dev/null || warn "Observability deploy needs review"
# Deploy samosaChaat
local VALUES_FILE="${SCRIPT_DIR}/helm/samosachaat/values-${ENV}.yaml"
log "Deploying samosaChaat to EKS..."
helm upgrade --install samosachaat "${SCRIPT_DIR}/helm/samosachaat" \
-f "${VALUES_FILE}" \
--set global.imageRegistry="${ECR_REGISTRY}" \
--set global.imageTag="dev-latest" \
--set ingress.acmCertArn="$(terraform output -raw acm_certificate_arn 2>/dev/null || echo '')" \
--namespace samosachaat --create-namespace \
--wait --timeout 10m
echo ""
log "EKS deploy complete!"
log " Cluster: ${CLUSTER_NAME}"
kubectl get pods -n samosachaat
echo ""
kubectl get ingress -n samosachaat 2>/dev/null || true
}
eks_down() {
local ENV="${1:-dev}"
warn "Tearing down EKS cluster (${ENV})... This saves ~\$0.10/hr + node costs."
cd "${SCRIPT_DIR}/terraform/environments/${ENV}"
# Remove Helm releases first (cleans up ALB, etc.)
local CLUSTER_NAME=$(terraform output -raw eks_cluster_name 2>/dev/null || echo "samosachaat-${ENV}")
aws eks update-kubeconfig --name "${CLUSTER_NAME}" --region ${AWS_REGION} 2>/dev/null || true
log "Removing Helm releases..."
helm uninstall samosachaat -n samosachaat 2>/dev/null || true
helm uninstall observability -n monitoring 2>/dev/null || true
helm uninstall aws-load-balancer-controller -n kube-system 2>/dev/null || true
# Destroy infrastructure
log "Running terraform destroy..."
terraform destroy -auto-approve
log "EKS cluster destroyed. Costs stopped."
}
#─── STATUS ───────────────────────────────────────────────────────────────────
show_status() {
echo ""
log "=== samosaChaat Deployment Status ==="
echo ""
# Check EC2
echo "EC2 Monolith (${EC2_HOST}):"
if ssh -i "${EC2_KEY}" -o ConnectTimeout=5 ${EC2_USER}@${EC2_HOST} \
"docker compose -f /home/ubuntu/samosachaat/docker-compose.yml -f /home/ubuntu/samosachaat/docker-compose.prod.yml ps --format 'table {{.Name}}\t{{.Status}}'" 2>/dev/null; then
echo ""
else
echo " Not running or unreachable."
fi
# Check EKS
echo "EKS Cluster:"
if kubectl get nodes 2>/dev/null; then
echo ""
kubectl get pods -n samosachaat 2>/dev/null || echo " No samosachaat namespace."
else
echo " No EKS cluster configured."
fi
# Check ECR images
echo ""
echo "ECR Images (latest):"
for svc in frontend auth chat-api inference; do
TAG=$(aws ecr describe-images --repository-name samosachaat/${svc} --region ${AWS_REGION} \
--query 'sort_by(imageDetails,&imagePushedAt)[-1].imageTags[0]' --output text 2>/dev/null || echo "none")
echo " samosachaat/${svc}: ${TAG}"
done
}
#─── MAIN ─────────────────────────────────────────────────────────────────────
case "${1:-help}" in
ec2) ec2_deploy ;;
ec2-down) ec2_down ;;
eks) eks_deploy "${2:-dev}" ;;
eks-down) eks_down "${2:-dev}" ;;
status) show_status ;;
*)
echo "samosaChaat Deploy Switch"
echo ""
echo "Usage: ./deploy.sh <mode>"
echo ""
echo "Modes:"
echo " ec2 Deploy monolith to EC2 (cheap, always-on)"
echo " ec2-down Stop EC2 services"
echo " eks [env] Provision EKS + deploy (demo/grading) [dev|uat|prod]"
echo " eks-down Tear down EKS (save \$\$\$)"
echo " status Show what's running"
;;
esac

36
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,36 @@
## Production override for EC2 monolith deployment.
## Usage: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
##
## This pulls pre-built images from ECR instead of building locally,
## and adds nginx as reverse proxy with SSL.
services:
frontend:
build: !reset null
image: ${ECR_REGISTRY:-883107058766.dkr.ecr.us-west-2.amazonaws.com}/samosachaat/frontend:${IMAGE_TAG:-dev-latest}
auth:
build: !reset null
image: ${ECR_REGISTRY:-883107058766.dkr.ecr.us-west-2.amazonaws.com}/samosachaat/auth:${IMAGE_TAG:-dev-latest}
chat-api:
build: !reset null
image: ${ECR_REGISTRY:-883107058766.dkr.ecr.us-west-2.amazonaws.com}/samosachaat/chat-api:${IMAGE_TAG:-dev-latest}
inference:
build: !reset null
image: ${ECR_REGISTRY:-883107058766.dkr.ecr.us-west-2.amazonaws.com}/samosachaat/inference:${IMAGE_TAG:-dev-latest}
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- frontend
- auth
- chat-api

75
nginx/nginx.conf Normal file
View File

@ -0,0 +1,75 @@
events { worker_connections 1024; }
http {
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
# Redirect HTTP HTTPS
server {
listen 80;
server_name samosachaat.art www.samosachaat.art;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name samosachaat.art www.samosachaat.art;
ssl_certificate /etc/letsencrypt/live/samosachaat.art/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/samosachaat.art/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 10M;
# Auth service
location /api/auth/ {
limit_req zone=api burst=10 nodelay;
proxy_pass http://auth:8001/auth/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Chat API SSE streaming needs special handling
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://chat-api:8002/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE support
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_set_header Connection '';
chunked_transfer_encoding off;
}
# Frontend (Next.js)
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (Next.js HMR in dev, general)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Grafana (optional, if monitoring is running)
location /grafana/ {
proxy_pass http://grafana:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}