Cómo desplegar un sitio web serverless con Terraform
Terraform es la principal herramienta open-source para crear infraestructura como código (IaC) en cualquier proveedor de nube. En este artículo, revisaremos cómo podemos usarla para desplegar un sitio web serverless en AWS, explorando diferentes opciones.
Terraform & AWS
Aprende Infraestructura como Código con Terraform en AWS, desde despliegues básicos hasta pipelines avanzados de CI/CD.
3 articles
1. Introducción
Como ya sabes, crear y gestionar infraestructura puede ser un proceso complejo y que consume mucho tiempo, y afortunadamente, herramientas como Terraform pueden simplificar este proceso permitiéndote definir tu Infraestructura como Código (IaC). En este artículo, exploraremos los conceptos básicos de Terraform y veremos cómo crear infraestructura en AWS usándola.
Sin embargo, este no es mi primer artículo sobre herramientas de Infraestructura como Código.
- Si quieres saber cómo crear infraestructura usando lenguajes de programación familiares, puedes revisar este artículo sobre CDK: Cómo crear infraestructura con CDK
- Si quieres crear aplicaciones serverless en AWS, puedes revisar este otro artículo sobre SAM: Cómo crear aplicaciones serverless con SAM
He subido el código fuente usado en este artículo en el siguiente repositorio de GitHub https://github.com/alazaroc/aws-terraform-serverless-website
2. Qué es Terraform
Terraform es una herramienta de software de Infraestructura como Código (IaC) open-source creada por HashiCorp. Te permite definir y gestionar tu infraestructura usando archivos de configuración legibles por humanos que puedes versionar, reutilizar y compartir. Luego puedes usar un flujo de trabajo consistente para aprovisionar y gestionar toda tu infraestructura a lo largo de su ciclo de vida. Terraform admite una amplia gama de proveedores de nube, incluyendo AWS, Microsoft Azure y Google Cloud Platform.
3. Cómo funciona Terraform
Terraform usa un lenguaje de configuración declarativo llamado HashiCorp Configuration Language (HCL).
En resumen:
- Write: Defines tu infraestructura en una serie de archivos .tf, que contienen la configuración de tus recursos
- Plan: Terraform luego usa esta configuración para generar un plan de ejecución, que describe los cambios que se harán a tu infraestructura.
- Apply: Una vez que hayas revisado el plan de ejecución, puedes aplicarlo a tu infraestructura usando el comando “
terraform apply”. Terraform entonces aprovisionará tus recursos según la configuración que definiste.
4. Comenzando con Terraform
Para comenzar con Terraform, necesitarás instalarlo en tu máquina. Puedes descargar la última versión de Terraform desde el sitio web oficial aquí, y si quieres desplegar en AWS, también necesitas AWS CLI.
Una vez que hayas instalado Terraform, puedes comenzar a crear tu infraestructura.
En esta sección, revisaremos cómo funciona Terraform creando un primer ejemplo para desplegar un bucket S3 simple:
Crea un nuevo directorio para tu archivo de configuración de Terraform:
1 2
mkdir my-serverless-website cd my-serverless-websiteCrea un nuevo archivo llamado “main.tf” y allí vamos a incluir la
información del proveedor de AWScon la región “us-east-1”, y un recurso de tipoaws_s3_bucketpara crear un bucket S3 con toda la configuración predeterminada. Recuerda que el nombre del bucket S3 debe ser único globalmente.1 2 3 4 5 6 7
provider "aws" { region = "us-east-1" } resource "aws_s3_bucket" "website_bucket" { bucket = "my-unique-bucket-name-12y398y13489148h" }
Inicializa tu configuración de Terraform.
1
terraform initCuando ejecutas
terraform init, Terraform descarga e instala los plugins de proveedor y módulos necesarios requeridos para tu configuración. También inicializa el backend, que es la ubicación de almacenamiento para tuarchivo de estado de Terraform. El archivo de estado se usa para almacenar el estado actual de tu infraestructura, y es usado por Terraform para planificar y aplicar cambios a tu infraestructura.Genera un plan de ejecución (opcional):
1
terraform planEste comando genera un plan de ejecución basado en la configuración en tu archivo
main.tf, y te permite verificar qué se va a desplegar.Aplica el plan de ejecución para desplegar en AWS:
1
terraform applyCuando ejecutas
terraform apply, Terraform compara el estado actual de tu infraestructura con el estado deseado definido en tu configuración. Luego crea, modifica o elimina recursos según sea necesario para llevar tu infraestructura al estado deseado. En nuestro ejemplo, se desplegará un nuevo bucket S3.Ahora, los recursos de AWS han sido creados. Vamos a verificarlo accediendo al servicio AWS S3 para acceder al nuevo recurso creado por Terraform:
Ya sabemos cómo desplegar recursos de AWS con Terraform, así que en la siguiente sección, evolucionaremos el ejemplo inicial para desplegar tres sitios web serverless usando bucket S3.
5. Práctica: Cómo desplegar un sitio web serverless con Terraform
Tenemos un bucket AWS S3 desplegado, y ahora vamos a usarlo para alojar un sitio web serverless. Hay diferentes formas de hacerlo, y vamos a evolucionar el bucket S3 para crear un sitio web estático serverless.
Esto es lo que vamos a construir:
- v1.1: bucket S3 público
- Ventaja: fácil de implementar
- Desventajas: sin dominio personalizado, no alineado con las mejores prácticas de seguridad (bucket público), sin caché para archivos estáticos
- v1.2: S3 público como
Static website hosting- Ventajas: fácil de implementar, documento de índice y página de error, reglas de redirección
- Desventajas: no alineado con las mejores prácticas de seguridad (bucket público), sin caché para archivos estáticos, los endpoints de sitio web de Amazon S3 no admiten HTTPS (si quieres usar HTTPS, puedes usar Amazon CloudFront para servir un sitio web estático alojado en Amazon S3)
- v2: distribución de CloudFront + bucket S3 privado
- Ventaja: fácil de implementar, bucket s3 privado, caché para archivos estáticos
- Desventajas: nombre de dominio generado automáticamente
- v3: Route53 + ACM + distribución de CloudFront + bucket S3 privado + opcionalmente Lambda Edge
- Ventajas: nombre de dominio personalizado usando certificados gestionados por AWS, bucket s3 privado, caché para archivos estáticos
- Desventajas: más complejo de implementar
En estos ejemplos, vamos a desplegar un sitio web estático basado en un archivo index.html con este contenido:
1
This is my serverless website
5.1. v1.1 - bucket S3 público
En esta versión 1.1 vamos a exponer el bucket S3 públicamente para que cualquier persona pueda acceder al archivo index.html usando el endpoint público de S3.
- Ventaja:
- fácil de implementar
- Desventajas:
- sin dominio personalizado
- no alineado con las mejores prácticas de seguridad (bucket público)
- sin caché para archivos estáticos
Esta solución no está alineada con las mejores prácticas de seguridad porque expone un bucket S3 públicamente.
Actualiza el archivo “main.tf” con el siguiente contenido:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "website_bucket" {
bucket = "my-unique-bucket-name-12y398y13489148h"
}
resource "aws_s3_object" "website_bucket" {
bucket = aws_s3_bucket.website_bucket.id
key = "index.html"
source = "index.html"
content_type = "text/html"
}
resource "aws_s3_account_public_access_block" "website_bucket" {
block_public_acls = false
block_public_policy = false
}
resource "aws_s3_bucket_public_access_block" "website_bucket" {
bucket = aws_s3_bucket.website_bucket.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "website_bucket" {
bucket = aws_s3_bucket.website_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = [
"s3:GetObject",
"s3:ListBucket",
]
Resource = [
"${aws_s3_bucket.website_bucket.arn}",
"${aws_s3_bucket.website_bucket.arn}/*"
]
}
]
})
}
Ahora, ejecuta el comando terraform apply: terraform apply --auto-approve
Luego, abre una ventana privada en tu navegador y accede al contenido de index.html usando el endpoint público de nuestro bucket S3. Si has ejecutado el código antes, será la siguiente URL: https://my-unique-bucket-name-12y398y13489148h.s3.amazonaws.com/index.html
5.2. v1.2 - Static website hosting usando S3
En esta versión, similar a la anterior, todavía tenemos expuesto el bucket S3 públicamente pero habilitaremos la función de alojamiento de sitio web estático de S3.
- Ventajas:
- fácil de implementar
- el sitio web estático incluye la configuración de un documento de índice, una página de error y también reglas de redirección
- Desventajas:
- no alineado con las mejores prácticas de seguridad (bucket público)
- sin caché para archivos estáticos
- Los endpoints de sitio web de Amazon S3 no admiten HTTPS (si quieres usar HTTPS, puedes usar Amazon CloudFront para servir un sitio web estático alojado en Amazon S3)
Esta solución no está alineada con las mejores prácticas de seguridad porque expone un bucket S3 públicamente.
Usando el contenido del “main.tf” mostrado en el ejemplo v1.1 anterior, agrega al final del documento las siguientes líneas:
1
2
3
4
5
6
resource "aws_s3_bucket_website_configuration" "website_bucket" {
bucket = aws_s3_bucket.website_bucket.id
index_document {
suffix = "index.html"
}
}
Ahora, ejecuta el comando terraform apply: terraform apply --auto-approve
Luego, abre una ventana privada en tu navegador y accede a través del sitio web estático al contenido de index.html: http://my-unique-bucket-name-12y398y13489148h.s3-website-us-east-1.amazonaws.com/
Si no te diste cuenta antes, mira que estamos usando HTTP, no HTTPS. El sitio web estático de S3 no admite HTTPS.
5.3. v2 - Distribución de CloudFront + bucket S3 privado
En esta versión vamos a volver a cambiar el bucket a privado (y también vamos a volver a habilitar la funcionalidad Block Public Access settings for this account de S3), y vamos a crear una distribución CloudFront conectada con el bucket S3 privado. De esta forma, accederemos al bucket S3 utilizando la distribución de CloudFront.
- Ventaja:
- fácil de implementar
- bucket S3 privado (alineado con las buenas prácticas de seguridad)
- incluye caché para archivos estáticos
- Desventajas:
- nombre de dominio autogenerado (por CloudFront)
Reemplaza el contenido del archivo main.tf por las siguientes líneas:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "website_bucket" {
bucket = "my-unique-bucket-name-12y398y13489148h"
}
resource "aws_s3_account_public_access_block" "website_bucket" {
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_object" "website_bucket" {
bucket = aws_s3_bucket.website_bucket.id
key = "index.html"
source = "index.html"
content_type = "text/html"
}
resource "aws_cloudfront_distribution" "cdn_static_site" {
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
comment = "my cloudfront in front of the s3 bucket"
origin {
domain_name = aws_s3_bucket.website_bucket.bucket_regional_domain_name
origin_id = "my-s3-origin"
origin_access_control_id = aws_cloudfront_origin_access_control.default.id
}
default_cache_behavior {
min_ttl = 0
default_ttl = 0
max_ttl = 0
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "my-s3-origin"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
locations = []
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
resource "aws_cloudfront_origin_access_control" "default" {
name = "cloudfront OAC"
description = "description of OAC"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
output "cloudfront_url" {
value = aws_cloudfront_distribution.cdn_static_site.domain_name
}
data "aws_iam_policy_document" "website_bucket" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.website_bucket.arn}/*"]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "aws:SourceArn"
values = [aws_cloudfront_distribution.cdn_static_site.arn]
}
}
}
resource "aws_s3_bucket_policy" "website_bucket_policy" {
bucket = aws_s3_bucket.website_bucket.id
policy = data.aws_iam_policy_document.website_bucket.json
}
Ahora, ejecuta el comando terraform apply: terraform apply --auto-approve
Después, abre una ventana privada en tu navegador y accede al contenido index.html utilizando el DNS de CloudFront. La ejecución de la plantilla de Terraform devuelve como salida la URL de la CloudFront Distribution.
Ahora el bucket S3 es privado, por lo que si accedes al endpoint público de S3 intentando obtener el archivo
index.html, recibirás un error
5.4. v3 - Route53 + ACM + CloudFront Distribution + bucket S3 privado
En el último ejemplo vamos a utilizar nuestro propio dominio (registrado en Route53) y vamos a crear un certificado utilizando ACM.
- Requisito:
- se requiere un dominio personalizado
- Ventajas:
- nombre de dominio personalizado utilizando certificados gestionados por AWS
- bucket S3 privado (alineado con las buenas prácticas de seguridad)
- incluye caché para archivos estáticos
- Desventajas:
- más complejo de implementar
Para poder ejecutar este ejemplo necesitas tu propio dominio (example.com) y tienes que registrarlo en el servicio Route53. Si no lo tienes, puedes comprar un nuevo dominio en Route53, pero te costará unos 10 dólares al año.
Estos son los cambios que tienes que hacer en el archivo main.tf anterior:
Actualiza en el recurso de CloudFront Distribution
aws_cloudfront_distributionlo siguiente (donde${var.domain_name_simple}es, por ejemplo,example.com):1 2 3
viewer_certificate { cloudfront_default_certificate = true }
sustituyéndolo por esto:
1 2 3 4 5 6 7 8 9 10
viewer_certificate { acm_certificate_arn = aws_acm_certificate.cert.arn ssl_support_method = "sni-only" minimum_protocol_version = "TLSv1.2_2021" } aliases = [ var.domain_name_simple, var.domain_name ]
A continuación, añade las siguientes líneas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
resource "aws_acm_certificate" "cert" { provider = aws.use_default_region domain_name = "*.${var.domain_name_simple}" validation_method = "DNS" subject_alternative_names = [var.domain_name_simple] lifecycle { create_before_destroy = true } } data "aws_route53_zone" "zone" { provider = aws.use_default_region name = var.domain_name_simple private_zone = false } resource "aws_route53_record" "cert_validation" { provider = aws.use_default_region for_each = { for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } allow_overwrite = true name = each.value.name records = [each.value.record] type = each.value.type zone_id = data.aws_route53_zone.zone.zone_id ttl = 60 } resource "aws_acm_certificate_validation" "cert" { provider = aws.use_default_region certificate_arn = aws_acm_certificate.cert.arn validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] } resource "aws_route53_record" "www" { zone_id = data.aws_route53_zone.zone.id name = "www.${var.domain_name_simple}" type = "A" alias { name = aws_cloudfront_distribution.cdn_static_site.domain_name zone_id = aws_cloudfront_distribution.cdn_static_site.hosted_zone_id evaluate_target_health = false } } resource "aws_route53_record" "apex" { zone_id = data.aws_route53_zone.zone.id name = var.domain_name_simple type = "A" alias { name = aws_cloudfront_distribution.cdn_static_site.domain_name zone_id = aws_cloudfront_distribution.cdn_static_site.hosted_zone_id evaluate_target_health = false } }
Ahora, ejecuta el comando terraform apply:
terraform apply --auto-approvePor último, abre una ventana privada en tu navegador y accede a tu dominio registrado: https://example.com
6. Conclusión
En este artículo hemos explorado los conceptos básicos de Terraform y hemos revisado cómo crear infraestructura mediante diferentes ejemplos. Con Terraform puedes definir tu infraestructura como código y automatizar el proceso de creación y actualización de tus recursos.
Este ejemplo está basado en el código que he creado para desplegar mi propio blog https://playingaws.com utilizando Route53 + ACM + CloudFront Distribution + bucket S3 privado.
7. Próximos pasos
Lecturas adicionales (IaC):
- CDK: Cómo crear aplicaciones Serverless con CDK
- SAM: Cómo crear aplicaciones Serverless con SAM
- SAM + CDK: ¿Te interesa cómo AWS SAM y AWS CDK pueden trabajar juntos? He explorado esto en otro artículo: Cómo crear aplicaciones serverless con CDK y SAM. Es un gran siguiente paso para quienes quieran ampliar sus conocimientos en arquitecturas serverless.
Espero tus opiniones y experiencias con AWS SAM. No dudes en compartirlas en los comentarios. ¡Feliz coding!





