1871 lines
69 KiB
YAML
1871 lines
69 KiB
YAML
# ============================================================================
|
||
# 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" <<EOF
|
||
/** @type {import('next').NextConfig} */
|
||
module.exports = {
|
||
output: "standalone",
|
||
};
|
||
EOF
|
||
fi
|
||
|
||
rm -f frontend.env backend.env docker_build.env vault.zip || true
|
||
rm -rf ci || true
|
||
# Use eval with the BUILD_ARGS properly expanded
|
||
eval "docker buildx build \
|
||
${BUILD_SECRET} \
|
||
${BUILD_ARGS} \
|
||
${CACHE_CONFIG} \
|
||
--tag '${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}' \
|
||
${LATEST_TAG} \
|
||
--target production \
|
||
--push \
|
||
--platform linux/amd64 \
|
||
--progress=plain \
|
||
." || {
|
||
echo "❌ Docker build failed with exit code: $?"
|
||
echo "SKIP_DOCKER_BUILD=false" > 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
|