From cbea3e932b659a1dec12b59365794a9a6c47815d Mon Sep 17 00:00:00 2001 From: Evert Daniel Romero Garrido Date: Tue, 14 Apr 2026 19:40:57 -0600 Subject: [PATCH] feat(ci): integra Terraform en pipeline de Bitbucket Pipelines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agrega paso 03_terraform para DEV y PROD con init, plan y apply - Crea backend.dev.hcl para configuración explícita de estado DEV - Refactoriza Route53/ACM en main.tf para soportar PROD cross-account usando count condicional sin romper estado de DEV - Descomenta provider aws.route53 en provider.tf - Añade domain_name faltante en prod.tfvars y confirma dev.tfvars - Corrige output route53_record para recursos con count - Elimina errored.tfstate corrupto local - Incluye permiso sts:AssumeRole en IAM policy para Route53 cross-account --- bitbucket-pipelines.yml | 48 +++++++++++++---- docs/iam-policy-ci-cd-proyectosacc.json | 8 +++ terraform/backend.dev.hcl | 5 ++ terraform/environments/dev.tfvars | 1 + terraform/environments/prod.tfvars | 1 + terraform/main.tf | 70 +++++++++++++++++++------ terraform/outputs.tf | 2 +- terraform/provider.tf | 19 +++++++ 8 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 terraform/backend.dev.hcl diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 616f130..e7943e0 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -1,8 +1,8 @@ # =============================================================================================================== # bitbucket-pipelines.yml - Pipeline CI/CD para proyectosacc # Descripción: -# Pipeline de 7 pasos estándar de CCsoft para desplegar el frontend -# React (S3+CloudFront) y la API backend (EC2) de SACC. +# Pipeline de 7 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 # =============================================================================================================== @@ -42,7 +42,7 @@ pipelines: name: 01_image-setup script: - set -euo pipefail - - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk awscli + - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk awscli wget unzip - mkdir -p ~/.ssh - echo "${DEV_SSH_PRIVATE_KEY_THOTH_PROYECTOSACC}" | base64 -d > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key @@ -59,11 +59,24 @@ pipelines: - git clone "https://x-token-auth:${BITBUCKET_PASSWORD}@bitbucket.org/ccsoft1/ci-cd-saac4.git" ci-cd-saac4 - step: - name: 03_dependencies + name: 03_terraform script: - set -euo pipefail - - npm ci - - ./gradlew dependencies + - 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_ACCESS_KEY_ID="${DEV_AWS_ACCESS_KEY_ID}" + - export AWS_SECRET_ACCESS_KEY="${DEV_AWS_SECRET_ACCESS_KEY}" + - export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-mx-central-1}" + - terraform init -backend-config=backend.dev.hcl + - terraform plan -var-file=environments/dev.tfvars -var="db_password=${DEV_DB_PASSWORD}" -out=dev.tfplan + - terraform apply -auto-approve dev.tfplan + - terraform output -json > terraform-outputs.json + - cat terraform-outputs.json + artifacts: + - terraform/terraform-outputs.json - step: name: 04_build @@ -108,6 +121,7 @@ pipelines: -o StrictHostKeyChecking=no \ "${DEV_SERVER_USER_PROYECTOSACC:-thoth}@${DEV_SERVER_IP_PROYECTOSACC}" \ "bash /home/thoth/deploy/setup/deploy.sh" + - 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 "/*" - bash ci-cd-commons/telegram_alert.sh "✅ Deploy DEV de proyectosacc completado exitosamente" @@ -116,7 +130,7 @@ pipelines: name: 01_image-setup script: - set -euo pipefail - - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk awscli + - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk awscli wget unzip - mkdir -p ~/.ssh - echo "${PROD_SSH_PRIVATE_KEY_THOTH_PROYECTOSACC}" | base64 -d > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key @@ -133,11 +147,24 @@ pipelines: - git clone "https://x-token-auth:${BITBUCKET_PASSWORD}@bitbucket.org/ccsoft1/ci-cd-saac4.git" ci-cd-saac4 - step: - name: 03_dependencies + name: 03_terraform script: - set -euo pipefail - - npm ci - - ./gradlew dependencies + - 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_ACCESS_KEY_ID="${PROD_AWS_ACCESS_KEY_ID}" + - export AWS_SECRET_ACCESS_KEY="${PROD_AWS_SECRET_ACCESS_KEY}" + - export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-mx-central-1}" + - terraform init -backend-config=backend.prod.hcl + - terraform plan -var-file=environments/prod.tfvars -var="db_password=${PROD_DB_PASSWORD}" -out=prod.tfplan + - terraform apply -auto-approve prod.tfplan + - terraform output -json > terraform-outputs.json + - cat terraform-outputs.json + artifacts: + - terraform/terraform-outputs.json - step: name: 04_build @@ -182,5 +209,6 @@ pipelines: -o StrictHostKeyChecking=no \ "${PROD_SERVER_USER_PROYECTOSACC:-thoth}@${PROD_SERVER_IP_PROYECTOSACC}" \ "bash /home/thoth/deploy/setup/deploy.sh" + - 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 "/*" - bash ci-cd-commons/telegram_alert.sh "✅ Deploy PROD de proyectosacc completado exitosamente" diff --git a/docs/iam-policy-ci-cd-proyectosacc.json b/docs/iam-policy-ci-cd-proyectosacc.json index be995f8..29d1c01 100644 --- a/docs/iam-policy-ci-cd-proyectosacc.json +++ b/docs/iam-policy-ci-cd-proyectosacc.json @@ -153,6 +153,14 @@ ], "Resource": "*" }, + { + "Sid": "AssumeRoute53CrossAccountRole", + "Effect": "Allow", + "Action": [ + "sts:AssumeRole" + ], + "Resource": "arn:aws:iam::262270938827:role/Route53ProyectosaccCrossAccountRole" + }, { "Sid": "ACMCertificateManagement", "Effect": "Allow", diff --git a/terraform/backend.dev.hcl b/terraform/backend.dev.hcl new file mode 100644 index 0000000..07e95ce --- /dev/null +++ b/terraform/backend.dev.hcl @@ -0,0 +1,5 @@ +bucket = "ccsoft-terraform-state" +key = "proyectosacc/terraform.tfstate" +region = "mx-central-1" +encrypt = true +dynamodb_table = "terraform-locks" diff --git a/terraform/environments/dev.tfvars b/terraform/environments/dev.tfvars index 2cd0259..b078c05 100644 --- a/terraform/environments/dev.tfvars +++ b/terraform/environments/dev.tfvars @@ -21,4 +21,5 @@ db_username = "sacc_admin_dev" db_password = "" s3_frontend_bucket = "ccsoft-proyectosacc-frontend-dev" s3_artifacts_bucket = "ccsoft-proyectosacc-artifacts-dev" +domain_name = "dev-sacc.ccsoft.mx" cloudfront_price_class = "PriceClass_100" diff --git a/terraform/environments/prod.tfvars b/terraform/environments/prod.tfvars index 53994ca..a3fb23d 100644 --- a/terraform/environments/prod.tfvars +++ b/terraform/environments/prod.tfvars @@ -21,4 +21,5 @@ db_username = "sacc_admin_prod" db_password = "" s3_frontend_bucket = "ccsoft-proyectosacc-frontend-prod" s3_artifacts_bucket = "ccsoft-proyectosacc-artifacts-prod" +domain_name = "sacc.ccsoft.mx" cloudfront_price_class = "PriceClass_100" diff --git a/terraform/main.tf b/terraform/main.tf index a981b51..1abf1fc 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -445,38 +445,64 @@ resource "aws_acm_certificate" "main" { } resource "aws_route53_record" "cert_validation" { - for_each = { - for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => { - name = dvo.resource_record_name - record = dvo.resource_record_value - type = dvo.resource_record_type - } - } - + count = var.environment != "prod" ? 1 : 0 allow_overwrite = true - name = each.value.name - records = [each.value.record] + name = tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_value] ttl = 60 - type = each.value.type - zone_id = data.aws_route53_zone.main.zone_id + type = tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_type + zone_id = local.route53_zone_id +} + +resource "aws_route53_record" "cert_validation_prod" { + provider = aws.route53 + count = var.environment == "prod" ? 1 : 0 + allow_overwrite = true + name = tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_type + zone_id = local.route53_zone_id } resource "aws_acm_certificate_validation" "main" { provider = aws.us_east_1 certificate_arn = aws_acm_certificate.main.arn - validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] + validation_record_fqdns = local.cert_validation_fqdns } # ------------------------------------------------------------------------------- # Route 53 # ------------------------------------------------------------------------------- data "aws_route53_zone" "main" { - name = "ccsoft.mx" + count = var.environment != "prod" ? 1 : 0 + name = var.domain_name private_zone = false } +data "aws_route53_zone" "main_prod" { + provider = aws.route53 + count = var.environment == "prod" ? 1 : 0 + name = var.domain_name + private_zone = false +} + +locals { + route53_zone_id = coalesce( + try(data.aws_route53_zone.main[0].zone_id, ""), + try(data.aws_route53_zone.main_prod[0].zone_id, "") + ) + + cert_validation_fqdns = compact(try( + [aws_route53_record.cert_validation[0].fqdn], + [aws_route53_record.cert_validation_prod[0].fqdn], + [] + )) +} + resource "aws_route53_record" "main" { - zone_id = data.aws_route53_zone.main.zone_id + count = var.environment != "prod" ? 1 : 0 + zone_id = local.route53_zone_id name = var.domain_name type = "A" @@ -487,6 +513,20 @@ resource "aws_route53_record" "main" { } } +resource "aws_route53_record" "main_prod" { + provider = aws.route53 + count = var.environment == "prod" ? 1 : 0 + zone_id = local.route53_zone_id + name = var.domain_name + type = "A" + + alias { + name = aws_cloudfront_distribution.main.domain_name + zone_id = aws_cloudfront_distribution.main.hosted_zone_id + evaluate_target_health = false + } +} + # ------------------------------------------------------------------------------- # CloudFront Distribution # ------------------------------------------------------------------------------- diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 13aa618..8edbe49 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -43,7 +43,7 @@ output "cloudfront_distribution_id" { output "route53_record" { description = "Registro DNS creado en Route 53" - value = aws_route53_record.main.name + value = try(aws_route53_record.main[0].name, aws_route53_record.main_prod[0].name, "") } output "acm_certificate_arn" { diff --git a/terraform/provider.tf b/terraform/provider.tf index 2b9ad76..91f6912 100644 --- a/terraform/provider.tf +++ b/terraform/provider.tf @@ -45,3 +45,22 @@ provider "aws" { } } } + +# Provider para Route 53 en cuenta cross-account (262270938827) +# Solo se usa en PROD mediante count condicional en los recursos de Route 53. +provider "aws" { + alias = "route53" + region = "us-east-1" + + assume_role { + role_arn = "arn:aws:iam::262270938827:role/Route53ProyectosaccCrossAccountRole" + } + + default_tags { + tags = { + Project = var.project_name + ManagedBy = "terraform" + Environment = var.environment + } + } +}