Skip to content

Terraform

Infraetructura como código multi-plataforma

Terraform es una herramienta, con versión Open Source, para infraestructura como código que nos habilita variedad de proveedores, incluyendo AWS, GPC, OCI y más. Para definir la infraestrctura como código se utilizan archivos con formato JSON o HCL (formato propio similar a Groovy, siendo el modo más sencillo para trabajar con Terraform).

Instalación de Terraform

En Windows se puede instalar Terraform si tienes un gestor de paquetes como Chocolatey, ejecutando:

powershell
choco install terraform

También se podría usar Scoop con el comando: scoop install main/terraform

Para macOS podemos usar Homebrew ejecutando desde la línea de comandos lo siguiente:

bash
brew install hashicorp/tap/terraform

En Linux Ubuntu/Debian se puede instalar con los siguientes comandos:

bash
sudo apt update
sudo apt install -y gnupg software-properties-common
curl https://apt.releases.hashicorp.com/gpg | gpg --dearmor > hashicorp.gpg
sudo install -o root -g root -m 644 hashicorp.gpg /etc/apt/trusted.gpg.d/
sudo apt-add-repository "deb [arch=$(dpkg --print-architecture)] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt install terraform

terraform --version nos permite verificar la versión instalada

Iniciando con la CLI de Terraform

Si queremos combinar Terraform con un proveedor como AWS, debemos primero configurar la CLI de AWS, siendo prerrequisito que puedes revisar en tu entorno. También podríamos utilizar LocalStack de modo que obtengamos un entorno local simulado para AWS.

Para inicializar un proyecto con Terraform, dentro de la carpeta correspondiente, ejecutaríamos:

bash
terraform init

init se usa inicializará nuestro proyecto a partir de algún archivo main.tf (que veremos a continuación).
El flujo de Terraform se basa principalmente en los comandos: init, plan, apply, destroy

Creamos entonces un archivo main.tf para y definimos el proveedor, por ejemplo:

groovy
terraform {
    required_providers {
        aws = {
            source = "hashicorp/aws"
            version = "~> 4"
        }
    }
}

provider "aws" {
    region = "us-east-1"
}

Si conoces algo de Gradle o el lenguaje Groovy puede observarse su semejanza. Se definen ciertas secciones o bloques declarativos (terraform, provider, resource, variable, output, local, data) con ciertos valores necesarios, Terraform hace la magia.

Puede ser conveniente incluir bajo el bloque terraform la version requerida, por ejemplo: required_version = "1.4.6"

Para uso local de un plan es posible incorporar bajo el bloque terraform la especificación backend "local" {}, por ejemplo:

groovy
terraform {
    backend "local" {}
}

Se recomienda definir un archivo para las variables, por ejemplo:

groovy
variable "aws_region" {
    default = "us-east-1"
}

variable "file" {
    default = ["~/.aws/credentials"]
}

Si tuvieramos un recurso, podemos definir este en otro archivo con un contenido como el siguiente:

groovy
resource "aws_s3_bucket" "devops" {
    bucket = "x"
    for_destroy = false
    tags = {
        responsible = "Infra"
    }
}

Un ejemplo sobre un recurso para Lamdas sería:

groovy
resource "aws_lambda_function" "draft" {
    function_name = "workshop"
    role = aws_iam_role.lambdarole.arn
    runtime = "nodejs16.x"
    handler = "index.handler"
    filename = "index.zip"
    source_code_hah = filebase64sha256 ("index.zip")
}

resource "aws_iam_role" "lambdarole" {
    name = "lambda-role"
    asume_role_policy = <<POLICY
    {

    }
    POLICY
}

resource "aws_lambda_function_url" "name" {
    function_name = aws_lambda_function.draft.function_name
    authorization_type = "NONE"
}

output "aws_lambda_function_url" {
    value = aws_lambda_function_url
}

output es un tipo de variable que se usa, por ejemplo, para exponer un dato en otras configuraciones de Terraform.
Es posible validar el archivo ejecutando: terraform validate

A partir de lo anterior se debe establecer un plan de aprovisionamiento. Se puede lanzar un plan así:

bash
terraform plan -out=iac.tfplan

iac.tfplan corresponde al nombre asignado (IaC significa: Infraestructure as Code).
Comunmente suele usarse terraform plan -out=tfplan

Y para aplicarlo, es decir, para desplegar nuestro plan ejecutamos:

bash
terraform apply "iac.tfplan"

Si el nombre fuera tfplan no necesita especificarse en apply

Alternativa usando LocalStack

LocalStack provee un entorno local y simulado de AWS. Si se tiene LocalStack debidamente instalado y corriendo, es posible incorporar el uso del módulo tflocal (ejecutando: pip3 install terraform-local). Sin embargo, realizaremos la configuración manual siguiendo ciertas pautas. Para empezar definiendo como proveedor AWS en el bloque respectivo, logrando un código como el siguiente:

groovy
terraform {
    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "~> 4"
        }
    }
    backend "local" {}
}

provider "aws" {
    region     = "us-east-1"
    access_key = "test"
    secret_key = "test"

    skip_region_validation      = true
    skip_credentials_validation = true
    skip_requesting_account_id  = true
    skip_metadata_api_check     = true
    skip_get_ec2_platforms      = true
    s3_use_path_style           = true

    endpoints {
        apigateway     = "http://localhost:4566"
        apigatewayv2   = "http://localhost:4566"
        cloudformation = "http://localhost:4566"
        cloudwatch     = "http://localhost:4566"
        cloudwatchlogs = "http://localhost:4566"
        dynamodb       = "http://localhost:4566"
        iam            = "http://localhost:4566"
        lambda         = "http://localhost:4566"
        s3             = "http://s3.localhost.localstack.cloud:4566"
        secretsmanager = "http://localhost:4566"
        ses            = "http://localhost:4566"
        sns            = "http://localhost:4566"
        sqs            = "http://localhost:4566"
        sts            = "http://localhost:4566"
    }
}

Con esta configuración habilitamos el entorno para LocalStack en buena medida. Nótese que se incluye el bloque endpoints

Expresiones y Control de Flujo

  • Las variables se pueden referenciar dentro de un dato usando el signo pesos y llaves con el objeto var., por ejemplo: ${var.my_var}
  • También se pueden asignar variables directamente cuando el valor es tal cual se espera, por ejemplo: argument = var.my_var
  • Existen condicionales expresados con un operador ternario, semejante a Javascript, por ejemplo: var.my_var == "" ? "true_value" : "false_value"
  • Se puede usar el bucle o iterador for con objetos, por ejemplo así: {for x in var.array : x => upper(x)} ó [for x in var.array : upper(x)]
  • Se puede usar el bucle o iterador for con tuplas, por ejemplo así: [for k, v in var.map : upper(v)]

Usando Data en Terraform

Normalmente usamos el bloque resource para definir y establecer infraestructura con Terraform. Si lo que se busca es determinar infraestructura ya disponile o previamente aprovisionada, no necesitamos establecer un recurso sino consultarlo para asociarlo o referirlo en algún otro componente.

Veamos un ejemplo consultando una VPC en AWS, para ello tendremos un archivo main.tf con un contenido como el siguiente:

groovy
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.0.1"
    }
  }
  required_version = ">= 1.4.6"
}

provider "aws" {
  region                   = local.aws_region
  shared_config_files      = ["~/.aws/config"]
  shared_credentials_files = ["~/.aws/credentials"]
  profile                  = local.aws_profile
}

data "aws_vpcs" "my_vpc" {
  tags = {
    Name = local.vpc_name
  }
}

data "aws_subnets" "my_subnets" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpcs.my_vpc.ids[0]]
  }
}

output "vpc_id" {
  value = data.aws_vpcs.my_vpc.ids[0]
}

output "subnet_ids" {
  value = data.aws_subnets.my_subnets.ids
}

En el anterior código podemos identificar lo siguiente:

  • Usamos data para obtener el identificador de la VPC a partir del nombre
  • Con el identificador de la VPC se pueden consultar también las subredes
  • Usamos output para visualizar los valores obtenidos
  • En este ejemplo, hemos establecido en el bloque provider valores para shared_config_files, shared_credentials_files y profile
  • Nos faltaría agregar el archivo de variables locals.tf a continuación...
groovy
locals {
  aws_region     = "us-east-1"
  aws_profile    = "default"
  vpc_name       = "my-global-vpc"
}

Usando Inputs y el archivo terraform.tfvars

Podemos seguir una idea semejante al anterior ejemplo pero definiendo inputs y, en lugar de locals, y estableciendo los valores para esas variables mediente el archivo terraform.tfvars.

Veamos un ejemplo para el archivo inputs.tf:

groovy
variable "aws_region" {
  type        = string
  description = "Specified AWS Region"
}

variable "aws_profile" {
  type        = string
  description = "AWS Profile to use"
}

variable "vpc_name" {
  type        = string
  description = "Name for Global VPC"
}

variable "routing_table_name" {
  type        = string
  description = "Name for Global Routing Table"
}

variable "internet_gateway_name" {
  type        = string
  description = "Name for Global Internet Gateway"
}

variable "public_subnet_name_a" {
  type        = string
  description = "Name for Public Subnet A"
}

variable "public_subnet_name_b" {
  type        = string
  description = "Name for Public Subnet B"
}

variable "private_subnet_name_a" {
  type        = string
  description = "Name for Private Subnet A"
}

variable "private_subnet_name_b" {
  type        = string
  description = "Name for Private Subnet B"
}

En un archivo terraform.tfvars asignamos los valors así:

ini
aws_region            = "us-east-1"
aws_profile           = "default"
vpc_name              = "global-vpc"
public_subnet_name_a  = "public-net-a"
public_subnet_name_b  = "public-net-b"
private_subnet_name_a = "private-net-a"
private_subnet_name_b = "private-net-b"
routing_table_name    = "global-rtb"
internet_gateway_name = "global-igw"

El archivo main.tf tendrá un código como el siguiente:

groovy
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.0.1"
    }
  }
  required_version = ">= 1.4.6"
}

provider "aws" {
  region                   = var.aws_region
  shared_config_files      = ["~/.aws/config"]
  shared_credentials_files = ["~/.aws/credentials"]
  profile                  = var.aws_profile
}

data "aws_vpcs" "existing_vpc" {
  tags = {
    Name = var.vpc_name
  }
}

data "aws_route_tables" "existing_route_table" {
  vpc_id = data.aws_vpcs.existing_vpc.ids[0]
  tags = {
    Name = var.routing_table_name
  }
}

data "aws_internet_gateway" "existing_igw" {
  tags = {
    Name = var.internet_gateway_name
  }
}

data "aws_subnets" "existing_public_subnets_a" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpcs.existing_vpc.ids[0]]
  }
  tags = {
    Name = var.public_subnet_name_a
  }
}

data "aws_subnets" "existing_public_subnets_b" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpcs.existing_vpc.ids[0]]
  }
  tags = {
    Name = var.public_subnet_name_b
  }
}

data "aws_subnets" "existing_private_subnets_a" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpcs.existing_vpc.ids[0]]
  }
  tags = {
    Name = var.private_subnet_name_a
  }
}

data "aws_subnets" "existing_private_subnets_b" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpcs.existing_vpc.ids[0]]
  }
  tags = {
    Name = var.private_subnet_name_b
  }
}

output "vpc_id" {
  value = data.aws_vpcs.existing_vpc.ids[0]
}

output "rtb_id" {
  value = data.aws_route_tables.existing_route_table.ids[0]
}

output "igw_id" {
  value = data.aws_internet_gateway.existing_igw.id
}

output "public_subnet_ids" {
  value = [data.aws_subnets.existing_public_subnets_a.ids[0],data.aws_subnets.existing_public_subnets_b.ids[0]]
}

output "private_subnet_ids" {
  value = [data.aws_subnets.existing_private_subnets_a.ids[0],data.aws_subnets.existing_private_subnets_b.ids[0]]
}

Ejemplo de Terraform con AWS AppRunner

En el siguiene ejercicio se configura en el archivo main.f un "backend" (el archivo de almacenamiento de terraform) con AWS S3. Además, usaremos AWS App Runner como servicio Serverless para una aplicación cuyos fuentes se encuentran en GitHub.

groovy
provider "aws" {
  region = "us-east-1"
}

terraform {
  backend "s3" {
    bucket                  = "tfstate-demo-2023"
    key                     = "terraform.tfstate"
    region                  = "us-east-1"
    encrypt                 = true
    shared_credentials_file = "./iac/.secretaws"
  }
}

resource "aws_iam_role" "apprunner_role" {
  name = "AppRunnerRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "apprunner.amazonaws.com",
        },
      },
    ],
  })
}

resource "aws_apprunner_service" "my_drone_service" {
  service_name = "MyDroneService"

  source_configuration {
    authentication_configuration {
      connection_arn = "arn:aws:apprunner:us-east-1:117979987706:connection/cicd-repo/d940c059ccdd4466a82f95b50e4cdec3"
    }

    auto_deployments_enabled = false

    code_repository {
      repository_url = "https://github.com/kaesar/cicddemo"

      source_code_version {
        type  = "BRANCH"
        value = "main"
      }

      code_configuration {
        configuration_source = "API"

        code_configuration_values {
          build_command = "npm install && npm run build"
          start_command = "npm start"
          port          = "3000"
          runtime       = "NODEJS_16"
        }
      }
    }
  }

  health_check_configuration {
    path = "/health"
  }
}

Si usamos la imagen desde AWS ECR, nuestro ejemplo de AWS AppRunner quedaría como el siguiente código:

groovy
provider "aws" {
  region = "us-east-1"
}

terraform {
  required_version = "~>1.4"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket                  = "tfstate-demo-2023"
    key                     = "terraform.tfstate"
    region                  = "us-east-1"
    encrypt                 = true
    shared_credentials_file = "./iac/.secretaws"
  }
}

data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["apprunner.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "apprunner_role" {
  name = "AppRunnerRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "apprunner_role_policy" {
  role       = aws_iam_role.apprunner_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

resource "aws_apprunner_service" "my_drone_service" {
  depends_on   = [aws_iam_role.apprunner_role]
  service_name = "MyDroneService"

  source_configuration {
    authentication_configuration {
      access_role_arn = aws_iam_role.apprunner_role.arn
    }

    auto_deployments_enabled = false

    image_repository {
      image_identifier      = "117979987706.dkr.ecr.us-east-1.amazonaws.com/cicd-hub:v1"
      image_repository_type = "ECR"
      image_configuration {
        port = "3000"
      }
    }
  }

  health_check_configuration {
    path = "/health"
  }
}

output "service_url" {
  value = aws_apprunner_service.my_drone_service.service_url
}

Es posible que se requiera usar terraform apply dos (2) veces para crear el servicio

Ejemplo de Terraform con ECS/Fargate

Si queremos usar en lugar de App Runner el servicio ECS/Fargate. Dado que se requieren involucrar más componentes podríamos considerar los siguientes archivos:

  • main.tf
  • locals.tf
  • inputs.tf
  • terraform.tfvars
  • ecs_cluster.tf
  • ecs_task_definition.tf
  • vpc.tf
  • security-groups.tf
  • iam.tf

Veamos a continuación el contenido de ejemplo del archivo main.tf

groovy
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.0.1"
    }
  }
  required_version = ">= 1.4.6"
}

provider "aws" {
  region                   = var.aws_region
  shared_config_files      = ["~/.aws/config"]
  shared_credentials_files = ["~/.aws/credentials"]
  profile                  = var.aws_profile
  default_tags {
    tags = var.tags
  }
}

En este caso usamos provider (AWS) con aws_profile mediante una variable.

Las varialbes de entrada las definimos en inputs.tf así:

groovy
variable "aws_account_id" {
  description = "AWS Account ID"
  type        = string
}

variable "aws_region" {
  type        = string
  description = "Specified AWS Region"
}

variable "aws_profile" {
  type        = string
  description = "AWS Profile to use"
}

variable "tags" {
  type        = map(string)
  description = "Default tags to be used in the project"
}

variable "project_name" {
  type        = string
  description = "Specified Project Name"
}

## VPC
variable "vpc_block_cidr" {
  type        = string
  description = "Specified CIDR Block for Global VPC"
}

variable "availability_zones" {
  type        = list(string)
  description = "List of availability zones"
}

variable "public_subnets" {
  type        = list(string)
  description = "List of Public Subnets"
}

En locals.tf establecemos lo siguiente:

groovy
locals {
  stack_name      = var.project_name
  container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myservice:v1"
}

Podemos asignar los valores a las variables con el archivo terraform.tfvars así:

ini
# Default variables
aws_account_id = "123456789012"
aws_region     = "us-east-1"
aws_profile    = "Developer-Role"
tags = {
  "Environment" = "develop"
  "IaC"         = "Terraform CLI"
  "Owner"       = "user"
  "Service"     = "ECS"
}
project_name = "example"

## VPC
vpc_block_cidr     = "172.31.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
public_subnets     = ["172.31.80.0/20", "172.31.16.0/20"]

En ecs_cluster tendríamos un código como el siguiente:

groovy
resource "aws_ecs_cluster" "ecs_cluster" {
  name       = "${local.stack_name}-ecs-cluster"
  depends_on = [aws_internet_gateway.igw]
  setting { # Need to be Enable to get Metrics from Task Executions
    name  = "containerInsights"
    value = "enabled"
  }
}

En ecs_task_definition...

groovy
resource "aws_ecs_task_definition" "ecs_task_definition" {
  family                   = "${local.stack_name}-ecs-task-definition"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 1024
  memory                   = 2048
  task_role_arn            = aws_iam_role.ecs_task_role.arn
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  container_definitions    = <<TASK_DEFINITION
[
  {
    "name": "webcontainer",
    "image": "${local.container_image}",
    "cpu": 1024,
    "memory": 2048,
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${aws_cloudwatch_log_group.cw_log_grp_update_client_ecs.id}",
        "awslogs-region": "${var.aws_region}",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "portMappings": [
      {
        "containerPort": 8080,
        "hostPort": 8080
      }
    ]
  }
]
TASK_DEFINITION

}

En vpc...

groovy
# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_block_cidr
  enable_dns_support   = "true"
  enable_dns_hostnames = "true"

  tags = {
    Name = "${local.stack_name}-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${local.stack_name}-igw"
  }
}

# Subnets
resource "aws_subnet" "main_subnet_public" {
  vpc_id                  = aws_vpc.main.id
  count                   = length(var.public_subnets)
  cidr_block              = element(var.public_subnets, count.index)
  availability_zone       = element(var.availability_zones, count.index)
  map_public_ip_on_launch = true
  depends_on              = [aws_internet_gateway.igw]
  tags = {
    Name = "${local.stack_name}-subnet-public"
  }
}

# Route Tables
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${local.stack_name}-routing-table-public"
  }
}

resource "aws_route" "public" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

resource "aws_route_table_association" "public" {
  count          = length(var.public_subnets)
  subnet_id      = element(aws_subnet.main_subnet_public.*.id, count.index)
  route_table_id = aws_route_table.public.id
}

En security-groups...

groovy
resource "aws_security_group" "sg" {
  name   = "${local.stack_name}-sg"
  vpc_id = aws_vpc.main.id
  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]

  }
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

En iam...

groovy
resource "aws_iam_role" "ecs_task_execution_role" {
  name                = "${local.stack_name}-ecs-task-execution-role"
  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      },
    ]
  })
}

resource "aws_iam_role" "ecs_task_role" {
  name = "${local.stack_name}-ecs-task-role"
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
  ]
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role" "eventbridge_invoke_ecs_role" {
  name                = "${local.stack_name}-eventbridge-invoke-ecs-role"
  managed_policy_arns = [aws_iam_policy.eventbridge_invoke_ecs_policy.arn]

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "events.amazonaws.com"
        }
      },
    ]
  })
}

resource "aws_iam_policy" "eventbridge_invoke_ecs_policy" {
  name = "${local.stack_name}-eventbridge-invoke-ecs-policy"

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Action" : [
          "ecs:RunTask"
        ],
        "Resource" : [
          "${aws_ecs_task_definition.ecs_task_definition.arn}:*",
          "${aws_ecs_task_definition.ecs_task_definition.arn}"
        ],
        "Condition" : {
          "ArnLike" : {
            "ecs:cluster" : "${aws_ecs_cluster.ecs_cluster.arn}"
          }
        }
      },
      {
        "Effect" : "Allow",
        "Action" : "iam:PassRole",
        "Resource" : [
          "*"
        ],
        "Condition" : {
          "StringLike" : {
            "iam:PassedToService" : "ecs-tasks.amazonaws.com"
          }
        }
      }
    ]
  })
}

Ejemplo de Terraform con AWS EventBridge

En el siguiente ejercicio usaremos el archivo main.tf, acompañado de un archivo de variables inputs.tf, para definir reglas con EventBridge y API Destination. Veamos el archivo main.tf:

groovy
terraform {
  #required_version = "1.4.4"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.59.0"
    }
  }
  backend "local" {}
}

provider "aws" {
  region = var.aws_region
}

resource "aws_iam_role" "eventbridge_role" {
  name = "eventbridge-role"
  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [{
      "Effect" : "Allow",
      "Principal" : {
        "Service" : "events.amazonaws.com"
      },
      "Action" : "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_policy" "eventbridge_policy" {
  name = "eventbridge-policy"

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Action" : [
          "events:PutRule",
          "events:PutTargets",
          "events:CreateConnection",
          "events:CreateApiDestination"
        ],
        "Resource" : "*"
      },
      {
        "Effect" : "Allow",
        "Action" : "events:InvokeApiDestination",
        "Resource" : [
          "arn:aws:events:us-east-1:${var.aws_id}:api-destination/${var.api_destination_name}-${var.env_name}/*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "eventbridge_policy_attachment" {
  role       = aws_iam_role.eventbridge_role.name
  policy_arn = aws_iam_policy.eventbridge_policy.arn
}

resource "aws_cloudwatch_event_rule" "events_put_rule" {
  name           = var.rule_name
  description    = "Regla para manejar evento"
  event_bus_name = var.bus_name
  event_pattern = jsonencode({
    "detail-type" : [var.event_name]
  })
  tags = {
    "Env"   = var.env_name
    "Owner" = "Platform"
  }
}

resource "aws_cloudwatch_event_connection" "events_create_connection" {
  name               = "${var.connection_name}-${var.env_name}"
  description        = "Conexion para el API Destination evento 'cancelCard'"
  authorization_type = "API_KEY"

  auth_parameters {
    api_key {
      key   = "x-api-key"
      value = var.api_key
    }
  }
}

resource "aws_cloudwatch_event_api_destination" "events_create_api_destination" {
  name                = "${var.api_destination_name}-${var.env_name}"
  description         = "API Destination para 'cancelCard'"
  connection_arn      = aws_cloudwatch_event_connection.events_create_connection.arn
  invocation_endpoint = var.api_destination_endpoint
  http_method         = "POST"
}

resource "aws_cloudwatch_event_target" "aws_events_put_targets" {
  event_bus_name = var.bus_name
  rule           = aws_cloudwatch_event_rule.events_put_rule.name
  target_id      = "6c1ea055-a48c-44f0-a904-c3311512f244"
  arn            = aws_cloudwatch_event_api_destination.events_create_api_destination.arn
  role_arn       = "arn:aws:iam::${var.aws_id}:role/physicalcard-token-event-bridge-invoke-api-role-${var.env_name}"
}

Y en el archivo inputs.tf tendremos un contenido como el siguiente:

groovy
variable "env_name" {
  description = "Environment name"
  type        = string
  default     = "qa"
}

variable "bus_name" {
  description = "Event bus name"
  type        = string
  default     = "Bus-QA"
}

variable "rule_name" {
  description = "Rule name"
  type        = string
  default     = "physicalcard-token-event-message-cancelcard"
}

variable "event_name" {
  description = "Event name"
  type        = string
  default     = "PHYSICALCARD.MPF0040_TOKEN_MANAGER.CANCELCARD"
}

variable "connection_name" {
  description = "Connection name"
  type        = string
  default     = "conn-physicalcard-token-manager-cancelcard"
}

variable "api_destination_name" {
  description = "API Destination name"
  type        = string
  default     = "api-physicalcard-token-manager-cancelcard"
}

variable "api_destination_endpoint" {
  description = "API Destination endpoint"
  type        = string
  // default     = "https://subdomain.amazonaws.com/api/action"
}

variable "api_key" {
  description = "API Key (de API Gateway)"
  type        = string
}

variable "aws_id" {
  description = "AWS ID"
  type        = string
  default     = "123456789123"
}

variable "aws_region" {
  description = "Region AWS"
  type        = string
  default     = "us-east-1"
}