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!