Implementación End-to-End de AWS RDS con Bastion Host Usando Terraform
Este artículo explora cómo automatizar la configuración de una infraestructura segura en AWS utilizando Terraform. Se cubrirá la creación de una VPC, subredes públicas y privadas, una base de datos MySQL RDS y un Bastion host para un acceso seguro. El objetivo es proporcionar un entorno de base de datos fiable para procesos ETL posteriores, siguiendo las mejores prácticas de ingeniería de datos y DevOps. La información de este blog viene principalmente de https://towardsdatascience.com/end-to-end-aws-rds-setup-with-bastion-host-using-terraform/.
Introducción
En cualquier arquitectura de datos, las fuentes de datos, especialmente las bases de datos, son la columna vertebral. Para simular un pipeline realista, se necesita un entorno de base de datos seguro y fiable que sirva como fuente de verdad para los trabajos ETL (Extract, Transform, Load) posteriores.
En lugar de provisionar esto manualmente, la automatización con Terraform permite no solo ahorrar tiempo, sino también asegurar que el entorno pueda ser recreado, escalado o destruido fácilmente con un solo comando, tal como se haría en un entorno de producción. Esto es crucial, especialmente si se está trabajando con la capa gratuita de AWS, ya que la automatización asegura que no se olvide ningún recurso que pueda generar costos.
Prerrequisitos
Para seguir este proyecto, se necesitan las siguientes herramientas y configuraciones:
- Terraform: Instalado.
- AWS CLI: Instalado y configurado con un usuario IAM con permisos para crear los recursos necesarios. Es importante mencionar que se recomienda no utilizar la política
AdministratorAccess
, y crear una política a la medida para limitar los permisos del usuario. - Par de claves SSH de AWS: Necesario para acceder al Bastion host.
- Entorno Unix: (Linux/macOS o WSL para Windows) para compatibilidad con el script y los comandos de Terraform.
Arquitectura General
Este proyecto provisiona un entorno AWS completo, similar a un entorno de producción, utilizando Terraform. Los recursos que se crearán incluyen:
Red
- Una VPC (Virtual Private Cloud) personalizada con un bloque CIDR (10.0.0.0/16). Esto es como construir nuestra propia red privada dentro de AWS.
- Dos subredes privadas en diferentes Zonas de Disponibilidad (para la instancia RDS). Estas subredes son como habitaciones seguras dentro de la red principal.
- Una subred pública (para el Bastion host). Esta subred permite el acceso a internet para el Bastion host.
- Internet Gateway y Route Tables para el enrutamiento de la subred pública. Actúan como las puertas y los mapas de la red.
- Un DB Subnet Group para el despliegue de RDS Multi-AZ.
Cómputo
- Una instancia EC2 Bastion en la subred pública. El Bastion host sirve como un punto de entrada seguro a la red privada. Es como el guardia de seguridad que permite el acceso a la base de datos.
- Provisionado con un grupo de seguridad personalizado que permite solo acceso SSH (puerto 22).
Base de Datos
- Una instancia MySQL RDS (Relational Database Service).
- Desplegada en subredes privadas (no accesible desde la internet pública).
- Configurada con un grupo de seguridad dedicado que permite el acceso solo desde el Bastion host.
Seguridad
- Grupos de seguridad:
- Bastion SG: Permite SSH entrante (puerto 22) desde la IP de tu máquina local.
- RDS SG: Permite MySQL entrante (puerto 3306) desde el SG del Bastion host.
Automatización
- Un script de configuración (
setup.sh
) que:- Exporta las variables de Terraform.
Diseño Modular con Terraform
La infraestructura se divide en módulos (network, bastion, rds) para permitir la reutilización, escalabilidad y prueba independiente de los componentes. Como analogía, podemos pensar en un conjunto de legos, donde cada módulo es una pieza que puede ser combinada y reutilizada en diferentes construcciones.
El siguiente diagrama ilustra cómo Terraform estructura las dependencias entre los diferentes componentes de la infraestructura:
- Network: Define la VPC y las subredes.
- Bastion: Define la instancia EC2 que actúa como puerta de enlace segura.
- RDS: Define la instancia de base de datos MySQL.
Esta visualización ayuda a verificar que:
- Los recursos están conectados correctamente (ej., la instancia RDS depende de las subredes privadas).
- Los módulos están aislados pero son interoperables (ej., network, bastion y rds).
- No hay dependencias circulares.
Estructura del Proyecto
Para mantener la configuración modular, el proyecto se estructura de la siguiente manera:
.
├── data
│ └── mysqlsampledatabase.sql # Datos de ejemplo para importar a la base de datos RDS
├── scripts
│ └── setup.sh # Script para exportar variables, obtener valores dinámicos y subir scripts de Glue
└── terraform
├── modules # Módulos de infraestructura reutilizables
│ ├── bastion
│ │ ├── compute.tf # Define la configuración de la instancia EC2 para el Bastion host
│ │ ├── network.tf # Usa data sources para referenciar la subred pública y VPC existentes
│ │ ├── outputs.tf # Exporta la IP pública del Bastion host
│ │ └── variables.tf # Variables de entrada requeridas por el módulo Bastion
│ ├── network
│ │ ├── network.tf # Provisiona VPC, subredes públicas/privadas, Internet gateway y route tables
│ │ ├── outputs.tf # Exporta VPC ID, subnet IDs y route table IDs
│ │ └── variables.tf # Variables de entrada como CIDR blocks y availability zones
│ └── rds
│ ├── network.tf # Define el DB subnet group usando los IDs de las subredes privadas
│ ├── outputs.tf # Exporta el endpoint de RDS y el security group
│ ├── rds.tf # Provisiona una instancia MySQL RDS dentro de las subredes privadas
│ └── variables.tf # Variables de entrada como el nombre de la DB, usuario, contraseña y tamaño de la instancia
└── rds-bastion # Configuración raíz de Terraform
├── backend.tf # Configura el backend de Terraform (ej., ubicación del archivo de estado)
├── main.tf # Archivo principal que conecta todos los módulos
├── outputs.tf # Consolida y re-exporta los outputs de los módulos
├── provider.tf # Define el proveedor de AWS y la versión requerida
└── variables.tf # Variables del proyecto que se pasan a los módulos
El archivo main.tf
en el directorio rds-bastion
actúa como el orquestador, uniendo los componentes principales: la red, la base de datos RDS y el bastion host. Cada módulo se invoca con las entradas requeridas, definidas en variables.tf
o a través de variables de entorno (TF_VAR_*
).
module "network" {
source = "../modules/network"
region = var.region
project_name = var.project_name
availability_zone_1 = var.availability_zone_1
availability_zone_2 = var.availability_zone_2
vpc_cidr = var.vpc_cidr
public_subnet_cidr = var.public_subnet_cidr
private_subnet_cidr_1 = var.private_subnet_cidr_1
private_subnet_cidr_2 = var.private_subnet_cidr_2
}
module "bastion" {
source = "../modules/bastion"
region = var.region
vpc_id = module.network.vpc_id
public_subnet_1 = module.network.public_subnet_id
availability_zone_1 = var.availability_zone_1
project_name = var.project_name
instance_type = var.instance_type
key_name = var.key_name
ami_id = var.ami_id
}
module "rds" {
source = "../modules/rds"
region = var.region
project_name = var.project_name
vpc_id = module.network.vpc_id
private_subnet_1 = module.network.private_subnet_id_1
private_subnet_2 = module.network.private_subnet_id_2
availability_zone_1 = var.availability_zone_1
availability_zone_2 = var.availability_zone_2
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
bastion_sg_id = module.bastion.bastion_sg_id
}
En esta configuración modular, cada componente de la infraestructura está débilmente acoplado, pero conectado a través de entradas y salidas bien definidas.
Después de provisionar la VPC y las subredes en el módulo de red, se recuperan sus IDs usando sus salidas, y se pasan como variables de entrada a otros módulos como rds y bastion. Esto evita el hardcoding y permite a Terraform resolver dinámicamente las dependencias y construir el grafo de dependencias internamente.
En algunos casos, como dentro del módulo bastion, también se utilizan data sources para referenciar recursos existentes creados por módulos anteriores, en lugar de recrearlos o duplicarlos.
La dependencia entre módulos se basa en la correcta definición y exposición de salidas de módulos creados previamente. Estas salidas se pasan como variables de entrada a módulos dependientes, permitiendo a Terraform construir un grafo de dependencias interno y orquestar el orden correcto de creación.
Por ejemplo, el módulo de red expone el VPC ID y los subnet IDs usando outputs.tf
. Estos valores son luego consumidos por módulos downstream como rds y bastion a través del archivo main.tf
de la configuración raíz.
Ejemplo Práctico: Outputs y Variables
Dentro de modules/network/outputs.tf
:
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
Dentro de modules/bastion/variables.tf
:
variable "vpc_id" {
description = "ID of the VPC"
type = string
}
Dentro de modules/bastion/network.tf
:
data "aws_vpc" "main" {
id = var.vpc_id
}
Para provisionar la instancia RDS, se crean dos subredes privadas en diferentes Zonas de Disponibilidad, ya que AWS requiere al menos dos subredes en AZs separadas para configurar un grupo de subredes de DB.
Aunque se cumple este requisito para una configuración correcta, se puede deshabilitar el despliegue Multi-AZ durante la creación de RDS para mantenerse dentro de los límites de la capa gratuita de AWS y evitar costos adicionales. Esta configuración simula un diseño de red de nivel de producción, mientras que sigue siendo rentable para el desarrollo y las pruebas.
Flujo de Despliegue
Con todos los módulos correctamente conectados a través de entradas y salidas, y la lógica de la infraestructura encapsulada en bloques reutilizables, el siguiente paso es automatizar el proceso de provisionamiento. En lugar de pasar variables manualmente cada vez, se utiliza un script auxiliar setup.sh
para exportar las variables de entorno necesarias (TF_VAR_*
).
Una vez que se ejecuta el script de configuración, desplegar la infraestructura se vuelve tan simple como ejecutar unos pocos comandos de Terraform.
source scripts/setup.sh
cd terraform/rds-bastion
terraform init
terraform plan
terraform apply
El script setup.sh
exporta automáticamente las variables de entorno requeridas usando la convención de nombres TF_VAR_
. Terraform detecta automáticamente las variables con este prefijo, evitando el hardcoding de valores en los archivos .tf
o la necesidad de ingresar manualmente los valores cada vez.
#!/bin/bash
set -e
export de_project="your_project_name"
export AWS_DEFAULT_REGION="your_aws_region"
# Define the variables to manage
declare -A TF_VARS=(
["TF_VAR_project_name"]="$de_project"
["TF_VAR_region"]="$AWS_DEFAULT_REGION"
["TF_VAR_availability_zone_1"]="us-east-1a"
["TF_VAR_availability_zone_2"]="us-east-1b"
["TF_VAR_ami_id"]=""
["TF_VAR_key_name"]=""
["TF_VAR_db_username"]=""
["TF_VAR_db_password"]=""
["TF_VAR_db_name"]=""
)
for var in "${!TF_VARS[@]}"; do
value="${TF_VARS[$var]}"
if grep -q "^export $var=" "$HOME/.bashrc"; then
sed -i "s|^export $var=.*|export $var=$value|" "$HOME/.bashrc"
else
echo "export $var=$value" >> "$HOME/.bashrc"
fi
done
# Source updated .bashrc to make changes available immediately in this shell
source "$HOME/.bashrc"
Después de ejecutar terraform apply
, Terraform provisionará todos los recursos definidos: VPC, subredes, route tables, instancia RDS y Bastion host. Una vez que el proceso se completa con éxito, verá valores de salida similares a los siguientes:
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
Outputs:
bastion_public_ip = "<Bastion EC2 Public IP>"
bastion_sg_id = "<Security Group ID for Bastion Host>"
db_endpoint = "<RDS Endpoint>:3306"
instance_public_dns = "<EC2 Public DNS>"
rds_db_name = "<Database Name>"
vpc_id = "<VPC ID>"
vpc_name = "<VPC Name>"
Estos outputs se definen en los archivos outputs.tf
de sus módulos y se re-exportan en el módulo raíz (rds-bastion/outputs.tf
). Son cruciales para:
- Conectarse al Bastion Host vía SSH.
- Conectarse de forma segura a la instancia RDS privada.
- Validar la creación de recursos.
Conexión a la RDS Vía Bastion Host e Inicialización de la Base de Datos
Una vez que la infraestructura está provisionada, el siguiente paso es poblar la base de datos MySQL alojada en la instancia RDS. Dado que la base de datos está dentro de una subred privada, no se puede acceder directamente desde la máquina local. En su lugar, se utilizará la instancia EC2 Bastion como un jump host para:
- Transferir el dataset de ejemplo (
mysqlsampledatabase.sql
) al Bastion. - Conectarse desde el Bastion a la instancia RDS.
- Importar los datos SQL para inicializar la base de datos.
Se puede mover dos directorios arriba desde el directorio principal de Terraform y enviar el contenido SQL al EC2 remoto (Bastion) después de leer el archivo SQL local dentro del directorio de datos.
cd ../..
cat data/mysqlsampledatabase.sql | ssh -i your-key.pem ec2-user@<BASTION_PUBLIC_IP> 'cat > ~/mysqlsampledatabase.sql'
Una vez que el dataset se copia a la instancia EC2 Bastion, el siguiente paso es conectarse al máquina remota vía SSH:
ssh -i ~/.ssh/new-key.pem ec2-user@<BASTION_PUBLIC_IP>
Después de conectarse, se puede usar el cliente MySQL (ya instalado si usó mariadb105 en su configuración de EC2) para importar el archivo SQL a su base de datos RDS:
mysql -h <DATABASE_ENDPOINT> -P 3306 -u <DATABASE_USERNAME> -p < mysqlsampledatabase.sql
Ingrese la contraseña cuando se le solicite.
Una vez que la importación se completa, puede conectarse a la base de datos RDS MySQL de nuevo para verificar que la base de datos y sus tablas se han creado con éxito.
Ejecute el siguiente comando desde dentro del Bastion host:
mysql -h <DATABASE_ENDPOINT> -P 3306 -u <DATABASE_USERNAME> -p
Después de ingresar su contraseña, puede listar las bases de datos y tablas disponibles:
Para asegurarse de que el dataset se importó correctamente en la instancia RDS, se ejecutó una consulta simple:
Esto devuelve una fila de la tabla de clientes, confirmando que:
- La base de datos y las tablas se crearon correctamente
- El dataset de ejemplo fue sembrado en la instancia RDS
- El Bastion host y la configuración RDS privada están funcionando según lo previsto
Esto completa la configuración de la infraestructura y el proceso de importación de datos.
Destruyendo la Infraestructura
Una vez que haya terminado de probar o demostrar su configuración, es importante destruir los recursos de AWS para evitar cargos innecesarios.
Dado que todo fue aprovisionado usando Terraform, derribar toda la infraestructura es tan simple como ejecutar un comando después de navegar a su directorio de configuración raíz:
cd terraform/rds-bastion
terraform destroy
Conclusión
Este artículo ha demostrado cómo aprovisionar una infraestructura de base de datos segura y similar a la de producción utilizando Terraform en AWS. En lugar de exponer la base de datos a la internet pública, se han implementado las mejores prácticas al colocar la instancia RDS en subredes privadas, accesibles solo a través de un bastion host en una subred pública.
Al estructurar el proyecto con configuraciones modulares de Terraform, se asegura que cada componente—red, base de datos y bastion host—esté débilmente acoplado, sea reutilizable y fácil de administrar. También se muestra cómo el gráfico de dependencia interna de Terraform maneja la orquestación y la secuenciación de la creación de recursos sin problemas.
Gracias a la infraestructura como código (IaC), todo el entorno se puede levantar o derribar con un solo comando, lo que lo hace ideal para la creación de prototipos de ETL, la práctica de ingeniería de datos o los pipelines de prueba de concepto. Lo más importante es que esta automatización ayuda a evitar costos inesperados al permitir destruir todos los recursos de forma limpia una vez que haya terminado.
El código fuente completo, la configuración de Terraform y los scripts de configuración se pueden encontrar en el repositorio de GitHub:
https://github.com/YagmurGULEC/rds-ec2-terraform.git
Próximos Pasos
Se puede extender esta configuración:
- Conectando un trabajo de AWS Glue a la instancia RDS para el procesamiento ETL.
- Añadiendo monitorización para su base de datos RDS e instancia EC2
Referencias
- Fuente original: End-to-End AWS RDS Setup with Bastion Host Using Terraform
- El script SQL utilizado en este proyecto (mysqlsampledatabase.sql) proviene de la Base de Datos de Muestra MySQL de código abierto. https://www.mysqltutorial.org/getting-started-with-mysql/mysql-sample-database/
- Creación y conexión a una instancia MySQL DB – https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_GettingStarted.CreatingConnecting.MySQL.html#CHAP_GettingStarted.Creating.MySQL.EC2