diff --git a/.github/workflows/deploy-ec2.yml b/.github/workflows/deploy-ec2.yml new file mode 100644 index 00000000..43fb1887 --- /dev/null +++ b/.github/workflows/deploy-ec2.yml @@ -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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 06cb35d1..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -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" diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..f5cdc3a0 --- /dev/null +++ b/deploy.sh @@ -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 " + 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 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..9b068cf1 --- /dev/null +++ b/docker-compose.prod.yml @@ -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 diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000..10e62b4e --- /dev/null +++ b/nginx/nginx.conf @@ -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; + } + } +}