avatar Artículo

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

In progress

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.

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:

how-terraform-works

  • 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-website
    
  • Crea un nuevo archivo llamado “main.tf” y allí vamos a incluir la información del proveedor de AWS con la región “us-east-1”, y un recurso de tipo aws_s3_bucket para 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 init
    

    Cuando 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 tu archivo 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 plan
    

    Este 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 apply
    

    Cuando 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:

    terraform-s3

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:

terraform-website

  • 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

s3-website

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/

s3-static-website

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.

terraform-cloudfront

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_distribution lo 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-approve

  • Por ú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):

Espero tus opiniones y experiencias con AWS SAM. No dudes en compartirlas en los comentarios. ¡Feliz coding!

Este artículo está licenciado bajo CC BY 4.0 por el autor.