Día 24 - Provider Docker en Terraform
🐳 Provider Docker en Terraform
¡Hoy conectamos dos mundos poderosos!
Aprenderemos a gestionar contenedores Docker usando Terraform, combinando Infrastructure as Code con containerización.
🔌 ¿Qué es el Provider Docker?
El Docker Provider permite a Terraform gestionar recursos Docker:
- 🖼️ Imágenes Docker (pull, build, tag)
- 📦 Contenedores (crear, configurar, gestionar lifecycle)
- 🌐 Redes (crear redes personalizadas)
- 💾 Volúmenes (almacenamiento persistente)
- 🏷️ Registries (autenticación y gestión)
🛠️ Configuración Inicial
Prerequisitos
# Verificar que Docker esté instalado y funcionando
docker version
docker ps
# Verificar Terraform
terraform version
Configuración del Provider
versions.tf
terraform {
required_version = ">= 1.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
# Configuración del provider Docker
provider "docker" {
}
⚠️ Nota:
El provider oficial de Docker para Terraform (kreuzwerker/docker
) está en mantenimiento limitado.
Existe un nuevo provider alternativo,calxus/docker
, que es compatible y ofrece mejoras.Para usarlo, cambia en
required_providers
:terraform {
required_providers {
docker = {
source = "calxus/docker"
version = "~> 3.0"
}
}
}La sintaxis de recursos y configuración es muy similar, pero revisa la documentación oficial para detalles y nuevas funcionalidades.
🖼️ Gestión de Imágenes Docker
Pulling Imágenes
# Imagen base desde Docker Hub
resource "docker_image" "nginx" {
name = "nginx:latest"
keep_locally = false # Eliminar imagen al hacer destroy
}
# Imagen específica con tag
resource "docker_image" "postgres" {
name = "postgres:15-alpine"
keep_locally = true # Mantener imagen localmente
}
# Imagen con digest específico (inmutable)
resource "docker_image" "redis" {
name = "redis@sha256:..."
}
Building Imágenes Personalizadas
# Build desde Dockerfile
resource "docker_image" "custom_app" {
name = "roxs-app:latest"
build {
context = path.module # Directorio con Dockerfile
dockerfile = "Dockerfile"
# Args de build
build_args = {
APP_VERSION = "1.0.0"
ENV = "production"
}
# Tags adicionales
tag = [
"roxs-app:1.0.0",
"roxs-app:latest"
]
}
# Triggers para rebuild
triggers = {
dockerfile_hash = filemd5("${path.module}/Dockerfile")
src_hash = sha256(join("", [
for f in fileset(path.module, "src/**") : filemd5("${path.module}/${f}")
]))
}
}
📦 Gestión de Contenedores
Contenedor Básico
resource "docker_container" "nginx_server" {
name = "my-nginx"
image = docker_image.nginx.image_id
# Configuración básica
restart = "unless-stopped"
# Puertos
ports {
internal = 80
external = 8080
protocol = "tcp"
}
# Variables de entorno
env = [
"ENV=production",
"DEBUG=false"
]
# Labels
labels {
label = "project"
value = "devops-challenge"
}
labels {
label = "managed-by"
value = "terraform"
}
}
Contenedor Avanzado
resource "docker_container" "webapp" {
name = "roxs-webapp"
image = docker_image.custom_app.image_id
# Configuración de restart
restart = "always"
# Múltiples puertos
ports {
internal = 3000
external = 3000
}
ports {
internal = 3001
external = 3001
}
# Variables de entorno desde archivo
env = [
"NODE_ENV=production",
"PORT=3000",
"DATABASE_URL=${var.database_url}",
"REDIS_URL=${var.redis_url}"
]
# Health check
healthcheck {
test = ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval = "30s"
timeout = "10s"
retries = 3
start_period = "40s"
}
# Límites de recursos
memory = 512 # MB
memory_swap = 1024 # MB
cpu_shares = 512
# Configuración de logs
log_driver = "json-file"
log_opts = {
"max-size" = "10m"
"max-file" = "3"
}
# Comando personalizado
command = ["npm", "start"]
# Working directory
working_dir = "/app"
# Usuario
user = "1000:1000"
# Capabilities
capabilities {
add = ["NET_ADMIN"]
drop = ["ALL"]
}
}
🌐 Gestión de Redes
Red Personalizada
resource "docker_network" "app_network" {
name = "roxs-app-network"
driver = "bridge"
# Configuración IPAM
ipam_config {
subnet = "172.20.0.0/16"
gateway = "172.20.0.1"
ip_range = "172.20.240.0/20"
}
# Opciones adicionales
options = {
"com.docker.network.bridge.name" = "roxs-bridge"
}
# Labels
labels {
label = "project"
value = "devops-challenge"
}
}
# Conectar contenedores a la red
resource "docker_container" "app_with_network" {
name = "app-networked"
image = docker_image.custom_app.image_id
# Conectar a red personalizada
networks_advanced {
name = docker_network.app_network.name
ipv4_address = "172.20.0.10"
aliases = ["app", "webapp"]
}
# También puede estar en la red por defecto
networks_advanced {
name = "bridge"
}
}
💾 Gestión de Volúmenes
Volúmenes Nombrados
# Crear volumen
resource "docker_volume" "app_data" {
name = "roxs-app-data"
# Driver específico
driver = "local"
# Opciones del driver
driver_opts = {
type = "none"
o = "bind"
device = "/host/path/data"
}
# Labels
labels {
label = "backup"
value = "daily"
}
}
# Usar volumen en contenedor
resource "docker_container" "app_with_volume" {
name = "app-persistent"
image = docker_image.custom_app.image_id
# Montar volumen nombrado
volumes {
volume_name = docker_volume.app_data.name
container_path = "/app/data"
read_only = false
}
# Bind mount
volumes {
host_path = "/host/config"
container_path = "/app/config"
read_only = true
}
# Volumen temporal
volumes {
container_path = "/tmp"
from_container = "temp-container"
}
}
🔧 Ejemplo Completo: Stack de Aplicación
Vamos a crear un stack completo con base de datos, cache y aplicación:
main.tf
terraform {
required_providers {
docker = {
source = "calxus/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
# Red para la aplicación
resource "docker_network" "app_network" {
name = "roxs-voting-network"
}
# Volúmenes
resource "docker_volume" "postgres_data" {
name = "postgres_data"
}
resource "docker_volume" "redis_data" {
name = "redis_data"
}
# Imágenes
resource "docker_image" "postgres" {
name = "postgres:15-alpine"
keep_locally = true
}
resource "docker_image" "redis" {
name = "redis:7-alpine"
keep_locally = true
}
resource "docker_image" "nginx" {
name = "nginx:alpine"
keep_locally = true
}
# Base de datos PostgreSQL
resource "docker_container" "postgres" {
name = "roxs-postgres"
image = docker_image.postgres.image_id
restart = "unless-stopped"
env = [
"POSTGRES_DB=${var.database_name}",
"POSTGRES_USER=${var.database_user}",
"POSTGRES_PASSWORD=${var.database_password}"
]
ports {
internal = 5432
external = var.postgres_external_port
}
volumes {
volume_name = docker_volume.postgres_data.name
container_path = "/var/lib/postgresql/data"
}
networks_advanced {
name = docker_network.app_network.name
aliases = ["database", "postgres"]
}
healthcheck {
test = ["CMD-SHELL", "pg_isready -U ${var.database_user}"]
interval = "10s"
timeout = "5s"
retries = 5
}
}
# Cache Redis
resource "docker_container" "redis" {
name = "roxs-redis"
image = docker_image.redis.image_id
restart = "unless-stopped"
command = [
"redis-server",
"--appendonly", "yes",
"--appendfsync", "everysec"
]
ports {
internal = 6379
external = var.redis_external_port
}
volumes {
volume_name = docker_volume.redis_data.name
container_path = "/data"
}
networks_advanced {
name = docker_network.app_network.name
aliases = ["cache", "redis"]
}
healthcheck {
test = ["CMD", "redis-cli", "ping"]
interval = "10s"
timeout = "3s"
retries = 3
}
}
# Nginx como reverse proxy
resource "docker_container" "nginx" {
name = "roxs-nginx"
image = docker_image.nginx.image_id
restart = "unless-stopped"
ports {
internal = 80
external = var.nginx_external_port
}
# Configuración personalizada de nginx
upload {
content = templatefile("${path.module}/nginx.conf", {
app_upstream = "app:3000"
})
file = "/etc/nginx/nginx.conf"
}
networks_advanced {
name = docker_network.app_network.name
aliases = ["proxy", "nginx"]
}
# Depende de que los otros servicios estén running
depends_on = [
docker_container.postgres,
docker_container.redis
]
}
variables.tf
variable "database_name" {
description = "Nombre de la base de datos"
type = string
default = "voting_app"
}
variable "database_user" {
description = "Usuario de la base de datos"
type = string
default = "postgres"
}
variable "database_password" {
description = "Contraseña de la base de datos"
type = string
sensitive = true
default = "postgres123"
}
variable "postgres_external_port" {
description = "Puerto externo para PostgreSQL"
type = number
default = 5432
}
variable "redis_external_port" {
description = "Puerto externo para Redis"
type = number
default = 6379
}
variable "nginx_external_port" {
description = "Puerto externo para Nginx"
type = number
default = 8080
}
outputs.tf
output "application_urls" {
description = "URLs de acceso a la aplicación"
value = {
nginx = "http://localhost:${var.nginx_external_port}"
postgres = "postgresql://${var.database_user}:${var.database_password}@localhost:${var.postgres_external_port}/${var.database_name}"
redis = "redis://localhost:${var.redis_external_port}"
}
}
output "container_info" {
description = "Información de contenedores"
value = {
postgres = {
id = docker_container.postgres.id
name = docker_container.postgres.name
ip = docker_container.postgres.network_data[0].ip_address
}
redis = {
id = docker_container.redis.id
name = docker_container.redis.name
ip = docker_container.redis.network_data[0].ip_address
}
nginx = {
id = docker_container.nginx.id
name = docker_container.nginx.name
ip = docker_container.nginx.network_data[0].ip_address
}
}
}
output "network_info" {
description = "Información de la red"
value = {
name = docker_network.app_network.name
driver = docker_network.app_network.driver
subnet = docker_network.app_network.ipam_config[0].subnet
}
}
output "volumes_info" {
description = "Información de volúmenes"
value = {
postgres_volume = docker_volume.postgres_data.name
redis_volume = docker_volume.redis_data.name
}
}
🔍 Comandos Útiles
Gestión del Stack
# Inicializar
terraform init
# Planificar
terraform plan
# Aplicar
terraform apply -auto-approve
# Ver estado
terraform show
# Ver outputs
terraform output
# Verificar contenedores
docker ps
# Ver logs
docker logs roxs-postgres
docker logs roxs-redis
docker logs roxs-nginx
# Limpiar todo
terraform destroy -auto-approve
Debugging
# Inspeccionar red
docker network inspect roxs-voting-network
# Inspeccionar volúmenes
docker volume inspect postgres_data
# Conectar a contenedor
docker exec -it roxs-postgres psql -U postgres -d voting_app
# Verificar conectividad
docker exec roxs-nginx ping postgres
docker exec roxs-nginx ping redis
📊 Data Sources
Los data sources permiten obtener información de recursos existentes:
# Obtener información de imagen existente
data "docker_image" "existing_nginx" {
name = "nginx:latest"
}
# Obtener información de red existente
data "docker_network" "existing_network" {
name = "bridge"
}
# Usar en recursos
resource "docker_container" "app_existing_network" {
name = "app-on-bridge"
image = data.docker_image.existing_nginx.image_id
networks_advanced {
name = data.docker_network.existing_network.name
}
}
🚨 Mejores Prácticas
1. Gestión de Imágenes
# ✅ Usar tags específicos en producción
resource "docker_image" "app_prod" {
name = "myapp:v1.2.3" # No usar 'latest'
}
# ✅ Usar keep_locally apropiadamente
resource "docker_image" "base_image" {
name = "postgres:15-alpine"
keep_locally = true # Para imágenes base
}
2. Configuración de Contenedores
# ✅ Usar health checks
resource "docker_container" "app" {
# ... configuración ...
healthcheck {
test = ["CMD", "curl", "-f", "http://localhost/health"]
interval = "30s"
timeout = "10s"
retries = 3
}
}
# ✅ Configurar límites de recursos
resource "docker_container" "app" {
# ... configuración ...
memory = 512
memory_swap = 1024
cpu_shares = 512
}
3. Redes y Seguridad
# ✅ Usar redes personalizadas
resource "docker_network" "app_network" {
name = "app-network"
driver = "bridge"
# Configuración específica
ipam_config {
subnet = "172.20.0.0/16"
}
}
# ✅ Exponer solo puertos necesarios
resource "docker_container" "database" {
# ... configuración ...
# NO exponer puerto si no es necesario
# ports {
# internal = 5432
# external = 5432
# }
}
4. Variables Sensibles
# ✅ Marcar passwords como sensitive
variable "database_password" {
description = "Database password"
type = string
sensitive = true
}
✅ ¿Qué Aprendiste Hoy?
✅ Configuración del Provider Docker en Terraform
✅ Gestión de imágenes: pull, build, y configuración
✅ Creación y configuración de contenedores avanzada
✅ Redes personalizadas y conectividad entre servicios
✅ Volúmenes para persistencia de datos
✅ Stack completo con múltiples servicios
✅ Data sources para recursos existentes
✅ Mejores prácticas de seguridad y performance
🔮 ¿Qué Sigue Mañana?
Mañana en el Día 25 aprenderemos sobre:
- Módulos en Terraform
- Creación de módulos reutilizables
- Registro de módulos
- Composición de infraestructura
💬 Comparte tu progreso en la comunidad con el hashtag #DevOpsConRoxs
¡Excelente trabajo gestionando Docker con Terraform! 🐳🎉