Olá pessoal, tudo bem? Hoje vamos criar todos os recursos necessários para hospedar um site estático usando S3, CloudFront e Route 53. Além disso, vou explicar alguns ajustes essenciais para que sites gerados por frameworks como Hugo e Vue.js possam ser hospedados no S3 de forma segura, evitando erros 403 que são bastante comuns quando o CloudFront não está configurado corretamente.

Assumiremos que o site está em um repositório e podemos prosseguir a partir desse ponto.

Estaremos utilizando uma ferramenta chamada CDK! Para quem ainda não conhece, trata-se de uma ferramenta de Infrastructure as Code (IAC), assim como o Terraform, mas que roda em cima do CloudFormation. Optamos por essa ferramenta porque toda a nossa aplicação utiliza apenas recursos da AWS, então não há necessidade de uma ferramenta cloud agnostic. Além disso, não precisaremos nos preocupar com o gerenciamento do estado da infraestrutura, pois o CloudFormation cuida disso automaticamente.

Existem vários benefícios ao utilizar o CDK em vez do Terraform, especialmente quando se trabalha exclusivamente com a AWS, mas isso é assunto para outra ocasião.

Iniciando Nosso Projeto

O CDK utiliza TypeScript, mas também suporta outras linguagens como Go, Python, C#, entre outras. Recomendo o uso do TypeScript, pois é nativo do CDK e possui uma documentação mais robusta.

Primeiro, vamos instalar o CDK em nossa máquina. Um requisito é ter o Node.js instalado. Você pode seguir este link para a melhor forma de instalação para o seu sistema operacional.

Agora, sigamos com o comando de instalação.

npm install -g aws-cdk

Verifique a instalação do CDK.

cdk --version
2.110.0 (build c6471f2)

Vamos criar o diretório da nossa aplicação.

mkdir BlogIac
cd BlogIac

Inicie o projeto.

cdk init app --language typescript

Esse comando deverá retornar algo semelhante a isso.

Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build`   compile typescript to js
* `npm run watch`   watch for changes and compile
* `npm run test`    perform the jest unit tests
* `npx cdk deploy`  deploy this stack to your default AWS account/region
* `npx cdk diff`    compare deployed stack with current state
* `npx cdk synth`   emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...
✅ All done!

Escrevendo Nossa IaC

Agora podemos começar a trabalhar no nosso código. O arquivo que iremos editar está dentro da pasta lib.

Com relação ao DNS, infelizmente não consigo sugerir a opção certa para todos os casos, pois cada pessoa pode ter um caso diferente. No meu caso, já tenho um Route 53 que foi criado e estarei apenas importando-o para dentro da minha Stack.

export class BlogIacStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const hostedZone = cdk.aws_route53.HostedZone.fromHostedZoneId(this, 'HostedZone', 'ID DA ZONA DE DNS');

    // Caso você queira criar uma zona

    const hostedZone = new cdk.aws_route53.HostedZone(this, `HostedZone`, {
      zoneName: "Seu dominio",
    });

  }
}

Dessa forma, estamos importando uma hosted zone específica e armazenando em uma variável.

Agora continuamos criando os recursos sempre dentro da nossa classe.

Aqui está a lista de todos os serviços que utilizaremos em nossa stack:

  • aws_route53
  • aws_certificatemanager
  • aws_s3
  • aws_cloudfront
  • aws_cloudfront_origins
  • aws_route53_targets

Agora, vamos criar o certificado.

const blogCert = new cdk.aws_certificatemanager.Certificate(this, 'BlogCert', {
  domainName: 'blog.dominio.com.br',
  validation: cdk.aws_certificatemanager.CertificateValidation.fromDns(hostedZone),
  subjectAlternativeNames: ['*.blog.dominio.com.br'],
});

blogCert.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

Sempre adiciono essa política para remover os recursos em caso de deletar a Stack. Por segurança, alguns recursos não são excluídos por padrão.

Agora, avançamos para a criação do bucket.

const blogBucket = new cdk.aws_s3.Bucket(this, 'BlogBucket', {
  autoDeleteObjects: true,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
  accessControl: cdk.aws_s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL,
  blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL,
});

Queremos que nosso bucket seja privado, e a única forma de acesso seja através do CloudFront. Claro que isso traz algumas complicações, as quais explicarei em breve.

Com o certificado e o bucket criados, agora podemos criar nossa distribuição do CloudFront que utilizará ambos os recursos.

const blogDistribution = new cdk.aws_cloudfront.Distribution(this, 'BlogDistribution', {
  domainNames: ['blog.dominio.com.br'],
  certificate: blogCert,
  defaultBehavior: {
    origin: new cdk.aws_cloudfront_origins.S3Origin(blogBucket),
    viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    functionAssociations: [
      {
        eventType: cdk.aws_cloudfront.FunctionEventType.VIEWER_REQUEST,
        function: new cdk.aws_cloudfront.Function(this, 'Function', {
          code: cdk.aws_cloudfront.FunctionCode.fromInline(`
            function handler(event) {
              var request = event.request;
              var uri = request.uri;
              if (uri.endsWith('/')) {
                request.uri += 'index.html';
              }
              else if (!uri.includes('.')) {
                request.uri += '/index.html';
              }
            }`),
        }),
      },
    ],
  },
  errorResponses: [
    {
      httpStatus: 403,
      responseHttpStatus: 200,
      responsePagePath: '/index.html',
    },
    {
      httpStatus: 404,
      responseHttpStatus: 200,
      responsePagePath: '/index.html',
    },
  ],
});

blogDistribution.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

Agora vamos abordar a função que estamos criando dentro da distribuição do CloudFront. Basicamente, se você estiver utilizando um gerador de site estático como Hugo, Pelican e outros, o que eles fazem é adicionar arquivos index.html dentro de paths como /blog/.

No entanto, isso não funciona bem com o CloudFront, e é por isso que precisamos reescrever a requisição e adicionar o index.html à requisição. Vale ressaltar que isso pode gerar um custo adicional, então esteja ciente.

Caso você não precise desse recurso, basta remover a função e configurar um defaultRootObject na sua distribuição. Dessa forma:

const blogDistribution = new cdk.aws_cloudfront.Distribution(this, 'BlogDistribution', {
  domainNames: ['blog.dominio.com.br'],
  defaultRootObject: 'index.html',
  certificate: blogCert,
  defaultBehavior: {
    origin: new cdk.aws_cloudfront_origins.S3Origin(blogBucket),
    viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
  },
  errorResponses: [
    {
      httpStatus: 403,
      responseHttpStatus: 200,
      responsePagePath: '/index.html',
    },
    {
      httpStatus: 404,
      responseHttpStatus: 200,
      responsePagePath: '/index.html',
    },
  ],
});

blogDistribution.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

Agora, o último passo seria criar um ponteiro do tipo A no Route 53 que aponte para a distribuição do CloudFront.

const blogAliasRecord =  new cdk.aws_route53.ARecord(this, 'BlogAliasRecord', {
  zone: hostedZone,
  recordName: 'blog.dominio.com.br',
  target: cdk.aws_route53.RecordTarget.fromAlias(new cdk.aws_route53_targets.CloudFrontTarget(blogDistribution)),
});

blogAliasRecord.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

Com isso, temos tudo o que é necessário para implantar a infraestrutura de um blog utilizando Route 53, ACM, S3 e CloudFront.

Agora, dentro do arquivo que está na pasta bin do nosso projeto, precisamos definir algumas informações. Dentro da inicialização da classe que está criando, você deve descomentar a linha comentada e adicionar suas informações.

new BlogIacStack(app, 'BlogIacStack', {
  // env: { account: '123456789012', region: 'us-east-1' },
});

Após isso, caso seja a sua primeira vez utilizando o CDK, você deve realizar o bootstrap da sua conta. Utilize este comando com as suas credenciais AWS já configuradas. Para saber como fazer, acesse este link.

cdk bootstrap aws://IdDaSuaConta/regiao

Na raiz do projeto você pode executar o seguinte comando para fazer o deploy da Stack.

cdk deploy BlogIacStack

Após a execução desse comando, basta identificar o bucket que foi criado e subir os arquivos do seu site para ele.

Agradeço por acompanhar este post! Se tiver alguma dúvida ou precisar de mais informações, fique à vontade para entrar em contato. Boa jornada no desenvolvimento do seu blog!