DEV Community

Cover image for CDN de alta disponibilidade na AWS com Terraform: um guia para reduzir a latência e aumentar a performance de seu site
Tiago Boeing for AWS Community Builders

Posted on • Updated on

CDN de alta disponibilidade na AWS com Terraform: um guia para reduzir a latência e aumentar a performance de seu site

Este artigo é uma continuação do primeiro sobre o mesmo assunto que foi escrito há algum tempo, mas sem abordar a utilização de Infrastructure as Code (IaC). Neste artigo, será apresentado um guia de como criar sua própria CDN de alta disponibilidade na AWS usando IaC com Terraform.

Utilize a CDN para hospedar um website estático e/ou servir arquivos estáticos com baixa latência e alta disponibilidade. Além de melhorar o desempenho do website, as CDNs garantem que o usuário sempre será servido do servidor mais próximo, o que reduz ainda mais a latência e garante uma experiência de navegação rápida e suave. Isso pode levar a um aumento de tráfego e conversões.

ℹ️ Para explicação dos serviços envolvidos, precificação e outras informações, leia o primeiro artigo sobre o mesmo assunto que demonstra a configuração via Console da AWS.

É necessário conhecimento do funcionamento do Terraform e da AWS. Não serão abordados aqui conceitos introdutórios sobre essas tecnologias.

Objetivos

Criar automaticamente toda a infraestrutura necessária dentro da AWS em poucos minutos e com alguns comandos, incluindo:

  • Configuração dos serviços necessários;
  • Criação dos registros DNS para validar o certificado e um alias para o CloudFront;
  • Geração do certificado SSL para servir conteúdo utilizando protocolo HTTPS;

Pré-requisitos

  • Conta AWS;
  • AWS CLI devidamente configurado;
  • Terraform devidamente instalado e configurado;

Composição da infraestrutura

  • S3 como fonte dos arquivos;
  • CloudFront para distribuir os arquivos;
  • Route53 para criar os registros DNS no domínio próprio;
  • Certificate Manager (ACM) para gerenciar os certificados SSL/TLS para a distribuição do CloudFront.

Mermaid diagram with IaC structure

IaC - Infrastructure as Code

É uma prática de gerenciamento de infraestrutura de TI em que a infraestrutura é gerenciada por meio de código, em vez de configurações manuais. Com IaC, a infraestrutura é definida em arquivos de configuração que podem ser versionados, testados e implementados automaticamente, permitindo que os desenvolvedores gerenciem a infraestrutura de maneira mais eficiente e consistente. Ferramentas populares de IaC incluem o Terraform, o AWS CloudFormation e o Ansible.

Terraform

O Terraform é uma ferramenta de IaC que permite definir e provisionar infraestrutura em nuvem de forma declarativa, utilizando arquivos de configuração para descrever os recursos necessários. Com o Terraform, é possível criar, modificar e excluir recursos em diversos provedores de nuvem, como AWS, Azure e Google Cloud Platform, de forma consistente e replicável. Além disso, o Terraform possui recursos avançados, como a possibilidade de criar módulos reutilizáveis e gerenciar o estado da infraestrutura, permitindo uma gestão mais eficiente e segura da infraestrutura em nuvem.

Para mais informações, consulte a documentação oficial do Terraform.


ℹ️ Se desejar apenas o código-fonte para provisionar a infraestrutura acesse o repositório no GitHub.

Código-fonte

O projeto Terraform será dividido em vários arquivos para facilitar compreensão:

  • provider.tf: Especifica com qual cloud e versões estamos trabalhando;
  • main.tf: Arquivo principal que centraliza configurações comuns do Terraform, data, locals, etc;
  • outputs.tf: Define as saídas dos recursos criados pelo Terraform;
  • inputs.tf: Define as variáveis de entrada para o Terraform;
  • s3.tf: Configurações do bucket, políticas e outros;
  • certificate.tf: Configurações do certificado SSL para servir conteúdo utilizando HTTPS;
  • cloudfront.tf: Criação e configuração do CloudFront;
  • test-cdn.tf: Apenas para demonstração e teste do funcionamento, este arquivo fará upload de um arquivo HTML para o bucket e invalidará o cache do CloudFront para que as alterações sejam refletidas rapidamente. Remova após validar a implementação;
  • terraform.tfvars: Arquivo onde o valor das variáveis pode ser especificado.

Para visualizar o conteúdo dos arquivos, confira o repositório no momento que o artigo foi escrito. As seções abaixo abordarão apenas arquivos chave para facilitar a compreensão.

Definição do provedor (provider.tf)

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.59.0"
    }
  }
}

provider "aws" {
  skip_credentials_validation = true
}
Enter fullscreen mode Exit fullscreen mode

Variáveis locais e queries (main.tf)

locals {
  service              = var.service
  stage                = var.stage
  resource_prefix_name = "${var.service}-${var.stage}"
  route53_private_zone = var.route53_private_zone
  route53_base_domain  = var.route53_zone_domain
  cdn_domain           = var.cdn_domain
}

# Get AWS Account ID
data "aws_caller_identity" "current" {}

# Get Route53 Zone
data "aws_route53_zone" "domain_zone" {
  name         = "${local.route53_base_domain}."
  private_zone = local.route53_private_zone
}
Enter fullscreen mode Exit fullscreen mode
  • São definidas algumas variáveis locais para utilizar nos demais arquivos;
  • aws_caller_identity: consulta usada para obter o ID da conta e concatená-lo com o nome do bucket do S3 para garantir que o nome seja único na AWS e tentar evitar falhas na criação do bucket.
  • aws_route53_zone: consulta uma Hosted Zone no Route53 que possua o domínio informado.

Definição das inputs (inputs.tf)

Na infraestrutura teremos as seguintes inputs:

  • service: nome com que os recursos serão criados dentro da AWS;
  • stage: estágio da aplicação caso desejar criar mais de uma CDN para o mesmo serviço (dev, test, prod). Nota: é possível utilizar a implantação contínua do próprio CloudFront;
  • route53_private_zone: define se a zona DNS é privada ou não;
  • route53_zone_domain: domínio da zona DNS
  • cdn_domain: nome do domínio para a CDN, pode ser deixado em branco para utilizar o domínio base (Hosted Zone do Route53);
  • cloudfront_allowed_methods: métodos HTTP permitidos no CloudFront;
  • cloudfront_cached_methods: métodos HTTP que serão mantidos em cache no CloudFront;
  • cloudfront_default_root_object: arquivo raiz padrão no CloudFront;
  • cloudfront_http_version: versão do protocolo HTTP no CloudFront.
variable "service" {
  default     = "terraform-cdn"
  type        = string
  description = "Service name"
}

variable "stage" {
  default     = "dev"
  type        = string
  description = "Stage (dev, test, prod)"
}

variable "route53_private_zone" {
  default = false
  type    = bool
}

variable "route53_zone_domain" {
  default     = ""
  type        = string
  description = "Route53 zone domain (base domain)"
}

variable "cdn_domain" {
  default     = ""
  type        = string
  description = "Domain name (Where you want to deploy the CloudFront distribution. Leave empty to deploy inside base domain)"
}

variable "cloudfront_allowed_methods" {
  default = ["GET", "HEAD", "OPTIONS"]
  type    = list(string)
}

variable "cloudfront_cached_methods" {
  default = ["GET", "HEAD", "OPTIONS"]
  type    = list(string)
}

variable "cloudfront_default_root_object" {
  default = "index.html"
  type    = string
}

variable "cloudfront_http_version" {
  default = "http2"
  type    = string
}
Enter fullscreen mode Exit fullscreen mode

Passando valores para as variáveis criadas (terraform.tfvars)

Existem várias formas de passar valores para as variáveis do Terraform, incluindo a utilização de arquivos JSON, variáveis de ambiente ou a passagem de valores diretamente pela linha de comando no momento da execução do Terraform. Consulte a documentação oficial do Terraform para mais informações.

Neste exemplo será utilizado um arquivo com a extensão .tfvars, então basta criar um arquivo terraform.tfvars e definir o seguinte conteúdo:

route53_zone_domain = "tiagoboeing.com"
cdn_domain          = "terraform-cdn.tiagoboeing.com"
Enter fullscreen mode Exit fullscreen mode

Neste caso o domínio (Hosted Zone no Route53) é tiagoboeing.com e a CDN será implantada em um subdomínio.

A propriedade route53_zone_domain serve como base para o Terraform obter a Hosted Zone no Route53.

Criação do bucket no S3 (s3.tf)

O primeiro passo é criar um bucket no S3 e definir uma política de segurança que restrinja o acesso público direto a qualquer conteúdo. Isso impedirá que um objeto seja tornado público diretamente via S3. O CloudFront será a única maneira de acessar os arquivos.

Crie um arquivo chamado s3.tf e adicione o seguinte conteúdo:

resource "aws_s3_bucket" "bucket" {
  bucket = "${local.resource_prefix_name}-bucket-${data.aws_caller_identity.current.account_id}"

  tags = {
    Stage   = local.stage,
    Service = local.service
  }
}

resource "aws_s3_bucket_acl" "bucket_acl" {
  bucket = aws_s3_bucket.bucket.id
  acl    = "private"
}

resource "aws_s3_bucket_public_access_block" "bucket_public_access" {
  bucket = aws_s3_bucket.bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "bucket_policy" {
  bucket = aws_s3_bucket.bucket.id

  policy = jsonencode({
    "Version" : "2008-10-17",
    "Statement" : [
      {
        "Sid" : "AllowCloudFrontServicePrincipalReadOnly",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "cloudfront.amazonaws.com",
        },
        "Action" : "s3:GetObject",
        "Resource" : "arn:aws:s3:::${aws_s3_bucket.bucket.bucket}/*",
        "Condition" : {
          "StringEquals" : {
            "aws:SourceArn" : aws_cloudfront_distribution.cloudfront.arn
          }
        }
      }
    ]
  })

  depends_on = [
    aws_s3_bucket.bucket
  ]
}
Enter fullscreen mode Exit fullscreen mode

Estamos definindo os seguintes recursos:

  • aws_s3_bucket: Cria o bucket que o CloudFront utilizará como origin.
  • aws_s3_bucket_policy: Define uma política de acesso para que o CloudFront seja capaz de obter os objetos do bucket no S3 (necessário quando trabalhamos com buckets privados)
  • aws_s3_bucket_acl: Define o controle de acesso ao bucket no S3 como privado.
  • aws_s3_bucket_public_access_block: Define os parâmetros de acesso público para o bucket no S3 da AWS, bloqueando a criação de buckets públicos, políticas públicas, listagens públicas e controle de acesso público.

Criação e validação do certificado SSL (certificate.tf)

resource "aws_acm_certificate" "certificate" {
  domain_name       = local.cdn_domain != "" ? local.cdn_domain : data.aws_route53_zone.domain_zone.name
  validation_method = "DNS"

  tags = {
    Stage   = local.stage,
    Service = local.service
  }
}

resource "aws_acm_certificate_validation" "certificate_validation" {
  certificate_arn         = aws_acm_certificate.certificate.arn
  validation_record_fqdns = [for record in aws_route53_record.certificate_records : record.fqdn]

  depends_on = [
    aws_acm_certificate.certificate
  ]

  timeouts {
    create = "10m"
  }
}

# Add Certificate Validation Records on Route53
resource "aws_route53_record" "certificate_records" {
  for_each = {
    for dvo in aws_acm_certificate.certificate.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]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.domain_zone.zone_id
}
Enter fullscreen mode Exit fullscreen mode

Estamos definindo os seguintes recursos:

Criação do CloudFront (cloudfront.tf)

resource "aws_cloudfront_origin_access_control" "cloudfront_acl" {
  name = "ACL - ${local.resource_prefix_name}"

  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "cloudfront" {
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "CDN for ${local.resource_prefix_name}"
  default_root_object = var.cloudfront_default_root_object
  http_version        = var.cloudfront_http_version

  aliases = local.cdn_domain != "" ? [local.cdn_domain] : local.route53_base_domain != "" ? [local.route53_base_domain] : []

  origin {
    origin_id                = aws_s3_bucket.bucket.id
    origin_access_control_id = aws_cloudfront_origin_access_control.cloudfront_acl.id
    domain_name              = aws_s3_bucket.bucket.bucket_regional_domain_name
  }

  default_cache_behavior {
    target_origin_id = aws_s3_bucket.bucket.id

    compress        = true
    allowed_methods = var.cloudfront_allowed_methods
    cached_methods  = var.cloudfront_cached_methods

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = false

    minimum_protocol_version = "TLSv1.2_2021"
    ssl_support_method       = "sni-only"

    acm_certificate_arn = aws_acm_certificate_validation.certificate_validation.certificate_arn
  }

  tags = {
    Stage   = local.stage,
    Service = local.service
  }

  depends_on = [
    aws_s3_bucket.bucket
  ]
}

# Create Route53 Record to CloudFront
resource "aws_route53_record" "domain_record" {
  name    = local.cdn_domain != "" ? local.cdn_domain : data.aws_route53_zone.domain_zone.name
  type    = "A"
  zone_id = data.aws_route53_zone.domain_zone.zone_id

  alias {
    name                   = aws_cloudfront_distribution.cloudfront.domain_name
    zone_id                = aws_cloudfront_distribution.cloudfront.hosted_zone_id
    evaluate_target_health = false
  }

  depends_on = [
    aws_cloudfront_distribution.cloudfront
  ]
}
Enter fullscreen mode Exit fullscreen mode

Estamos definindo os seguintes recursos:

Teste da infraestrutura (test-cdn.tf)

Aqui será a etapa onde um arquivo HTML será enviado para o bucket S3 e então o cache do CloudFront invalidado para garantir que as alterações reflitam imediatamente.

#############################
# Only for tests purposes
# Upload one HTML file to S3
#############################
resource "aws_s3_object" "public_folder" {
  for_each     = fileset("public/", "*")
  bucket       = aws_s3_bucket.bucket.id
  key          = each.value
  source       = "public/${each.value}"
  etag         = filemd5("public/${each.value}")
  content_type = "text/html"
}

#############################
# Only for tests purposes
# Invalidate index.html from CloudFront cache
#############################
resource "null_resource" "cache_invalidation" {

  # prevent invalidating cache before new s3 file is uploaded
  depends_on = [
    aws_s3_object.public_folder
  ]

  for_each = fileset("${path.module}/public/", "**")

  triggers = {
    hash = filemd5("public/${each.value}")
  }

  provisioner "local-exec" {
    # sleep is necessary to prevent throttling when invalidating many files; a dynamic sleep time would be more reliable
    # possible way of dealing with parallelism (though would lose the indiviual triggers): https://discuss.hashicorp.com/t/specify-parallelism-for-null-resource/20884/2
    command = "sleep 1; aws cloudfront create-invalidation --distribution-id ${aws_cloudfront_distribution.cloudfront.id} --paths '/${each.value}'"
  }
}
Enter fullscreen mode Exit fullscreen mode

Crie uma pasta chamada public e adicione um arquivo index.html com qualquer conteúdo apenas para teste.

Provisionar infraestrutura

Com os recursos configurados, é hora de provisionar a infraestrutura.

# Valide o plano de criação
terraform plan

# Se tudo estiver correto, aplique o plano para iniciar a criação
terraform apply
Enter fullscreen mode Exit fullscreen mode

O provisionamento inicial normalmente leva alguns minutos e a distribuição do CloudFront pode demorar mais alguns minutos para ficar disponível.

Aguarde o tempo necessário. Ao finalizar, acesse o domínio configurado e valide se o arquivo HTML foi apresentado corretamente.

No exemplo do artigo o subdomínio onde a CDN foi provisionada é terraform-cdn.tiagoboeing.com


Parabéns! Você agora possui uma CDN de alta disponibilidade na AWS utilizando IaC. Não hesite em explorar outras soluções e recursos da AWS e continue aprendendo!

Links do artigo

Leia também

English

Redes sociais

Quer saber mais sobre mim, o autor deste artigo? Então dê uma olhada nas minhas redes sociais:

Espero que tenha gostado do artigo e encontrado informações úteis. Se tiver alguma dúvida ou sugestão, deixe nos comentários.


Você sabia que pode se tornar um Community Builder da AWS? Como Community Builder, você terá a oportunidade de se conectar com outros profissionais da área de tecnologia e aprender com especialistas da AWS. Além disso, você poderá compartilhar seus conhecimentos e experiências com outros membros da comunidade. Não perca a chance de se tornar um líder na sua área e se juntar a uma comunidade global de entusiastas da AWS! Saiba mais em aws.amazon.com/developer/community/community-builders.

Top comments (0)