From d9896542e19dda1c6956d262c88a8b4c01cd74ef Mon Sep 17 00:00:00 2001 From: Sami Fawcett <34167607+samif0@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:24:35 -0700 Subject: [PATCH] Add CodeDeploy integration --- .github/workflows/deploy.yml | 444 ++-------------------------------- appspec.yml | 22 ++ infra/bin/blackflow-infra.ts | 6 + infra/lib/codedeploy-stack.ts | 21 ++ infra/package.json | 8 + infra/tsconfig.json | 13 + scripts/install.sh | 4 + scripts/start.sh | 4 + scripts/stop.sh | 4 + scripts/validate.sh | 3 + 10 files changed, 110 insertions(+), 419 deletions(-) create mode 100644 appspec.yml create mode 100644 infra/bin/blackflow-infra.ts create mode 100644 infra/lib/codedeploy-stack.ts create mode 100644 infra/package.json create mode 100644 infra/tsconfig.json create mode 100755 scripts/install.sh create mode 100755 scripts/start.sh create mode 100755 scripts/stop.sh create mode 100755 scripts/validate.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 02bfc99..c8ab886 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -98,422 +98,28 @@ jobs: cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/blackflow-auth:buildcache cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/blackflow-auth:buildcache,mode=max - deploy: - name: Deploy to EC2 - needs: build-and-push - runs-on: ubuntu-latest - environment: production - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install SSH Key - uses: shimataro/ssh-key-action@v2 - with: - key: ${{ secrets.SSH_PRIVATE_KEY }} - name: id_ed25519 - known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} - if_key_exists: replace - - - name: Create robust deployment script - run: | - cat > deploy-robust.sh << 'EOF' - #!/bin/bash - # Robust deployment script with full setup checks - - set -e - - # Colors for output - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[1;33m' - BLUE='\033[0;34m' - NC='\033[0m' - - # Configuration - APP_DIR="${HOME}/app" - DOMAIN_NAME="${DOMAIN_NAME:-blackflowlabs.com}" - STAGING_DOMAIN_NAME="staging.${DOMAIN_NAME}" - LETSENCRYPT_DIR="/etc/letsencrypt/live/${DOMAIN_NAME}" - - # Logging functions - log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } - log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } - log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } - log_error() { echo -e "${RED}[ERROR]${NC} $1"; } - - # Create directory structure - log_info "Creating directory structure..." - mkdir -p "${APP_DIR}"/{nginx/{conf.d,ssl},scripts/{deploy,monitor,cleanup,nginx,ssl},logs,blackflow} - - # Handle Git repository - log_info "Managing Git repository..." - cd "${APP_DIR}" - - if [ -d "blackflow/.git" ]; then - log_info "Updating existing repository..." - cd blackflow - git fetch origin - git checkout main || git checkout -b main origin/main - git reset --hard origin/main - cd .. - else - log_info "Cloning repository..." - rm -rf blackflow - git clone https://github.com/samif0/blackflow.git blackflow - fi - - # Handle SSL certificates - log_info "Managing SSL certificates..." - CERT_DEST="${APP_DIR}/nginx/ssl/cert.pem" - KEY_DEST="${APP_DIR}/nginx/ssl/key.pem" - - if [ -f "${LETSENCRYPT_DIR}/fullchain.pem" ] && [ -f "${LETSENCRYPT_DIR}/privkey.pem" ]; then - log_info "Copying Let's Encrypt certificates..." - sudo cp "${LETSENCRYPT_DIR}/fullchain.pem" "${CERT_DEST}" - sudo cp "${LETSENCRYPT_DIR}/privkey.pem" "${KEY_DEST}" - sudo chown $USER:$USER "${CERT_DEST}" "${KEY_DEST}" - sudo chmod 600 "${CERT_DEST}" "${KEY_DEST}" - log_success "SSL certificates copied" - else - log_warning "Let's Encrypt certificates not found" - if [ ! -f "${CERT_DEST}" ] || [ ! -f "${KEY_DEST}" ]; then - log_warning "Creating self-signed certificates..." - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout "${KEY_DEST}" -out "${CERT_DEST}" \ - -subj "/CN=${DOMAIN_NAME}" 2>/dev/null - chmod 600 "${KEY_DEST}" "${CERT_DEST}" - fi - fi - - # Docker login - if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_TOKEN" ]; then - log_info "Logging into Docker Hub..." - echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin - fi - - # Clean up old containers - log_info "Cleaning up old containers..." - docker-compose down 2>/dev/null || true - docker ps -a | grep -E "blackflow|nginx|auth-service" | awk '{print $1}' | xargs -r docker rm -f || true - - # Pull latest images - log_info "Pulling latest images..." - docker pull ${DOCKER_USERNAME}/blackflow:prod-latest - docker pull ${DOCKER_USERNAME}/blackflow:staging-latest - docker pull ${DOCKER_USERNAME}/blackflow-auth:latest - - # Start services - log_info "Starting services..." - cd "${APP_DIR}" - docker-compose up -d - - # Logout from Docker Hub - docker logout - - log_success "Deployment completed successfully!" - docker ps - EOF - - chmod +x deploy-robust.sh - - - name: Copy deployment script to EC2 - run: | - scp deploy-robust.sh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:~/deploy-robust-temp.sh - - - name: Prepare EC2 environment - run: | - ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} << 'EOF' - # Ensure basic tools are installed - if ! command -v docker >/dev/null 2>&1; then - echo "Installing Docker..." - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - sudo usermod -aG docker $USER - rm get-docker.sh - fi - - if ! command -v docker-compose >/dev/null 2>&1; then - echo "Installing Docker Compose..." - sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - sudo chmod +x /usr/local/bin/docker-compose - fi - - if ! command -v git >/dev/null 2>&1; then - echo "Installing Git..." - sudo apt-get update && sudo apt-get install -y git - fi - - # Ensure Docker service is running - sudo systemctl start docker || true - sudo systemctl enable docker || true - - # Create app directory if it doesn't exist - mkdir -p ~/app/scripts/deploy - - # Move deployment script to proper location - mv ~/deploy-robust-temp.sh ~/app/scripts/deploy/deploy-robust.sh - chmod +x ~/app/scripts/deploy/deploy-robust.sh - EOF - - - name: Create Docker Compose file on EC2 - run: | - ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} << 'EOF' - cd ~/app - - cat > docker-compose.yml << 'DOCKERCOMPOSE' - version: '3' - - services: - nginx: - image: nginx:alpine - container_name: nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - - ./nginx/ssl:/etc/nginx/ssl:ro - depends_on: - - blackflow-prod - - blackflow-staging - - auth-service - networks: - - frontend - - backend - restart: unless-stopped - - blackflow-prod: - image: ${{ secrets.DOCKER_USERNAME }}/blackflow:prod-latest - container_name: blackflow-prod - ports: - - "3001:3000" - environment: - - NODE_ENV=production - - API_URL=${{ secrets.PROD_API_URL }} - - AUTH_SECRET=${{ secrets.PROD_AUTH_SECRET }} - - DATABASE_URL=${{ secrets.PROD_DATABASE_URL }} - networks: - - frontend - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 - - blackflow-staging: - image: ${{ secrets.DOCKER_USERNAME }}/blackflow:staging-latest - container_name: blackflow-staging - ports: - - "3002:3000" - environment: - - NODE_ENV=staging - - API_URL=${{ secrets.STAGING_API_URL }} - - AUTH_SECRET=${{ secrets.STAGING_AUTH_SECRET }} - - DATABASE_URL=${{ secrets.STAGING_DATABASE_URL }} - networks: - - frontend - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 - - auth-service: - image: ${{ secrets.DOCKER_USERNAME }}/blackflow-auth:latest - container_name: auth-service - expose: - - "8080" - networks: - - backend - restart: unless-stopped - - networks: - frontend: - driver: bridge - backend: - driver: bridge - DOCKERCOMPOSE - EOF - - - name: Create Nginx configuration on EC2 - run: | - ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} << 'EOF' - cd ~/app - mkdir -p nginx/conf.d - - # Create main nginx.conf - cat > nginx/nginx.conf << 'NGINXCONF' - user nginx; - worker_processes auto; - error_log /var/log/nginx/error.log notice; - pid /run/nginx.pid; - - events { - worker_connections 1024; - } - - http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log main; - sendfile on; - tcp_nopush on; - keepalive_timeout 65; - types_hash_max_size 4096; - include /etc/nginx/conf.d/*.conf; - } - NGINXCONF - - # Create server blocks - cat > nginx/conf.d/default.conf << 'NGINXDEFAULT' - server { - listen 80; - server_name ${{ secrets.DOMAIN_NAME }}; - location / { - return 301 https://$host$request_uri; - } - } - - server { - listen 443 ssl http2; - server_name ${{ secrets.DOMAIN_NAME }}; - - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; - - location / { - proxy_pass http://blackflow-prod:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } - - location /api/auth/ { - proxy_pass http://auth-service:8080/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } - } - - server { - listen 80; - server_name staging.${{ secrets.DOMAIN_NAME }}; - location / { - return 301 https://$host$request_uri; - } - } - - server { - listen 443 ssl http2; - server_name staging.${{ secrets.DOMAIN_NAME }}; - - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; - - location / { - proxy_pass http://blackflow-staging:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } - - location /api/auth/ { - proxy_pass http://auth-service:8080/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } - } - NGINXDEFAULT - EOF - - - name: Run robust deployment - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} - run: | - ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} \ - "cd ~/app && \ - DOMAIN_NAME='${{ secrets.DOMAIN_NAME }}' \ - DOCKER_USERNAME='$DOCKER_USERNAME' \ - DOCKER_TOKEN='$DOCKER_TOKEN' \ - ./scripts/deploy/deploy-robust.sh" - - - name: Verify deployment - run: | - echo "Waiting for services to stabilize..." - sleep 20 - - echo "Checking services on EC2..." - ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} 'docker ps' - - echo "Testing health endpoints..." - - # Test internal endpoints first - ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} << 'EOF' - echo "Testing internal health endpoints..." - - # Production health - if curl -sf http://localhost:3001/api/health; then - echo "✓ Production service is healthy" - else - echo "✗ Production service health check failed" - fi - - # Staging health - if curl -sf http://localhost:3002/api/health; then - echo "✓ Staging service is healthy" - else - echo "✗ Staging service health check failed" - fi - - # Check nginx - if docker exec nginx nginx -t 2>/dev/null; then - echo "✓ Nginx configuration is valid" - else - echo "✗ Nginx configuration error" - fi - EOF - - # Test external endpoints if available - echo "" - echo "Testing external endpoints..." - curl -k -I https://${{ secrets.DOMAIN_NAME }}/api/health || echo "External production check failed (may need DNS setup)" - curl -k -I https://staging.${{ secrets.DOMAIN_NAME }}/api/health || echo "External staging check failed (may need DNS setup)" - - - name: Post-deployment summary - if: always() - run: | - echo "" - echo "======================================" - echo "Deployment Summary" - echo "======================================" - echo "Production: https://${{ secrets.DOMAIN_NAME }}" - echo "Staging: https://staging.${{ secrets.DOMAIN_NAME }}" - echo "" - echo "To monitor logs:" - echo " ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}" - echo " docker logs -f blackflow-prod" - echo " docker logs -f blackflow-staging" - echo " docker logs -f nginx" - echo "" + deploy: + name: Deploy with CodeDeploy + needs: build-and-push + runs-on: ubuntu-latest + environment: production + 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: ${{ secrets.AWS_REGION }} + - name: Bundle application + run: zip -r deployment.zip . -x "*.git*" "node_modules/*" ".github/*" "infra/*" + - name: Upload bundle to S3 + run: aws s3 cp deployment.zip s3://${{ secrets.CODEDEPLOY_BUCKET }}/deployment.zip + - name: Create CodeDeploy deployment + run: | + aws deploy create-deployment \ + --application-name BlackflowApp \ + --deployment-group-name BlackflowDG \ + --s3-location bucket=${{ secrets.CODEDEPLOY_BUCKET }},bundleType=zip,key=deployment.zip + diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..7b8f9c1 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,22 @@ +version: 0.0 +os: linux +files: + - source: / + destination: /home/ec2-user/app +hooks: + BeforeInstall: + - location: scripts/stop.sh + timeout: 300 + runas: ec2-user + AfterInstall: + - location: scripts/install.sh + timeout: 300 + runas: ec2-user + ApplicationStart: + - location: scripts/start.sh + timeout: 300 + runas: ec2-user + ValidateService: + - location: scripts/validate.sh + timeout: 300 + runas: ec2-user diff --git a/infra/bin/blackflow-infra.ts b/infra/bin/blackflow-infra.ts new file mode 100644 index 0000000..3a98aee --- /dev/null +++ b/infra/bin/blackflow-infra.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { CodeDeployStack } from '../lib/codedeploy-stack'; + +const app = new cdk.App(); +new CodeDeployStack(app, 'BlackflowCodeDeployStack'); diff --git a/infra/lib/codedeploy-stack.ts b/infra/lib/codedeploy-stack.ts new file mode 100644 index 0000000..1bbd06c --- /dev/null +++ b/infra/lib/codedeploy-stack.ts @@ -0,0 +1,21 @@ +import * as cdk from 'aws-cdk-lib'; +import * as codedeploy from 'aws-cdk-lib/aws-codedeploy'; + +export class CodeDeployStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const application = new codedeploy.ServerApplication(this, 'BlackflowApp', { + applicationName: 'BlackflowApp', + }); + + new codedeploy.ServerDeploymentGroup(this, 'BlackflowDG', { + application, + deploymentGroupName: 'BlackflowDG', + ec2InstanceTags: new codedeploy.InstanceTagSet({ + Name: ['blackflow-ec2'], + }), + deploymentConfig: codedeploy.ServerDeploymentConfig.ONE_AT_A_TIME, + }); + } +} diff --git a/infra/package.json b/infra/package.json new file mode 100644 index 0000000..854f276 --- /dev/null +++ b/infra/package.json @@ -0,0 +1,8 @@ +{ + "name": "blackflow-infra", + "private": true, + "dependencies": { + "aws-cdk-lib": "^2.137.0", + "constructs": "^10.3.0" + } +} diff --git a/infra/tsconfig.json b/infra/tsconfig.json new file mode 100644 index 0000000..2be2404 --- /dev/null +++ b/infra/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist" + }, + "include": ["**/*.ts"] +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..14b904f --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cd /home/ec2-user/app +docker compose pull diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..e8fffce --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cd /home/ec2-user/app +docker compose up -d diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 0000000..444c572 --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cd /home/ec2-user/app +docker compose down || true diff --git a/scripts/validate.sh b/scripts/validate.sh new file mode 100755 index 0000000..90d13e5 --- /dev/null +++ b/scripts/validate.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +curl -f http://localhost:3000/api/health