# =============================================================================================================== # bitbucket-pipelines.yml - Pipeline CI/CD para proyectosacc # Descripción: # Pipeline de 9 pasos estándar de CCsoft para desplegar infraestructura (Terraform), # frontend React (S3+CloudFront) y API backend (EC2) de SACC. # # Autor: Área de Tecnología y Desarrollo - CCsoft # =============================================================================================================== image: atlassian/default-image:5 options: max-time: 120 oidc: audiences: - sts.amazonaws.com definitions: steps: - step: &install-aws-cli name: Install AWS CLI script: - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - step: ¬ify-start name: Notify Start script: - export TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh start - step: ¬ify-fail name: Notify Failure script: - export TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh failure "Paso: ${BITBUCKET_STEP_KEY}" pipelines: default: - step: name: 04_build script: - set -euo pipefail - echo "=== Build de proyectosacc (sin deploy) ===" - | if [ -f package.json ]; then npm ci npm run build else echo "INFO: No se encontró package.json. Saltando build npm." fi - | if [ -f gradlew ] || [ -f build.gradle ]; then ./gradlew clean bootJar else echo "INFO: No se encontró gradlew ni build.gradle. Saltando build Gradle." fi - echo "Build condicional completado." branches: developer: - step: name: 01_image-setup script: - set -euo pipefail - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk wget unzip curl expect - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - mkdir -p ~/.ssh - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key - ssh-keyscan -p "22" "${DEV_INSTANCE_IP}" >> ~/.ssh/known_hosts 2>/dev/null || true - eval "$(ssh-agent -s)" - | expect -c " spawn ssh-add ~/.ssh/sacc4_key expect \"Enter passphrase\" send \"${SSH_PASSPHRASE_THOTH}\r\" expect eof " - export TELEGRAM_BOT_TOKEN="${DEV_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${DEV_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh start - step: name: 02_pre_terraform_check oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh dev - bash scripts/terraform-lock-cleanup.sh dev proyectosacc/terraform.tfstate - step: name: 03_terraform oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh dev - cd terraform - wget -q "https://releases.hashicorp.com/terraform/1.11.4/terraform_1.11.4_linux_amd64.zip" - unzip -q terraform_1.11.4_linux_amd64.zip - mv terraform /usr/local/bin/terraform - terraform version - export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-mx-central-1}" - terraform init -backend-config=backend.dev.hcl - echo "=== Ejecutando Terraform Plan (solo verificación) ===" - terraform plan -lock-timeout=5m -var-file=environments/dev.tfvars -var="db_password=${DEV_DB_PASSWORD}" # NOTA DE SEGURIDAD: -auto-approve es aceptable en DEV porque: # 1. El plan ya se ejecutó arriba y se visualiza en los logs del pipeline # 2. DEV es un ambiente de desarrollo donde los cambios son reversibles # 3. El pipeline corre en branches 'developer' con revisión previa vía PR # Para PROD nunca usar -auto-approve sin revisión manual del plan. - echo "=== Ejecutando Terraform Apply (auto-approve en DEV) ===" - terraform apply -lock-timeout=5m -auto-approve -var-file=environments/dev.tfvars -var="db_password=${DEV_DB_PASSWORD}" - terraform output -json > terraform-outputs.json - cat terraform-outputs.json artifacts: - terraform/terraform-outputs.json - step: name: 04_build script: - set -euo pipefail - | if [ -f package.json ]; then npm ci npm run build else echo "INFO: No se encontró package.json. Saltando build npm." fi - | if [ -f gradlew ] || [ -f build.gradle ]; then ./gradlew clean bootJar else echo "INFO: No se encontró gradlew ni build.gradle. Saltando build Gradle." fi - echo "Build condicional completado." artifacts: - build/** - build/libs/*.jar - step: name: 05_publish oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh dev - | if [ -d build/ ] && [ "$(ls -A build/ 2>/dev/null)" ]; then aws s3 sync build/ "s3://${DEV_S3_FRONTEND_BUCKET}/" --delete else echo "INFO: No se encontró directorio build/ con contenido. Saltando sync a S3." fi - | if ls build/libs/*.jar >/dev/null 2>&1; then aws s3 cp build/libs/*.jar "s3://${DEV_S3_ARTIFACTS_BUCKET}/develop/proyectosacc-app.jar" else echo "INFO: No se encontró JAR en build/libs/. Saltando copia a S3." fi - echo "Publish condicional completado." - step: name: 06_update_ssh_keys script: - set -euo pipefail - apt-get update -y && apt-get install -y expect - mkdir -p ~/.ssh - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key - eval "$(ssh-agent -s)" - | expect -c " spawn ssh-add ~/.ssh/sacc4_key expect \"Enter passphrase\" send \"${SSH_PASSPHRASE_THOTH}\r\" expect eof " - | DEV_PUB_KEY=$(echo "${SSH_PRIVATE_KEY_THOTH}" | ssh-keygen -y -f /dev/stdin) ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ "thoth@${DEV_INSTANCE_IP}" \ "bash -c 'mkdir -p /home/thoth/.ssh && chmod 700 /home/thoth/.ssh && echo \"${DEV_PUB_KEY}\" > /home/thoth/.ssh/authorized_keys && chmod 600 /home/thoth/.ssh/authorized_keys && chown -R thoth:thoth /home/thoth/.ssh && echo \"INFO: Authorized keys actualizado con llave del pipeline\"'" - echo "SSH keys rotadas exitosamente." - step: name: 07_install script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip expect - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - | JAR_LOCAL_PATTERN="build/libs/*.jar" JAR_S3_URI="s3://${DEV_S3_ARTIFACTS_BUCKET}/develop/proyectosacc-app.jar" HAS_LOCAL_JAR=false if ls ${JAR_LOCAL_PATTERN} >/dev/null 2>&1; then HAS_LOCAL_JAR=true fi if [ "${HAS_LOCAL_JAR}" = "true" ]; then echo "INFO: Artefacto JAR encontrado localmente. Procediendo con instalación en servidor." mkdir -p ~/.ssh echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key chmod 600 ~/.ssh/sacc4_key eval "$(ssh-agent -s)" expect -c " spawn ssh-add ~/.ssh/sacc4_key expect \"Enter passphrase\" send \"${SSH_PASSPHRASE_THOTH}\r\" expect eof " ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ "thoth@${DEV_INSTANCE_IP}" \ "bash -c 'mkdir -p /home/thoth/deploy/artifacts/current && aws s3 cp ${JAR_S3_URI} /home/thoth/deploy/artifacts/current/proyectosacc-app.jar && chown osiris:osiris /home/thoth/deploy/artifacts/current/proyectosacc-app.jar'" else echo "INFO: No se encontró artefacto JAR localmente. Saltando instalación." fi - echo "Install condicional completado." - step: name: 08_deploy oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip expect - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh dev - mkdir -p ~/.ssh - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key - eval "$(ssh-agent -s)" - | expect -c " spawn ssh-add ~/.ssh/sacc4_key expect \"Enter passphrase\" send \"${SSH_PASSPHRASE_THOTH}\r\" expect eof " - | ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ "thoth@${DEV_INSTANCE_IP}" \ "bash -c 'if [ -f /home/thoth/deploy/setup/deploy.sh ]; then bash /home/thoth/deploy/setup/deploy.sh; else echo \"INFO: No se encontró script de deploy. Saltando deploy backend.\"; fi'" - | if [ -f terraform/terraform-outputs.json ]; then export CLOUDFRONT_DISTRIBUTION_ID=$(python3 -c "import json; print(json.load(open('terraform/terraform-outputs.json'))['cloudfront_distribution_id']['value'])") aws cloudfront create-invalidation --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" --paths "/*" else echo "INFO: No se encontró terraform-outputs.json. Saltando invalidación de CloudFront." fi - export TELEGRAM_BOT_TOKEN="${DEV_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${DEV_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh success "Deploy condicional completado" - step: name: 09_health_check oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl - source scripts/aws-oidc-setup.sh dev - echo "=== Health Check post-deploy (DEV) ===" - | export ENV=dev export HEALTH_URL="http://${DEV_INSTANCE_IP}:8080/actuator/health" export MAX_RETRIES=12 export RETRY_INTERVAL=10 # Verificar CloudFront CLOUDFRONT_DOMAIN=$(aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items[0]=='sacc.ccsoft.mx'].DomainName" --output text) if [ -n "${CLOUDFRONT_DOMAIN}" ]; then export CF_DOMAIN="${CLOUDFRONT_DOMAIN}" fi # Verificar RDS si está configurado if [ -n "${DEV_DB_HOST:-}" ]; then export DB_HOST="${DEV_DB_HOST}" fi bash scripts/health-check.sh - export TELEGRAM_BOT_TOKEN="${DEV_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${DEV_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh success "Health Check pasó - Servicios saludables" master: - step: name: 01_image-setup script: - set -euo pipefail - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk wget unzip curl - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - mkdir -p ~/.ssh - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key - ssh-keyscan -p "22" "${PROD_INSTANCE_IP}" >> ~/.ssh/known_hosts 2>/dev/null || true - export TELEGRAM_BOT_TOKEN="${PROD_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${PROD_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh start - step: name: 02_pre_terraform_check oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh prod - bash scripts/terraform-lock-cleanup.sh prod proyectosacc/terraform.tfstate - step: name: 03_terraform oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh prod - cd terraform - wget -q "https://releases.hashicorp.com/terraform/1.11.4/terraform_1.11.4_linux_amd64.zip" - unzip -q terraform_1.11.4_linux_amd64.zip - mv terraform /usr/local/bin/terraform - terraform version - export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-mx-central-1}" - terraform init -backend-config=backend.prod.hcl # NOTA DE SEGURIDAD: En PROD nunca usar -auto-approve sin revisión manual. # El plan se guarda en prod.tfplan y debe ser revisado antes de aplicar. # El paso 08_deploy requiere aprobación manual (trigger: manual). - echo "=== Ejecutando Terraform Plan (PROD - requiere revisión manual) ===" - terraform plan -lock-timeout=5m -var-file=environments/prod.tfvars -var="db_password=${PROD_DB_PASSWORD}" -out=prod.tfplan - echo "=== Terraform Plan guardado en prod.tfplan ===" - echo "AVISO: Revisar el plan arriba antes de aprobar el deploy manual." - terraform output -json > terraform-outputs.json - cat terraform-outputs.json artifacts: - terraform/terraform-outputs.json - terraform/prod.tfplan - step: name: 04_build script: - set -euo pipefail - | if [ -f package.json ]; then npm ci npm run build else echo "INFO: No se encontró package.json. Saltando build npm." fi - | if [ -f gradlew ] || [ -f build.gradle ]; then ./gradlew clean bootJar else echo "INFO: No se encontró gradlew ni build.gradle. Saltando build Gradle." fi - echo "Build condicional completado." artifacts: - build/** - build/libs/*.jar - step: name: 05_publish oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh prod - | if [ -d build/ ] && [ "$(ls -A build/ 2>/dev/null)" ]; then aws s3 sync build/ "s3://${PROD_S3_FRONTEND_BUCKET}/" --delete else echo "INFO: No se encontró directorio build/ con contenido. Saltando sync a S3." fi - | if ls build/libs/*.jar >/dev/null 2>&1; then aws s3 cp build/libs/*.jar "s3://${PROD_S3_ARTIFACTS_BUCKET}/main/proyectosacc-app.jar" else echo "INFO: No se encontró JAR en build/libs/. Saltando copia a S3." fi - echo "Publish condicional completado." - step: name: 06_update_ssh_keys script: - set -euo pipefail - mkdir -p ~/.ssh - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key # Actualizar authorized_keys del usuario thoth con la llave pública del pipeline # Esto asegura que solo el pipeline actual pueda acceder, rotando llaves automáticamente - | PROD_PUB_KEY=$(echo "${SSH_PRIVATE_KEY_THOTH}" | ssh-keygen -y -f /dev/stdin) ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ "thoth@${PROD_INSTANCE_IP}" \ "bash -c 'mkdir -p /home/thoth/.ssh && chmod 700 /home/thoth/.ssh && echo \"${PROD_PUB_KEY}\" > /home/thoth/.ssh/authorized_keys && chmod 600 /home/thoth/.ssh/authorized_keys && chown -R thoth:thoth /home/thoth/.ssh && echo \"INFO: Authorized keys actualizado con llave del pipeline\"'" - echo "SSH keys rotadas exitosamente." - step: name: 07_install script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - | JAR_LOCAL_PATTERN="build/libs/*.jar" JAR_S3_URI="s3://${PROD_S3_ARTIFACTS_BUCKET}/main/proyectosacc-app.jar" HAS_LOCAL_JAR=false if ls ${JAR_LOCAL_PATTERN} >/dev/null 2>&1; then HAS_LOCAL_JAR=true fi if [ "${HAS_LOCAL_JAR}" = "true" ]; then echo "INFO: Artefacto JAR encontrado localmente. Procediendo con instalación en servidor." echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key chmod 600 ~/.ssh/sacc4_key ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ "thoth@${PROD_INSTANCE_IP}" \ "bash -c 'mkdir -p /home/thoth/deploy/artifacts/current && aws s3 cp ${JAR_S3_URI} /home/thoth/deploy/artifacts/current/proyectosacc-app.jar && chown osiris:osiris /home/thoth/deploy/artifacts/current/proyectosacc-app.jar'" else echo "INFO: No se encontró artefacto JAR localmente. Saltando instalación." fi - echo "Install condicional completado." - step: name: 07b_notify_approval script: - set -euo pipefail - export TELEGRAM_BOT_TOKEN="${PROD_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${PROD_TELEGRAM_CHAT_ID}" - | bash scripts/telegram-pipeline-notify.sh start "⏸️ Pipeline pausado esperando aprobación manual para deploy a PRODUCCIÓN. Ve a Bitbucket > Pipelines > proyectosacc > master para aprobar o rechazar." - step: name: 08_deploy oidc: true deployment: production trigger: manual script: - set -euo pipefail - apt-get update -y && apt-get install -y curl unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install - aws --version - source scripts/aws-oidc-setup.sh prod # Aplicar Terraform plan previamente generado y guardado en artifact - | if [ -f terraform/prod.tfplan ]; then echo "=== Aplicando Terraform Plan previamente aprobado ===" cd terraform terraform apply -lock-timeout=5m prod.tfplan cd .. else echo "WARNING: No se encontró prod.tfplan. Terraform apply saltado." fi - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key - | ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ "thoth@${PROD_INSTANCE_IP}" \ "bash -c 'if [ -f /home/thoth/deploy/setup/deploy.sh ]; then bash /home/thoth/deploy/setup/deploy.sh; else echo \"INFO: No se encontró script de deploy. Saltando deploy backend.\"; fi'" - | if [ -f terraform/terraform-outputs.json ]; then export CLOUDFRONT_DISTRIBUTION_ID=$(python3 -c "import json; print(json.load(open('terraform/terraform-outputs.json'))['cloudfront_distribution_id']['value'])") aws cloudfront create-invalidation --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" --paths "/*" else echo "INFO: No se encontró terraform-outputs.json. Saltando invalidación de CloudFront." fi - export TELEGRAM_BOT_TOKEN="${PROD_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${PROD_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh success "CloudFront invalidado | Deploy a PROD aprobado y completado" - step: name: 09_health_check oidc: true script: - set -euo pipefail - apt-get update -y && apt-get install -y curl - source scripts/aws-oidc-setup.sh prod - echo "=== Health Check post-deploy (PROD) ===" - | export ENV=prod export HEALTH_URL="http://${PROD_INSTANCE_IP}:8080/actuator/health" export MAX_RETRIES=12 export RETRY_INTERVAL=10 # Verificar CloudFront CLOUDFRONT_DOMAIN=$(aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items[0]=='sacc.ccsoft.mx'].DomainName" --output text) if [ -n "${CLOUDFRONT_DOMAIN}" ]; then export CF_DOMAIN="${CLOUDFRONT_DOMAIN}" fi # Verificar RDS si está configurado if [ -n "${PROD_DB_HOST:-}" ]; then export DB_HOST="${PROD_DB_HOST}" fi bash scripts/health-check.sh - export TELEGRAM_BOT_TOKEN="${PROD_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${PROD_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh success "Health Check pasó - Servicios de PROD saludables"