# ============================================================================ # OPTIMIZED GITLAB CI/CD PIPELINE FOR NODE.JS APPLICATIONS # ============================================================================ # Features: # - Auto-detection: Next.js, NestJS, React.js, Express.js frameworks # - Flexible Authentication: SSH keys + passwords per environment # - GitLab Self-hosted Compatible # - Robust Detection: CI basic → Ansible comprehensive fallback # - No Auto-Nginx: Manual control with generated templates # - Single Server: Port management + resource optimization # - Production Ready: Security, caching, zero-downtime deployment include: - project: '$PROJECT_CI_ANSIBLE_PATH' ref: 'main' file: '.gitlab-ci/${CI_PROJECT_NAME}.yml' rules: - exists: - '.gitlab-ci/${CI_PROJECT_NAME}.yml' # ============================================================================ # GLOBAL CONFIGURATION # ============================================================================ variables: # Docker and Build Configuration DOCKER_DRIVER: overlay2 DOCKER_BUILDKIT: "1" BUILDKIT_PROGRESS: plain # Auto-detected Application Info APP_NAME: "${CI_PROJECT_NAME}" DOCKER_REGISTRY: "${CI_REGISTRY}" DOCKER_IMAGE: "${CI_REGISTRY_IMAGE}" DEPLOY_TYPE: "${DEPLOY_TYPE:-docker}" BUILD_CACHE_ENABLED: "${BUILD_CACHE_ENABLED:-true}" # GitLab Self-hosted Detection GITLAB_HOST: "${CI_SERVER_HOST}" GITLAB_PORT: "${CI_SERVER_PORT:-443}" GITLAB_ANSIBLE_USERNAME: "${GITLAB_ANSIBLE_USERNAME:-}" GITLAB_ANSIBLE_TOKEN: "${GITLAB_ANSIBLE_TOKEN:-}" # Ansible Configuration ANSIBLE_HOST_KEY_CHECKING: "False" ANSIBLE_STDOUT_CALLBACK: "yaml" ANSIBLE_FORCE_COLOR: "True" # VAULT VAULT_ENGINE_NAME: "${CI_PROJECT_NAME}" VAULT_ADDR: "https://vault.forlife.one" VAULT_USERNAME: "${CI_VAULT_USERNAME}" VAULT_PASSWORD: "${CI_VAULT_PASSWORD}" # Global cache for dependencies cache: key: "$CI_COMMIT_REF_SLUG" paths: - node_modules/ - .npm/ - .yarn/ - .pnpm-store/ policy: push when: on_success # Pipeline stages stages: - prepare - generate - test - security - build - deploy - verify - cleanup # ============================================================================ # REUSABLE TEMPLATES # ============================================================================ .docker_template: &docker_template image: gitlab.forlife.one:5050/share/docker:24-dind services: - gitlab.forlife.one:5050/share/docker:24-dind before_script: - apk add --no-cache bash - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin - | echo "⏳ Waiting for Docker daemon..." for i in {1..10}; do if docker info > /dev/null 2>&1; then echo "✅ Docker is ready!" break fi echo "❌ Docker not ready (attempt $i), retrying in 3s..." sleep 3 done .ansible_template: &ansible_template image: gitlab.forlife.one:5050/share/docker:ansible-2.13 before_script: - apk add --no-cache openssh-client ansible git nodejs npm sshpass wget bash curl jq bc unzip - curl -fsSL https://releases.hashicorp.com/vault/1.15.3/vault_1.15.3_linux_amd64.zip -o vault.zip - unzip vault.zip && mv vault /usr/local/bin/ && chmod +x /usr/local/bin/vault - pip install ansible # Setup SSH agent - eval $(ssh-agent -s) # Flexible SSH key setup based on environment - | echo "Setting up authentication for environment: ${ENVIRONMENT}" if [ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]; then echo "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" | tr -d '\r' | ssh-add - echo "✓ Using Deployment SSH key" else echo "ℹ No SSH key found, will use password authentication" fi # Setup SSH configuration - mkdir -p ~/.ssh && chmod 700 ~/.ssh - echo "Host *" >> ~/.ssh/config - echo " StrictHostKeyChecking no" >> ~/.ssh/config - echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config - echo " LogLevel ERROR" >> ~/.ssh/config # Add known hosts - | if [ -n "$DEPLOYMENT_SERVER_HOST" ]; then ssh-keyscan -H "$DEPLOYMENT_SERVER_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true fi # Clone deployment tools - try multiple methods - | echo "Attempting to clone deployment tools..." rm -rf ci if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then echo "♋ Successfully cloned with CI_JOB_TOKEN" elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN" fi # Install Ansible requirements #- ansible-galaxy install -r requirements.yml || echo "No requirements.yml found" # ============================================================================ # PREPARE STAGE - ENHANCED DETECTION # ============================================================================ prepare:auto-detect: stage: prepare image: gitlab.forlife.one:5050/share/node:18-alpine before_script: - apk add --no-cache jq curl script: - echo "🔍 Starting auto-detection process..." # Enhanced Node.js version detection - | echo "Detecting Node.js version..." NODE_VERSION="" if [ -f ".nvmrc" ]; then NODE_VERSION=$(cat .nvmrc | tr -d 'v' | tr -d '\n' | head -1) if [ -n "$NODE_VERSION" ]; then echo "NODE_VERSION=${NODE_VERSION}" >> deploy.env echo "✓ Detected Node.js version from .nvmrc: ${NODE_VERSION}" else echo "⚠ Empty .nvmrc file, deferring to Ansible" fi elif [ -f "package.json" ] && jq -e '.engines.node' package.json > /dev/null 2>&1; then RAW_VERSION=$(jq -r '.engines.node' package.json) NODE_VERSION=$(echo "$RAW_VERSION" | grep -oE '[0-9]+' | head -n 1) if [ -n "$NODE_VERSION" ] && [ "$NODE_VERSION" != "$RAW_VERSION" ]; then echo "NODE_VERSION=${NODE_VERSION}" >> deploy.env echo "✓ Detected Node.js version from package.json: ${NODE_VERSION} (from ${RAW_VERSION})" else echo "⚠ Complex Node.js version format, deferring to Ansible: ${RAW_VERSION}" fi fi if [ -n "$NODEJS_VERSION" ]; then NODE_VERSION=$(echo "$NODEJS_VERSION" | grep -oE '[0-9]+' | head -n 1) echo "✓ NODE_VERSION: ${NODEJS_VERSION}" echo "NODE_VERSION=${NODEJS_VERSION}" >> deploy.env fi # Enhanced framework detection - | echo "Detecting framework..." # Detect PHP / Laravel first (composer.json or artisan file) if [ -f "composer.json" ] || [ -f "artisan" ]; then if grep -q '"laravel/framework"' composer.json 2>/dev/null || [ -f "artisan" ]; then FRAMEWORK="laravel" CONTAINER_PORT="3000" echo "✓ Detected Laravel (PHP) framework" fi elif [ -f "package.json" ]; then # Check for common frameworks if jq -e '.dependencies["@nestjs/core"] or .dependencies["@nestjs/common"]' package.json > /dev/null 2>&1; then FRAMEWORK="nestjs" CONTAINER_PORT="3000" echo "✓ Detected NestJS framework" elif jq -e '.dependencies["next"]' package.json > /dev/null 2>&1; then FRAMEWORK="nextjs" CONTAINER_PORT="3000" echo "✓ Detected Next.js framework" elif jq -e '.dependencies["react"] and .dependencies["react-scripts"]' package.json > /dev/null 2>&1; then FRAMEWORK="reactjs" CONTAINER_PORT="3000" echo "✓ Detected React.js (CRA) framework" elif jq -e '.dependencies["react"] and .dependencies["vite"]' package.json > /dev/null 2>&1; then FRAMEWORK="reactjs-vite" CONTAINER_PORT="3000" echo "✓ Detected React.js (Vite) framework" elif jq -e '.dependencies["express"]' package.json > /dev/null 2>&1; then FRAMEWORK="expressjs" CONTAINER_PORT="3000" echo "✓ Detected Express.js framework" elif jq -e '.dependencies["fastify"]' package.json > /dev/null 2>&1; then FRAMEWORK="fastify" CONTAINER_PORT="3000" echo "✓ Detected Fastify framework" elif jq -e '.dependencies["@angular/core"]' package.json > /dev/null 2>&1; then FRAMEWORK="angular" CONTAINER_PORT="3000" echo "✓ Detected Angular framework" else FRAMEWORK="" CONTAINER_PORT="3000" echo "ℹ Framework will be detected by Ansible (complex dependencies)" fi else FRAMEWORK="" CONTAINER_PORT="3000" echo "ℹ No package.json found, framework will be detected by Ansible" fi echo "FRAMEWORK=${FRAMEWORK}" >> deploy.env echo "CONTAINER_PORT=${CONTAINER_PORT}" >> deploy.env # Check if this is a frontend project if [ "${FRAMEWORK}" = "nextjs" ] || [ "${FRAMEWORK}" = "reactjs" ] || [ "${FRAMEWORK}" = "reactjs-vite" ] || [ "${FRAMEWORK}" = "angular" ]; then export IS_FRONTEND="true" echo "IS_FRONTEND=true" >> deploy.env else echo "IS_FRONTEND=false" >> deploy.env fi # Enhanced package manager detection - | echo "Detecting package manager..." if [ -f "composer.json" ]; then PACKAGE_MANAGER="composer" echo "✓ Detected Composer (composer.json found)" elif [ -f "pnpm-lock.yaml" ]; then PACKAGE_MANAGER="pnpm" echo "✓ Detected PNPM (pnpm-lock.yaml found)" elif [ -f "yarn.lock" ]; then PACKAGE_MANAGER="yarn" echo "✓ Detected Yarn (yarn.lock found)" elif [ -f "package-lock.json" ]; then PACKAGE_MANAGER="npm" echo "✓ Detected NPM (package-lock.json found)" elif [ -f "package.json" ]; then PACKAGE_MANAGER="npm" echo "✓ Defaulting to NPM (package.json found)" else PACKAGE_MANAGER="" echo "ℹ Package manager will be detected by Ansible" fi echo "PACKAGE_MANAGER=${PACKAGE_MANAGER}" >> deploy.env # Enhanced command detection - | echo "Detecting build and start commands..." if [ -f "package.json" ]; then BUILD_CMD=$(jq -r '.scripts.build // ""' package.json) START_CMD=$(jq -r '.scripts.start // ""' package.json) if [ "$BUILD_CMD" != "" ] && [ "$BUILD_CMD" != "null" ]; then echo "✓ Detected build command: ${BUILD_CMD}" else BUILD_CMD="" echo "ℹ Build command will be detected by Ansible" fi if [ "$START_CMD" != "" ] && [ "$START_CMD" != "null" ]; then echo "✓ Detected start command: ${START_CMD}" else START_CMD="" echo "ℹ Start command will be detected by Ansible" fi else BUILD_CMD="" START_CMD="" echo "ℹ Build and start commands will be detected by Ansible" fi echo "BUILD_COMMAND=${BUILD_CMD}" >> deploy.env echo "START_COMMAND=${START_CMD}" >> deploy.env # Detection summary - | echo "" echo "📋 AUTO-DETECTION SUMMARY" echo "=========================" echo "Project: ${CI_PROJECT_NAME}" echo "Framework: ${FRAMEWORK}" echo "Node.js: ${NODE_VERSION}" echo "Package Manager: ${PACKAGE_MANAGER}" echo "Container Port: ${CONTAINER_PORT}" echo "Build Command: ${BUILD_CMD}" echo "Start Command: ${START_CMD}" echo "" echo "Note: Complex cases will be handled by Ansible's enhanced detection" - echo "🌍 Preparing deployment environment..." - echo "APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}" >> deploy.env - echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> deploy.env - echo "GIT_COMMIT=${CI_COMMIT_SHA}" >> deploy.env - echo "GIT_BRANCH=${CI_COMMIT_REF_NAME}" >> deploy.env - echo "GIT_SHORT_SHA=${CI_COMMIT_SHORT_SHA}" >> deploy.env - echo "PIPELINE_ID=${CI_PIPELINE_ID}" >> deploy.env # Pass through detected values with prefixes for Ansible - echo "CI_DETECTED_NODE_VERSION=${NODE_VERSION:-}" >> deploy.env - echo "CI_DETECTED_FRAMEWORK=${FRAMEWORK:-}" >> deploy.env - echo "CI_DETECTED_PACKAGE_MANAGER=${PACKAGE_MANAGER:-}" >> deploy.env - echo "CI_DETECTED_BUILD_COMMAND=${BUILD_COMMAND:-}" >> deploy.env - echo "CI_DETECTED_START_COMMAND=${START_COMMAND:-}" >> deploy.env - echo "CI_DETECTED_CONTAINER_PORT=${CONTAINER_PORT:-3000}" >> deploy.env - echo "✓ Environment variables prepared" - | # Detect environment if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then ENV="development" elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then ENV="staging" elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then ENV="production" else ENV="development" fi - echo "ENVIRONMENT=${ENVIRONMENT:-$ENV}" >> deploy.env artifacts: reports: dotenv: deploy.env expire_in: 2 hours rules: - if: $CI_COMMIT_TAG # ============================================================================ # TEST STAGE - FRAMEWORK AWARE # ============================================================================ # ============================================================================ # SECURITY STAGE # ============================================================================ # ============================================================================ # GENERATE STAGE - DYNAMIC DOCKERFILE CREATION # ============================================================================ generate:dockerfile: <<: *ansible_template stage: generate needs: ["prepare:auto-detect", "prepare:frontend-env"] image: gitlab.forlife.one:5050/share/docker:alpine-latest before_script: - apk add --no-cache envsubst git curl jq script: - | echo "Attempting to clone deployment tools..." rm -rf ci if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then echo "♋ Successfully cloned with CI_JOB_TOKEN" elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN" fi - | echo "🔧 Generating Dockerfile for ${FRAMEWORK:-nodejs} framework..." # Check if Dockerfile already exists if [ "$FRAMEWORK" = "LARAVEL" ] && [ -f "Dockerfile" ]; then echo "✅ Dockerfile already exists, using existing one" echo "DOCKERFILE_GENERATED=false" >> dockerfile.env exit 0 fi if [ ! -d "ci" ]; then echo "⚠️ ci folder does not exists, using existing one" exit 0 fi # Detect environment if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then ENV="development" elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then ENV="staging" elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then ENV="production" else ENV="development" fi # Determine MEMORY_LIMIT with fallback logic MEMORY_LIMIT=${MEMORY_LIMIT:-} if [ -z "$MEMORY_LIMIT" ]; then echo "🔍 MEMORY_LIMIT not set in CI variables, checking config files..." # Try project-specific config first PROJECT_CONFIG="ci/projects/${APP_NAME}/group_vars/${ENV}.yml" if [ -f "$PROJECT_CONFIG" ]; then echo "📄 Found project config: $PROJECT_CONFIG" MEMORY_LIMIT=$(grep -A3 "resource_limits:" "$PROJECT_CONFIG" | grep -A2 "${ENV}:" | grep "memory:" | awk '{print $2}' | tr -d '"' | head -n1) fi # Try vault config if still empty if [ -z "$MEMORY_LIMIT" ]; then VAULT_CONFIG="ci/group_vars/vault/${ENV}.yml" if [ -f "$VAULT_CONFIG" ]; then echo "📄 Found vault config: $VAULT_CONFIG" MEMORY_LIMIT=$(grep -A3 "resource_limits:" "$VAULT_CONFIG" | grep -A2 "${ENV}:" | grep "memory:" | awk '{print $2}' | tr -d '"' | head -n1) fi fi # Default fallback if [ -z "$MEMORY_LIMIT" ]; then echo "⚙️ No MEMORY_LIMIT found, using default: 2g" MEMORY_LIMIT="2g" else echo "✅ Found MEMORY_LIMIT: $MEMORY_LIMIT" fi else echo "✅ Using MEMORY_LIMIT from CI variables: $MEMORY_LIMIT" fi # Convert memory limit to number (in MB, without unit) if echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+g$'; then MEMORY_GB=$(echo "$MEMORY_LIMIT" | sed 's/g$//') MEMORY_LIMIT_MB=$((MEMORY_GB * 1024)) echo "📊 Memory limit: ${MEMORY_GB}g = ${MEMORY_LIMIT_MB} (MB)" elif echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+m$'; then MEMORY_LIMIT_MB=$(echo "$MEMORY_LIMIT" | sed 's/m$//') echo "📊 Memory limit: ${MEMORY_LIMIT_MB} (MB)" elif echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+$'; then MEMORY_LIMIT_MB=$MEMORY_LIMIT echo "📊 Memory limit: ${MEMORY_LIMIT_MB} (MB)" else echo "⚠️ Invalid MEMORY_LIMIT format: $MEMORY_LIMIT, using default 2048" MEMORY_LIMIT_MB=2048 fi # Apply 80% factor to MEMORY_LIMIT_MB MEMORY_LIMIT_MB=$(awk "BEGIN {printf \"%.0f\", $MEMORY_LIMIT_MB * 0.8}") APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} # Method 1: Check for local templates first (in ci/roles/docker/templates/dockerfiles/) TEMPLATE_DIR="ci/roles/docker/templates/dockerfiles" if [ -d "$TEMPLATE_DIR" ]; then echo "📁 Found local template directory: $TEMPLATE_DIR" USE_LOCAL_TEMPLATES=true else echo "📥 No local templates, will fetch from ansible repo" USE_LOCAL_TEMPLATES=false fi # Determine framework-specific settings FRAMEWORK="${FRAMEWORK:-}" RUN_CMD="${RUN_CMD:-}" case "$FRAMEWORK" in "expressjs"|"fastify") TEMPLATE_FILE="expressjs.Dockerfile.j2" DEFAULT_PORT="3000" RUN_CMD='["dumb-init", "node", "dist/main"]' if [[ "$CI_COMMIT_TAG" == *worker* ]]; then RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]' fi ;; "nestjs") TEMPLATE_FILE="nestjs.Dockerfile.j2" DEFAULT_PORT="3000" RUN_CMD='["dumb-init", "node", "dist/main"]' if [[ "$CI_COMMIT_TAG" == *worker* ]]; then RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]' fi ;; "nextjs") TEMPLATE_FILE="nextjs.Dockerfile.j2" DEFAULT_PORT="3000" #RUN_CMD='["npm", "start"]' if [[ "$RUN_CMD" == "" ]]; then RUN_CMD='["node", "server.js"]' fi ;; "reactjs"|"reactjs-vite"|"angular") TEMPLATE_FILE="reactjs.Dockerfile.j2" DEFAULT_PORT="3000" RUN_CMD='[ "serve", "-s", "build" ]' ;; "laravel") TEMPLATE_FILE="laravel.Dockerfile.j2" DEFAULT_PORT="3000" RUN_CMD='[ "serve", "-s", "build" ]' ;; *) TEMPLATE_FILE="nodejs.Dockerfile.j2" DEFAULT_PORT="3000" RUN_CMD='["dumb-init", "node", "dist/main"]' if [[ "$CI_COMMIT_TAG" == *worker* ]]; then RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]' fi ;; esac if [ "${FRAMEWORK}" = "laravel" ]; then echo "Determining required PHP version based on Laravel version..." # 1. Đọc phiên bản Laravel từ composer.json (ví dụ: "^10.0") LARAVEL_CONSTRAINT=$(jq -r '.require."laravel/framework" // empty' composer.json) if [ -z "$LARAVEL_CONSTRAINT" ]; then echo "Laravel framework version not found in composer.json. Using default PHP 8.1" PHP_VERSION="8.1" else # 2. Lấy số phiên bản chính (ví dụ: từ "^10.0" -> "10") LARAVEL_MAJOR_VERSION=$(echo $LARAVEL_CONSTRAINT | tr -d '^~>=' | cut -d'.' -f1) PHP_VERSION="" # 3. Ánh xạ phiên bản Laravel sang phiên bản PHP yêu cầu case "$LARAVEL_MAJOR_VERSION" in "13") PHP_VERSION="8.4" ;; "12") PHP_VERSION="8.3" ;; "11") PHP_VERSION="8.2" ;; "10") PHP_VERSION="8.1" ;; "9") PHP_VERSION="8.1" ;; "8") PHP_VERSION="8.1" ;; *) # Nếu không khớp, đặt một giá trị mặc định an toàn và cảnh báo echo "Warning: Unsupported or unknown Laravel version ($LARAVEL_MAJOR_VERSION). Falling back to PHP 8.1." PHP_VERSION="8.1" ;; esac echo "Detected Laravel version: $LARAVEL_MAJOR_VERSION -> Required PHP version: $PHP_VERSION" fi fi echo "📦 Target template: ${TEMPLATE_FILE}" echo "🚀 Framework: ${FRAMEWORK}" echo "🔌 Port: ${CONTAINER_PORT:-$DEFAULT_PORT}" # Method 1: Use local templates (if exists) if [ "$USE_LOCAL_TEMPLATES" = "true" ] && [ -f "$TEMPLATE_DIR/$TEMPLATE_FILE" ]; then echo "✅ Using local template: $TEMPLATE_DIR/$TEMPLATE_FILE" TEMPLATE_PATH="$TEMPLATE_DIR/$TEMPLATE_FILE" # Method 2: Fallback to inline templates else echo "⚠️ No template source configured, using inline fallback" echo "💡 Set ANSIBLE_REPO_URL or TEMPLATE_BASE_URL for external templates" TEMPLATE_PATH="" fi # Process template if found if [ -n "$TEMPLATE_PATH" ] && [ -f "$TEMPLATE_PATH" ]; then echo "🔄 Processing template: $TEMPLATE_PATH" if [ -n "${BUILD_COMMAND:-}" ]; then BUILD_COMMAND=$(echo "$BUILD_COMMAND" | sed 's/&&/\\\&\\\&/g') fi if [ -n "${START_COMMAND:-}" ]; then START_COMMAND=$(echo "$START_COMMAND" | sed 's/&&/\\\&\\\&/g') fi # Set all variables for Jinja2-style replacement export app_name="${APP_NAME}" export node_version="${NODE_VERSION:-18}" export package_manager="${PACKAGE_MANAGER:-npm}" export build_command="${BUILD_COMMAND:-}" export start_command="${START_COMMAND:-node index.js}" export framework_settings_port="${CONTAINER_PORT:-$DEFAULT_PORT}" export docker_image_tag="${APP_VERSION:-latest}" export APP_PORT="${framework_settings_port}" export PHP_VERSION="${PHP_VERSION:-8.1}" export php_version="${PHP_VERSION:-8.1}" export MEMORY_LIMIT_MB="${MEMORY_LIMIT_MB:-2048}" # Add frontend environment variables if applicable if [ "${IS_FRONTEND:-false}" = "true" ] && [ -f ".env" ]; then echo "🌐 Adding frontend environment variables..." # Process .env file safely while IFS='=' read -r key value || [ -n "$key" ]; do # Skip comments and empty lines case "$key" in \#*|'') continue ;; esac # Skip empty keys if [ -z "$key" ]; then continue fi # Clean the value (remove quotes if present) case "$value" in \"*\") value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') ;; \'*\') value=$(echo "$value" | sed "s/^'//" | sed "s/'$//") ;; esac export "${key}=${value}" done < .env fi # Method A: Simple variable substitution (works for basic templates) echo "🔧 Method A: Simple variable substitution" sed -e "s|{{ app_name }}|${app_name}|g" \ -e "s|{{ node_version }}|${node_version}|g" \ -e "s|{{ package_manager }}|${package_manager}|g" \ -e "s|{{ build_command }}|${build_command}|g" \ -e "s|{{ start_command }}|${start_command}|g" \ -e "s|{{ framework_settings.port }}|${framework_settings_port}|g" \ -e "s|{{ framework_settings_port }}|${framework_settings_port}|g" \ -e "s|{{ docker_image_tag }}|${docker_image_tag}|g" \ -e "s|{{ RUN_CMD }}|${RUN_CMD}|g" \ -e "s|{{ php_version }}|${PHP_VERSION:-8.1}|g" \ -e "s|{{ MEMORY_LIMIT_MB }}|${MEMORY_LIMIT_MB:-2048}|g" \ "$TEMPLATE_PATH" > Dockerfile.tmp # Method B: Advanced replacement for complex Jinja2 (if needed) if grep -q "{%" "$TEMPLATE_PATH"; then echo "🔧 Method B: Complex Jinja2 detected, using advanced processing" # Install j2cli for proper Jinja2 processing (optional) pip install j2cli 2>/dev/null || { echo "⚠️ j2cli not available, falling back to sed replacement" # Handle {% if %} blocks with sed (basic approach) sed -i '/{% if start_command and start_command != /,/{% else %}/c\ # Using detected start command' Dockerfile.tmp sed -i '/{% endif %}/d' Dockerfile.tmp } # Alternative: Use j2cli if available if command -v j2 >/dev/null 2>&1; then echo "✅ Using j2cli for proper Jinja2 processing" # Create variables file cat > /tmp/vars.json << EOF { "app_name": "${app_name}", "node_version": "${node_version}", "package_manager": "${package_manager}", "build_command": "${build_command}", "start_command": "${start_command}", "php_version": "${PHP_VERSION:-8.1}", "framework_settings_port": "${framework_settings_port}", "framework_settings": { "port": "${framework_settings_port}" }, "docker_image_tag": "${docker_image_tag}", "RUN_CMD": "${RUN_CMD}", "MEMORY_LIMIT_MB": "${MEMORY_LIMIT_MB:-2048}" } EOF j2 "$TEMPLATE_PATH" /tmp/vars.json > Dockerfile.tmp fi fi # Final processing with envsubst for any remaining ${VAR} patterns envsubst < Dockerfile.tmp > Dockerfile rm -f Dockerfile.tmp echo "✅ Generated Dockerfile from template: $TEMPLATE_FILE" else echo "🔄 Using inline fallback template for ${FRAMEWORK}" # Fallback to previous inline method case "$FRAMEWORK" in "expressjs"|"fastify"|*) cat > Dockerfile << 'EOF' # Generated Express.js/Node.js Dockerfile ARG NODE_VERSION=18 FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base RUN apk add --no-cache libc6-compat curl dumb-init WORKDIR /app FROM base AS deps COPY package*.json yarn.lock* pnpm-lock.yaml* ./ RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi FROM base AS builder COPY --from=deps /app/node_modules ./node_modules COPY . . RUN if [ -f package.json ] && grep -q '"build"' package.json; then npm run build; fi FROM base AS production RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nodejs COPY --from=builder --chown=nodejs:nodejs /app . USER nodejs EXPOSE 3000 CMD ["dumb-init", "node", "index.js"] EOF ;; esac fi # Show final Dockerfile preview echo "📄 Generated Dockerfile preview:" head -20 Dockerfile echo "..." echo "DOCKERFILE_GENERATED=true" >> dockerfile.env echo "TEMPLATE_USED=${TEMPLATE_FILE:-inline}" >> dockerfile.env artifacts: paths: - Dockerfile reports: dotenv: dockerfile.env expire_in: 1 hour rules: - if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2" generate:dockerignore: <<: *ansible_template stage: generate needs: ["prepare:auto-detect"] image: gitlab.forlife.one:5050/share/docker:alpine-latest before_script: - apk add --no-cache git curl script: - | echo "📁 Fetching .dockerignore file..." # Check if project already has .dockerignore if [ -f ".dockerignore" ]; then echo "ℹ️ Project already has .dockerignore file" exit 0 fi ANSIBLE_DOCKERIGNORE="ci/.dockerignore" if [ -f "$ANSIBLE_DOCKERIGNORE" ]; then echo "✅ Found .dockerignore in Ansible repo" cp "$ANSIBLE_DOCKERIGNORE" .dockerignore echo "DOCKERIGNORE_SOURCE=ansible-repo" >> dockerignore.env fi artifacts: paths: - .dockerignore reports: dotenv: dockerignore.env expire_in: 1 hour when: always rules: - if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2" # Enhanced prepare stage with frontend env support prepare:frontend-env: stage: prepare needs: ["prepare:auto-detect"] image: gitlab.forlife.one:5050/share/docker:alpine-latest before_script: - apk add --no-cache bash # Đảm bảo có bash script: - | #!/bin/bash set -e # Exit on error echo "🎯 Setting up environment with quote validation..." # Detect environment if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then ENV="development" elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then ENV="staging" elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then ENV="production" else ENV="development" fi echo "📝 Environment: $ENV" # Check if it's a frontend framework is_frontend=false if [ "${FRAMEWORK}" = "nextjs" ] || [ "${FRAMEWORK}" = "reactjs" ] || [ "${FRAMEWORK}" = "reactjs-vite" ] || [ "${FRAMEWORK}" = "angular" ]; then is_frontend=true fi if [ "$is_frontend" = "true" ]; then if [[ -f "./.gitlab-ci/${ENV}.env" ]]; then echo "✅ Found: .gitlab-ci/${ENV}.env" cp "./.gitlab-ci/${ENV}.env" "./.env" fi if [[ -f "./.gitlab/ci/${ENV}.sh" ]]; then bash ".gitlab/ci/${ENV}.sh" fi if [ -f ".env" ]; then echo "✅ Found: .env" # 🔧 ADVANCED QUOTE CLEANING echo "🧹 Cleaning quotes and special characters..." # Step 1: Create working copy cp ".env" "temp.env" # Step 2: Clean problematic content sed -i 's/\r$//' temp.env # Remove Windows line endings sed -i '/^$/d' temp.env # Remove empty lines sed -i '/^[[:space:]]*#/d' temp.env # Remove comments sed -i 's/^[[:space:]]*//' temp.env # Remove leading spaces sed -i 's/[[:space:]]*$//' temp.env # Remove trailing spaces # Step 3: Initialize frontend.env cat > frontend.env << EOF TARGET_ENV=${ENV} FRAMEWORK=${FRAMEWORK} IS_FRONTEND=true EOF else echo "⚠️ No env file: .env" cat > frontend.env << EOF TARGET_ENV=${ENV} FRAMEWORK=${FRAMEWORK} IS_FRONTEND=false EOF fi else echo "⏭️ Backend project" cat > frontend.env << EOF TARGET_ENV=${ENV} FRAMEWORK=${FRAMEWORK} IS_FRONTEND=false EOF fi # Final validation echo "🔍 Validating frontend.env syntax..." # Test if file exists and is readable if [ ! -f "frontend.env" ]; then echo "❌ frontend.env file not created" exit 1 fi # Basic syntax check - try to read each line validation_failed=false while IFS= read -r line || [ -n "$line" ]; do if [ -n "$line" ] && ! echo "$line" | grep -q "^[A-Z][A-Z0-9_]*="; then echo "❌ Invalid line format: $line" validation_failed=true fi done < frontend.env if [ "$validation_failed" = "true" ]; then echo "❌ Syntax validation failed" echo "📋 File contents:" cat frontend.env exit 1 fi echo "✅ Syntax validation passed" echo "📋 Final preview:" head -10 frontend.env | sed 's/=.*/=***/' artifacts: paths: - .env - frontend.env # Đảm bảo cả 2 file đều được lưu reports: dotenv: frontend.env expire_in: 2 hours rules: - if: $CI_COMMIT_TAG # ============================================================================ # BUILD STAGE - FRAMEWORK OPTIMIZED # ============================================================================ build:docker: <<: *docker_template stage: build needs: ["prepare:auto-detect", "prepare:frontend-env", "generate:dockerfile", "generate:dockerignore"] retry: max: 1 when: - script_failure - runner_system_failure before_script: - apk add --no-cache bash envsubst curl jq unzip - curl -fsSL https://releases.hashicorp.com/vault/1.15.3/vault_1.15.3_linux_amd64.zip -o vault.zip - unzip vault.zip && mv vault /usr/local/bin/ && chmod +x /usr/local/bin/vault && rm -f vault.zip - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin - | echo "⏳ Waiting for Docker daemon..." for i in {1..10}; do if docker info > /dev/null 2>&1; then echo "✅ Docker is ready!" break fi echo "❌ Docker not ready (attempt $i), retrying in 3s..." sleep 3 done # Nếu hết retry mà vẫn fail thì exit để GitLab tự retry job if ! docker info > /dev/null 2>&1; then echo "❌ Docker vẫn chưa sẵn sàng sau nhiều lần thử. Thoát để GitLab retry job." MESSAGE="🚨 CI job *failed* in project \`$CI_PROJECT_PATH\` at stage \`$CI_JOB_STAGE\`!\n\nDocker daemon is not ready on runner \`$CI_RUNNER_DESCRIPTION\`.\nCommit: \`$CI_COMMIT_SHORT_SHA\`\nJob URL: $CI_JOB_URL" curl -s -X POST https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage \ -d chat_id="${TELEGRAM_CHAT_ID}" \ -d parse_mode=Markdown \ -d text="$MESSAGE" exit 1 fi script: - | echo "Attempting to clone deployment tools..." rm -rf ci if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then echo "♋ Successfully cloned with CI_JOB_TOKEN" elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN" fi - | #!/bin/bash set -e # Exit on error echo "🐳 Building Docker image with framework optimization..." echo "Framework: ${FRAMEWORK:-}" echo "Node.js: ${NODE_VERSION:-}" echo "Package Manager: ${PACKAGE_MANAGER:-}" echo "Environment: ${ENVIRONMENT:-development}" # Check Dockerfile status if [ "${DOCKERFILE_GENERATED:-false}" == "true" ]; then echo "✅ Using auto-generated Dockerfile for ${FRAMEWORK}" elif [ -f "Dockerfile" ]; then echo "✅ Using existing Dockerfile from repository" else echo "❌ No Dockerfile available - this should not happen" exit 1 fi if [ "${FRAMEWORK}" = "nextjs" ] && [ ! -f "next.config.ts" ] && [ ! -f "next.config.js" ]; then echo "❌ Không tìm thấy next.config.js hoặc next.config.ts trong dự án Next.js." echo "❌ Hãy thêm next.config.js/ts và cấu hình output standalone để build Docker image đúng cách." exit 1 fi if [ "${FRAMEWORK}" = "expressjs" ]; then if [ ! -d "src/public" ]; then mkdir -p "src/public" fi fi if [ "${FRAMEWORK}" = "expressjs" ] || [ "${FRAMEWORK}" = "nestjs" ]; then if [ ! -f "src/main.ts" ] && [ -f "src/index.ts" ]; then cp "src/index.ts" "src/main.ts" fi fi if [[ "$IS_FRONTEND" == "true" ]]; then cd ci CI_PROJECT_DIR="projects/${APP_NAME}" if [[ -f "../.gitlab-ci/${ENVIRONMENT}.env" ]]; then echo "✅ Found: .gitlab-ci/${ENVIRONMENT}.env" cp "../.gitlab-ci/${ENVIRONMENT}.env" "../.env" fi if [[ -f "../.gitlab/ci/${ENVIRONMENT}.sh" ]]; then cd ../ && bash ".gitlab/ci/${ENVIRONMENT}.sh" && cd ci fi if [ ! -d "../public" ]; then mkdir -p "../public" fi cd ../ fi export VAULT_ADDR="${VAULT_ADDR}" VAULT_PATH="${VAULT_ENGINE_NAME}/${ENVIRONMENT}" export VAULT_PATH="${VAULT_PATH}" VAULT_RESPONSE=$(curl --silent --request POST \ --data "{\"password\": \"${VAULT_PASSWORD}\"}" \ "${VAULT_ADDR}/v1/auth/userpass/login/${VAULT_USERNAME}") #echo "🔐 Vault login response:" #echo "$VAULT_RESPONSE" | jq VAULT_TOKEN=$(echo "$VAULT_RESPONSE" | jq -r '.auth.client_token') if [[ "$VAULT_TOKEN" == "null" || -z "$VAULT_TOKEN" ]]; then echo "❌ Failed to get Vault token" exit 1 fi export VAULT_TOKEN="${VAULT_TOKEN}" VAULT_OUTPUT=$(vault kv get -format=json "$VAULT_PATH" 2>/dev/null || echo "") # Kiểm tra nếu không lấy được VAULT_OUTPUT if [[ -z "$VAULT_OUTPUT" ]] || ! echo "$VAULT_OUTPUT" | jq empty >/dev/null 2>&1; then echo "⚠️ Failed to get or parse Vault response for ${VAULT_PATH}" else SECRETS_JSON=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | @json') if [[ -n "$SECRETS_JSON" && "$SECRETS_JSON" != "null" ]]; then echo "🔐 Vault setting for ${VAULT_PATH} will be used!" # Dùng jq để chuyển đổi JSON sang định dạng KEY="VALUE" # to_entries[]: Chuyển object {"k1":"v1", "k2":"v2"} thành các dòng {"key":"k1", "value":"v1"} # "\(.key)=\"\(.value)\"": Định dạng mỗi dòng thành KEY="VALUE" #SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\"\(.value)\""') SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"') # Kiểm tra xem có secret nào được tìm thấy không if [[ -n "$SECRETS_TO_ENV" ]]; then # Ghi các secret đã được định dạng vào file .env echo "$SECRETS_TO_ENV" > .env fi else echo "⚠️ No secrets found at ${VAULT_PATH}" fi fi APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} # Setup Docker Buildx with unique name BUILDER_NAME="builder-${CI_PIPELINE_ID}-${CI_JOB_ID}" docker buildx create --use --name "$BUILDER_NAME" || true # ======================================================================== # FRAMEWORK-SPECIFIC BUILD ARGS PREPARATION # ======================================================================== echo "🔧 Preparing build arguments..." # Initialize build args BUILD_ARGS="" # Add base build args one by one BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_VERSION='${NODE_VERSION:-18}'" BUILD_ARGS="${BUILD_ARGS} --build-arg PACKAGE_MANAGER='${PACKAGE_MANAGER:-npm}'" BUILD_ARGS="${BUILD_ARGS} --build-arg APP_PORT='${CONTAINER_PORT:-3000}'" BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_ENV='production'" BUILD_ARGS="${BUILD_ARGS} --build-arg APP_VERSION='${APP_VERSION}'" BUILD_ARGS="${BUILD_ARGS} --build-arg FRAMEWORK='${FRAMEWORK:-nodejs}'" # Add optional build args if they have values if [ -n "${BUILD_COMMAND:-}" ]; then ESCAPED=$(echo "$START_COMMAND" | sed 's/"/\\"/g' | sed "s/'/'\"'\"'/g") BUILD_ARGS="${BUILD_ARGS} --build-arg BUILD_COMMAND='${ESCAPED}'" fi if [ -n "${START_COMMAND:-}" ]; then ESCAPED=$(echo "$START_COMMAND" | sed 's/"/\\"/g' | sed "s/'/'\"'\"'/g") #BUILD_ARGS="${BUILD_ARGS} --build-arg START_COMMAND='${ESCAPED}'" fi if [ -n "${BUILD_DATE:-}" ]; then BUILD_ARGS="${BUILD_ARGS} --build-arg BUILD_DATE='${BUILD_DATE}'" fi if [ -n "${GIT_COMMIT:-}" ]; then BUILD_ARGS="${BUILD_ARGS} --build-arg GIT_COMMIT='${GIT_COMMIT}'" fi if [ -n "${PIPELINE_ID:-}" ]; then BUILD_ARGS="${BUILD_ARGS} --build-arg PIPELINE_ID='${PIPELINE_ID}'" fi # Add frontend environment variables if applicable if [ "${IS_FRONTEND:-false}" = "true" ] && [ -f ".env" ]; then echo "🌐 Adding frontend environment variables..." # Process .env file safely while IFS='=' read -r key value || [ -n "$key" ]; do # Skip comments and empty lines case "$key" in \#*|'') continue ;; esac # Skip empty keys if [ -z "$key" ]; then continue fi # Skip keys that are already in base build args to avoid duplicates case "$key" in NODE_VERSION|PACKAGE_MANAGER|APP_PORT|PORT|NODE_ENV|APP_VERSION|FRAMEWORK|BUILD_COMMAND|START_COMMAND|BUILD_DATE|GIT_COMMIT|PIPELINE_ID) echo "⚠️ Skipped duplicate: ${key} (already in base args)" continue ;; esac # Clean the value (remove quotes if present) case "$value" in \"*\") value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') ;; \'*\') value=$(echo "$value" | sed "s/^'//" | sed "s/'$//") ;; esac # Add to build args with proper quoting BUILD_ARGS="${BUILD_ARGS} --build-arg ${key}='${value}'" echo "✓ Added build arg: ${key}=${value:0:30}..." done < .env fi # Debug: Show actual BUILD_ARGS content echo "🔍 DEBUG: BUILD_ARGS content:" echo "BUILD_ARGS='${BUILD_ARGS}'" echo "🔍 DEBUG: First few build args:" echo "${BUILD_ARGS}" | tr ' ' '\n' | head -10 # ======================================================================== # DOCKER BUILD EXECUTION # ======================================================================== echo "🚀 Starting Docker build with ${FRAMEWORK} optimization..." # Setup cache configuration CACHE_CONFIG="" if [ "${BUILD_CACHE_ENABLED:-true}" == "true" ]; then if [ -n "${FRAMEWORK:-}" ]; then CACHE_CONFIG="--cache-from type=registry,ref=${DOCKER_IMAGE}:cache-${FRAMEWORK} --cache-to type=registry,ref=${DOCKER_IMAGE}:cache-${FRAMEWORK},mode=max" else CACHE_CONFIG="--cache-from type=registry,ref=${DOCKER_IMAGE}:cache --cache-to type=registry,ref=${DOCKER_IMAGE}:cache,mode=max" fi else CACHE_CONFIG="--no-cache" fi # Create output file first (in case of failure) - SIMPLE dotenv format echo "SKIP_DOCKER_BUILD=false" > docker_build.env echo "BUILD_SUCCESS=false" >> docker_build.env echo "DOCKER_IMAGE_BUILT=${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" >> docker_build.env echo "🔨 Executing Docker build command..." echo "🔍 DEBUG: Command length: ${#BUILD_ARGS} characters" # Write build args to a file to avoid command line length issues echo "📝 Writing build args to file for docker buildx..." echo "$BUILD_ARGS" | tr ' ' '\n' | grep -E "^--build-arg" > /tmp/build_args.txt echo "📝 Build args file created: $(wc -l < /tmp/build_args.txt) lines" # Show sample of build args file echo "🔍 Sample build args:" head -5 /tmp/build_args.txt echo "▶️ Starting docker buildx build..." # Set conditional tag LATEST_TAG="" if [ "${ENVIRONMENT:-development}" == "production" ]; then LATEST_TAG="--tag '${DOCKER_IMAGE}:${APP_VERSION}-latest'" fi # Kiểm tra nếu có .env thì thêm vào BUILD_SECRET if [ -f .env ]; then echo "✅ Found .env, using as Docker secret" BUILD_SECRET="--secret id=env,src=.env" else echo "⚠️ .env not found, skipping --secret" BUILD_SECRET="" fi if [ "${FRAMEWORK}" = "laravel" ]; then cp -r ci/roles/docker/templates/nginx ./docker fi if [[ "$FRAMEWORK" == "expressjs" ]]; then if [ ! -d "src/public" ]; then mkdir -p "src/public" fi fi if [[ "$FRAMEWORK" == "nextjs" ]] && [[ ! -f "../next.config.js" ]]; then cat > "../next.config.js" < docker_build.env echo "BUILD_SUCCESS=false" >> docker_build.env echo "DOCKER_IMAGE_BUILT=" >> docker_build.env rm -f /tmp/build_args.txt exit 1 } # Build successful - update dotenv file echo "SKIP_DOCKER_BUILD=false" > docker_build.env echo "BUILD_SUCCESS=true" >> docker_build.env echo "DOCKER_IMAGE_BUILT=${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" >> docker_build.env # Cleanup rm -f /tmp/build_args.txt echo "✅ Docker image built successfully:" echo " - ${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" if [ "${ENVIRONMENT:-development}" == "production" ]; then echo " - ${DOCKER_IMAGE}:${APP_VERSION}-latest" fi # Cleanup docker buildx rm "$BUILDER_NAME" || true # artifacts: # reports: # dotenv: docker_build.env # expire_in: 1 hour # when: always rules: - if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2" # ============================================================================ # ADDITIONAL JOBS FOR ENVIRONMENT-SPECIFIC BUILDS # ============================================================================ # Build for specific environments (if needed) #build:docker:development: # extends: build:docker # variables: # ENVIRONMENT: "development" # # NextJS development-specific variables # NEXT_PUBLIC_API_URL: "${NEXT_PUBLIC_API_URL_DEV:-http://localhost:3001}" # NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL_DEV:-http://localhost:3000}" # # React development-specific variables # REACT_APP_API_URL: "${REACT_APP_API_URL_DEV:-http://localhost:3001}" # # Build optimizations for development # OPTIMIZE_IMAGES: "false" # COMPRESS_ASSETS: "false" # ENABLE_BUNDLE_ANALYZER: "false" # rules: # - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+$/ && $DEPLOY_TYPE != "pm2" # ============================================================================ # DEPLOYMENT STAGES - FLEXIBLE AUTHENTICATION # ============================================================================ .deploy_template: &deploy_template <<: *ansible_template needs: ["prepare:auto-detect", "prepare:frontend-env", "build:docker"] script: - | APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} echo "🚀 Starting deployment with auto-scaling" echo "App: ${APP_NAME}" echo "Version: ${APP_VERSION}" echo "Framework: ${FRAMEWORK:-}" echo "Environment: ${ENVIRONMENT:-unknown}" echo "Node.js: ${NODE_VERSION:-}" echo "Host: ${DEPLOYMENT_SERVER_HOST:-not-set}:${DEPLOYMENT_SERVER_PORT:-not-set}" echo "User: ${DEPLOYMENT_SERVER_USERNAME:-not-set}" echo "Target Replicas: ${DEPLOYMENT_REPLICAS:-1}" echo "Deployment Strategy: ${DEPLOYMENT_STRATEGY:-rolling}" # Validate required variables if [ -z "$DEPLOYMENT_SERVER_HOST" ]; then echo "❌ DEPLOYMENT_SERVER_HOST is required" exit 1 fi cd ci CI_PROJECT_DIR="projects/${APP_NAME}" if [ ! -d "$CI_PROJECT_DIR" ]; then echo "📁 Folder '$CI_PROJECT_DIR' does not exist. Creating..." mkdir -p "$CI_PROJECT_DIR/templates" fi if [[ ! -f "$CI_PROJECT_DIR/vars.yml" ]]; then touch "$CI_PROJECT_DIR/vars.yml" fi if [ ! -d "$CI_PROJECT_DIR/templates" ]; then mkdir -p "$CI_PROJECT_DIR/templates" fi INVENTORY_FILE="inventory/${ENVIRONMENT}.yml" # Check if inventory file exists if [[ ! -f "$INVENTORY_FILE" ]]; then echo "Error: Inventory file not found: $INVENTORY_FILE" exit 1 fi AUTH_METHOD=${AUTH_METHOD:-password} if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]] || [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_PATH" ]]; then AUTH_METHOD="ssh_key" fi ENV_TAG=${ENV_TAG:-""} if [[ "$ENV_TAG" == "worker" ]]; then ENV_TAG="-${ENV_TAG}" fi ANSIBLE_CMD="ansible-playbook -i $INVENTORY_FILE playbook.yml" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"app_name=${APP_NAME} deploy_env=${ENVIRONMENT} gitlab_ci_token=${CI_JOB_TOKEN} git_repo=${CI_REPOSITORY_URL} git_tag=${CI_COMMIT_TAG} git_sha=${CI_COMMIT_SHA} git_short_sha=${CI_COMMIT_SHORT_SHA}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_user=${DEPLOYMENT_SERVER_USERNAME} auth_method=${AUTH_METHOD:-password} deploy_strategy=${DEPLOY_TYPE:-docker} env_tag=${ENV_TAG}\"" # Auto-scaling configuration if [[ -n "${DEPLOYMENT_REPLICAS:-}" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"replicas=${DEPLOYMENT_REPLICAS:-1}\"" fi if [[ -n "${DEPLOYMENT_MAX_REPLICAS:-}" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"max_replicas=${DEPLOYMENT_MAX_REPLICAS:-9}\"" fi ANSIBLE_CMD=" $ANSIBLE_CMD -e \"deployment_strategy=${DEPLOYMENT_STRATEGY:-rolling}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"health_check_timeout=${HEALTH_CHECK_TIMEOUT:-30}\"" if [ "${ENVIRONMENT}" = "production" ]; then if [[ -n "${CPU_LIMIT:-}" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_cpu_limit=${CPU_LIMIT}\"" fi if [[ -n "${MEMORY_LIMIT:-}" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_memory_limit=${MEMORY_LIMIT}\"" fi fi # Add authentication and other parameters if [[ -n "$DEPLOYMENT_SERVER_HOST" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_host=${DEPLOYMENT_SERVER_HOST}\"" fi if [[ -n "$DEPLOYMENT_SERVER_PORT" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_port=${DEPLOYMENT_SERVER_PORT}\"" fi if [[ "$AUTH_METHOD" == "password" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_password=${DEPLOYMENT_SERVER_PASSWORD} server_become_password=${DEPLOYMENT_SERVER_PASSWORD}\"" elif [[ "$AUTH_METHOD" == "ssh_key" ]]; then if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_PATH" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"ssh_key_path=${DEPLOYMENT_SERVER_SSH_KEY_PATH}\"" fi if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"ssh_key_content=${DEPLOYMENT_SERVER_SSH_KEY_CONTENT}\"" fi fi if [[ -n "$GIT_USER" && -n "$GIT_TOKEN" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD -e \"git_username=$GIT_USER git_token=$GIT_TOKEN\"" fi ANSIBLE_CMD=" $ANSIBLE_CMD -e \"is_frontend=${IS_FRONTEND}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_registry=${CI_REGISTRY}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_project_path=${CI_PROJECT_PATH}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_name=${CI_REGISTRY_IMAGE}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_tag=${APP_VERSION}-${CI_COMMIT_SHORT_SHA}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_full=${CI_REGISTRY_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"node_version=${NODE_VERSION:-}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"framework=${FRAMEWORK:-}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"package_manager=${PACKAGE_MANAGER:-}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"build_command=${BUILD_COMMAND:-}\"" #ANSIBLE_CMD=" $ANSIBLE_CMD -e \"start_command=${START_COMMAND:-}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_port=${CONTAINER_PORT:-3000}\"" ANSIBLE_CMD=" $ANSIBLE_CMD -e \"pipeline_id=${PIPELINE_ID}\"" export VAULT_ADDR="${VAULT_ADDR}" VAULT_PATH="${VAULT_ENGINE_NAME}/${ENVIRONMENT}" export VAULT_PATH="${VAULT_PATH}" VAULT_RESPONSE=$(curl --silent --request POST \ --data "{\"password\": \"${VAULT_PASSWORD}\"}" \ "${VAULT_ADDR}/v1/auth/userpass/login/${VAULT_USERNAME}") #echo "🔐 Vault login response:" #echo "$VAULT_RESPONSE" | jq VAULT_TOKEN=$(echo "$VAULT_RESPONSE" | jq -r '.auth.client_token') if [[ "$VAULT_TOKEN" == "null" || -z "$VAULT_TOKEN" ]]; then echo "❌ Failed to get Vault token" exit 1 fi export VAULT_TOKEN="${VAULT_TOKEN}" VAULT_OUTPUT=$(vault kv get -format=json "$VAULT_PATH" 2>/dev/null || echo "") # Kiểm tra nếu không lấy được VAULT_OUTPUT if [[ -z "$VAULT_OUTPUT" ]] || ! echo "$VAULT_OUTPUT" | jq empty >/dev/null 2>&1; then echo "⚠️ Failed to get or parse Vault response for ${VAULT_PATH}" else SECRETS_JSON=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | @json') if [[ -n "$SECRETS_JSON" && "$SECRETS_JSON" != "null" ]]; then echo "🔐 Vault setting for ${VAULT_PATH} will be used!" # Dùng jq để chuyển đổi JSON sang định dạng KEY="VALUE" # to_entries[]: Chuyển object {"k1":"v1", "k2":"v2"} thành các dòng {"key":"k1", "value":"v1"} # "\(.key)=\"\(.value)\"": Định dạng mỗi dòng thành KEY="VALUE" #SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\"\(.value)\""') SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"') # Kiểm tra xem có secret nào được tìm thấy không if [[ -n "$SECRETS_TO_ENV" ]]; then # Ghi các secret đã được định dạng vào file .env echo "$SECRETS_TO_ENV" > "$CI_PROJECT_DIR/templates/.env.j2" fi fi fi if [[ -n "$EXTRA_VARS" ]]; then ANSIBLE_CMD=" $ANSIBLE_CMD --extra-vars '$EXTRA_VARS'" fi echo $ANSIBLE_CMD eval $ANSIBLE_CMD # --extra-vars '{ # "database_host": "'$DEV_DATABASE_HOST'", # "database_user": "'$DEV_DATABASE_USER'", # "database_password": "'$DEV_DATABASE_PASSWORD'", # "database_name": "myapp_dev", # "redis_host": "'$DEV_REDIS_HOST'", # "jwt_secret": "'$DEV_JWT_SECRET'", # "app_url": "http://'$DEV_SERVER_HOST':3000" # }' # Development deployment deploy:development: extends: .deploy_template stage: deploy environment: name: development variables: ENVIRONMENT: "development" DEPLOYMENT_SERVER_HOST: "$DEVELOPMENT_SERVER_HOST" DEPLOYMENT_SERVER_PORT: "$DEVELOPMENT_SERVER_PORT" DEPLOYMENT_SERVER_USERNAME: "$DEVELOPMENT_SERVER_USERNAME" DEPLOYMENT_SERVER_PASSWORD: "$DEVELOPMENT_SERVER_PASSWORD" DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$DEVELOPMENT_SERVER_SSH_KEY_CONTENT" DEPLOYMENT_SERVER_SSH_KEY_PATH: "$DEVELOPMENT_SERVER_SSH_KEY_PATH" # Auto-scaling configuration for development DEPLOYMENT_REPLICAS: "$DEVELOPMENT_REPLICAS" DEPLOYMENT_MAX_REPLICAS: "$DEVELOPMENT_MAX_REPLICAS" DEPLOYMENT_STRATEGY: "$DEVELOPMENT_DEPLOYMENT_STRATEGY" HEALTH_CHECK_TIMEOUT: "$DEVELOPMENT_HEALTH_CHECK_TIMEOUT" rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+$/ # Staging deployment deploy:staging: extends: .deploy_template stage: deploy environment: name: staging variables: ENVIRONMENT: "staging" DEPLOYMENT_SERVER_HOST: "$STAGING_SERVER_HOST" DEPLOYMENT_SERVER_PORT: "$STAGING_SERVER_PORT" DEPLOYMENT_SERVER_USERNAME: "$STAGING_SERVER_USERNAME" DEPLOYMENT_SERVER_PASSWORD: "$STAGING_SERVER_PASSWORD" DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$STAGING_SERVER_SSH_KEY_CONTENT" DEPLOYMENT_SERVER_SSH_KEY_PATH: "$STAGING_SERVER_SSH_KEY_PATH" # Auto-scaling configuration for staging DEPLOYMENT_REPLICAS: "$STAGING_REPLICAS" DEPLOYMENT_MAX_REPLICAS: "$STAGING_MAX_REPLICAS" DEPLOYMENT_STRATEGY: "$STAGING_DEPLOYMENT_STRATEGY" HEALTH_CHECK_TIMEOUT: "$STAGING_HEALTH_CHECK_TIMEOUT" rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-stg\.\d+$/ # Production deployment deploy:production: extends: .deploy_template stage: deploy environment: name: production variables: ENVIRONMENT: "production" DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST" DEPLOYMENT_SERVER_PORT: "$PRODUCTION_SERVER_PORT" DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME" DEPLOYMENT_SERVER_PASSWORD: "$PRODUCTION_SERVER_PASSWORD" DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$PRODUCTION_SERVER_SSH_KEY_CONTENT" DEPLOYMENT_SERVER_SSH_KEY_PATH: "$PRODUCTION_SERVER_SSH_KEY_PATH" # Auto-scaling configuration for production DEPLOYMENT_REPLICAS: "$PRODUCTION_REPLICAS" DEPLOYMENT_MAX_REPLICAS: "$PRODUCTION_MAX_REPLICAS" DEPLOYMENT_STRATEGY: "$PRODUCTION_DEPLOYMENT_STRATEGY" HEALTH_CHECK_TIMEOUT: "$PRODUCTION_HEALTH_CHECK_TIMEOUT" rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ when: manual allow_failure: false # Development deployment for worker deploy:development:worker: extends: .deploy_template stage: deploy environment: name: development variables: ENVIRONMENT: "development" DEPLOYMENT_SERVER_HOST: "$DEVELOPMENT_SERVER_HOST" DEPLOYMENT_SERVER_PORT: "$DEVELOPMENT_SERVER_PORT" DEPLOYMENT_SERVER_USERNAME: "$DEVELOPMENT_SERVER_USERNAME" DEPLOYMENT_SERVER_PASSWORD: "$DEVELOPMENT_SERVER_PASSWORD" DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$DEVELOPMENT_SERVER_SSH_KEY_CONTENT" DEPLOYMENT_SERVER_SSH_KEY_PATH: "$DEVELOPMENT_SERVER_SSH_KEY_PATH" ENV_TAG: "worker" # Worker-specific auto-scaling (typically needs more replicas) DEPLOYMENT_REPLICAS: "$DEVELOPMENT_WORKER_REPLICAS" DEPLOYMENT_MAX_REPLICAS: "$DEVELOPMENT_WORKER_MAX_REPLICAS" DEPLOYMENT_STRATEGY: "$DEVELOPMENT_WORKER_DEPLOYMENT_STRATEGY" HEALTH_CHECK_TIMEOUT: "$DEVELOPMENT_HEALTH_CHECK_TIMEOUT" rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+\-worker$/ # Staging deployment for worker deploy:staging:worker: extends: .deploy_template stage: deploy environment: name: staging variables: ENVIRONMENT: "staging" DEPLOYMENT_SERVER_HOST: "$STAGING_SERVER_HOST" DEPLOYMENT_SERVER_PORT: "$STAGING_SERVER_PORT" DEPLOYMENT_SERVER_USERNAME: "$STAGING_SERVER_USERNAME" DEPLOYMENT_SERVER_PASSWORD: "$STAGING_SERVER_PASSWORD" DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$STAGING_SERVER_SSH_KEY_CONTENT" DEPLOYMENT_SERVER_SSH_KEY_PATH: "$STAGING_SERVER_SSH_KEY_PATH" ENV_TAG: "worker" # Worker-specific auto-scaling (typically needs more replicas) DEPLOYMENT_REPLICAS: "$STAGING_WORKER_REPLICAS" DEPLOYMENT_MAX_REPLICAS: "$STAGING_WORKER_MAX_REPLICAS" DEPLOYMENT_STRATEGY: "$STAGING_WORKER_DEPLOYMENT_STRATEGY" HEALTH_CHECK_TIMEOUT: "$STAGING_HEALTH_CHECK_TIMEOUT" rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-stg\.\d+\-worker$/ # Production deployment for worker deploy:production:worker: extends: .deploy_template stage: deploy environment: name: production variables: ENVIRONMENT: "production" DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST" DEPLOYMENT_SERVER_PORT: "$PRODUCTION_SERVER_PORT" DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME" DEPLOYMENT_SERVER_PASSWORD: "$PRODUCTION_SERVER_PASSWORD" DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$PRODUCTION_SERVER_SSH_KEY_CONTENT" DEPLOYMENT_SERVER_SSH_KEY_PATH: "$PRODUCTION_SERVER_SSH_KEY_PATH" ENV_TAG: "worker" # Production worker auto-scaling DEPLOYMENT_REPLICAS: "$PRODUCTION_WORKER_REPLICAS" DEPLOYMENT_MAX_REPLICAS: "$PRODUCTION_WORKER_MAX_REPLICAS" DEPLOYMENT_STRATEGY: "$PRODUCTION_WORKER_DEPLOYMENT_STRATEGY" HEALTH_CHECK_TIMEOUT: "$PRODUCTION_HEALTH_CHECK_TIMEOUT" rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+\-worker$/ when: manual allow_failure: false # ============================================================================ # VERIFICATION STAGE # ============================================================================ #verify:deployment: # stage: verify # image: gitlab.forlife.one:5050/share/docker:alpine-latest # needs: # - job: deploy:development # optional: true # - job: deploy:staging # optional: true # before_script: # - apk add --no-cache curl jq bc # script: # - | # echo "🔍 Verifying deployment..." # # # Determine which environment was deployed # if [ "$CI_COMMIT_BRANCH" = "dev" ]; then # VERIFY_HOST="${DEVELOPMENT_HOST}" # VERIFY_PORT="3001" # elif [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then # VERIFY_HOST="${STAGING_HOST}" # VERIFY_PORT="3002" # else # echo "ℹ No verification needed for this branch" # exit 0 # fi # # echo "Verifying: http://${VERIFY_HOST}:${VERIFY_PORT}" # # # Basic connectivity test # response=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "http://${VERIFY_HOST}:${VERIFY_PORT}/" || echo "000") # if [ "$response" = "200" ] || [ "$response" = "301" ] || [ "$response" = "302" ]; then # echo "✅ Basic connectivity test passed (HTTP $response)" # else # echo "❌ Basic connectivity test failed (HTTP $response)" # exit 1 # fi # # # Performance test # load_time=$(curl -s -o /dev/null -w "%{time_total}" --max-time 15 "http://${VERIFY_HOST}:${VERIFY_PORT}/" || echo "999") # echo "⏱️ Response time: ${load_time}s" # # if [ "$(echo "$load_time < 5.0" | bc)" = "1" ]; then # echo "✅ Performance test passed" # else # echo "⚠️ Performance test warning: Response time is high (${load_time}s)" # fi # # echo "✅ Deployment verification completed successfully" # # rules: # - if: $CI_COMMIT_BRANCH == "dev" # - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # allow_failure: true # ============================================================================ # CLEANUP STAGE # ============================================================================ cleanup:images: <<: *docker_template stage: cleanup script: - | echo "🧹 Cleaning up old Docker images..." # Remove images older than 7 days docker image prune -af --filter "until=168h" || true # Keep only last 5 versions per framework if [ "${FRAMEWORK:-}" != "" ]; then echo "Cleaning up old ${FRAMEWORK} images..." # This would require additional logic to clean specific framework images fi echo "✅ Cleanup completed" rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: delayed start_in: 2 hours allow_failure: true # ============================================================================ # MANUAL OPERATIONS # ============================================================================ manual:rollback: <<: *ansible_template stage: deploy script: - | echo "🔄 Rolling back ${APP_NAME} in ${ENVIRONMENT} environment" echo "Target version: ${ROLLBACK_VERSION:-previous}" echo "Target replicas: ${ROLLBACK_REPLICAS:-current}" cd ci ansible-playbook \ -i inventory/${ENVIRONMENT}.yml \ playbook.yml \ -e app_name="${APP_NAME}" \ -e deploy_env="${ENVIRONMENT}" \ -e rollback_version="${ROLLBACK_VERSION:-}" \ -e rollback_replicas="${ROLLBACK_REPLICAS:-}" \ -e force_rollback=true \ --tags rollback echo "✅ Rollback completed" variables: ENVIRONMENT: "production" ROLLBACK_VERSION: "$ROLLBACK_VERSION" ROLLBACK_REPLICAS: "$ROLLBACK_REPLICAS" when: manual rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ && $CI_COMMIT_BRANCH == "main_rollback" - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+\-worker$/ && $CI_COMMIT_BRANCH == "main_rollback_worker" allow_failure: false manual:emergency-scale: <<: *ansible_template stage: deploy script: - | echo "🚨 Emergency scaling ${APP_NAME} to ${TARGET_REPLICAS} replicas" echo "Environment: ${SCALE_ENVIRONMENT}" # Direct script execution for emergency scaling ssh ${DEPLOYMENT_SERVER_USERNAME}@${DEPLOYMENT_SERVER_HOST} \ "/home/${DEPLOYMENT_SERVER_USERNAME}/scripts/manage-${APP_NAME}-${SCALE_ENVIRONMENT}.sh scale ${TARGET_REPLICAS}" # Show status after scaling ssh ${DEPLOYMENT_SERVER_USERNAME}@${DEPLOYMENT_SERVER_HOST} \ "/home/${DEPLOYMENT_SERVER_USERNAME}/scripts/manage-${APP_NAME}-${SCALE_ENVIRONMENT}.sh status" variables: SCALE_ENVIRONMENT: "$SCALE_ENVIRONMENT" TARGET_REPLICAS: "$TARGET_REPLICAS" DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST" DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME" when: manual rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ && $CI_COMMIT_BRANCH == "main_scale" allow_failure: true # ============================================================================ # NOTIFICATIONS # ============================================================================ notify:slack: stage: .post image: gitlab.forlife.one:5050/share/docker:alpine-latest before_script: - apk add --no-cache curl jq script: - | echo "📢 Sending deployment notification with auto-scaling info..." # Determine status and emoji if [ "$CI_JOB_STATUS" = "success" ]; then color="good" emoji="🎉" status_text="SUCCESS" else color="danger" emoji="❌" status_text="FAILED" fi # Determine environment if echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+"; then env_name="Development" replicas="${DEV_REPLICAS:-2}" strategy="${DEV_DEPLOYMENT_STRATEGY:-rolling}" elif echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+-stg\.[0-9]+"; then env_name="Staging" replicas="${STG_REPLICAS:-3}" strategy="${STG_DEPLOYMENT_STRATEGY:-rolling}" elif echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+"; then env_name="Production" replicas="${PROD_REPLICAS:-5}" strategy="${PROD_DEPLOYMENT_STRATEGY:-rolling}" else env_name="Unknown" replicas="2" strategy="rolling" fi # Send Slack notification curl -X POST -H 'Content-type: application/json' \ --data "{ \"text\": \"${emoji} Auto-Scaling Deployment ${status_text}\", \"attachments\": [{ \"color\": \"${color}\", \"fields\": [ {\"title\": \"Application\", \"value\": \"${APP_NAME}\", \"short\": true}, {\"title\": \"Environment\", \"value\": \"${env_name}\", \"short\": true}, {\"title\": \"Framework\", \"value\": \"${FRAMEWORK:-Node.js}\", \"short\": true}, {\"title\": \"Version\", \"value\": \"${APP_VERSION}\", \"short\": true}, {\"title\": \"Replicas\", \"value\": \"${replicas}\", \"short\": true}, {\"title\": \"Strategy\", \"value\": \"${strategy}\", \"short\": true}, {\"title\": \"Node.js\", \"value\": \"${NODE_VERSION:-auto-detected}\", \"short\": true}, {\"title\": \"Branch\", \"value\": \"${CI_COMMIT_REF_NAME}\", \"short\": true}, {\"title\": \"Commit\", \"value\": \"${CI_COMMIT_SHORT_SHA}\", \"short\": true}, {\"title\": \"Pipeline\", \"value\": \"<${CI_PIPELINE_URL}|#${CI_PIPELINE_ID}>\", \"short\": true} ], \"footer\": \"GitLab CI/CD Auto-Scaling\", \"ts\": $(date +%s) }] }" \ "${SLACK_WEBHOOK_URL}" || echo "Failed to send Slack notification" rules: - if: $SLACK_WEBHOOK_URL when: always allow_failure: true # ============================================================================ # GITLAB CI VARIABLES CONFIGURATION EXAMPLES # ============================================================================ # Example GitLab CI/CD Variables for auto-scaling: # # Development Environment: # DEV_REPLICAS=2 # DEV_MAX_REPLICAS=4 # DEV_DEPLOYMENT_STRATEGY=rolling # DEV_HEALTH_CHECK_TIMEOUT=30 # # Staging Environment: # STG_REPLICAS=3 # STG_MAX_REPLICAS=5 # STG_DEPLOYMENT_STRATEGY=rolling # STG_HEALTH_CHECK_TIMEOUT=30 # # Production Environment: # PROD_REPLICAS=5 # PROD_MAX_REPLICAS=10 # PROD_DEPLOYMENT_STRATEGY=rolling # PROD_HEALTH_CHECK_TIMEOUT=45 # # Worker Applications: # DEV_WORKER_REPLICAS=3 # DEV_WORKER_MAX_REPLICAS=6 # PROD_WORKER_REPLICAS=8 # PROD_WORKER_MAX_REPLICAS=15