Stack completa de infraestrutura e desenvolvimento de um website em Docker na AWS.
Este projeto tem o propósito de implementar uma aplicação composta de um Frontend estático e um Backend executado em Docker na nuvem.
Para a aplicação, foram feitas modificações no projeto do TodoMVC Vue.js feito por Evan You para que o mesmo utilizasse um backend para salvar o estado das listas.
O Backend foi implementado em Python, utilizando o framework Falcon. Para desenvolvimento, o estado é salvo em disco. Para produção, esse estado é salvo em um Bucket S3.
O Frontend é servido utilizando S3 + CloudFront, e o backend é executado utilizando o Elastic Container Service sobre EC2 em múltiplas zonas de disponibilidade.
A Stack de infraestrutura é provisionada utilizando Terraform 0.12.26. O deploy dos serviços é realizado através de roles do Ansible. O ambiente de desenvolvimento utiliza docker-compose para abstrair os serviços de frontend e backend.
O projeto é subdividido da seguinte forma:
$ lsd --tree --depth 1
.
├── backend
│ └── controllers
├── cloud-infrastructure
│ ├── modules
│ ├── production
│ └── shared
├── deployment
│ ├── ecr
│ └── frontend
└── frontend
└── js
- backend: código da API em Python
- cloud-infrastructure: código do terraform. Subdivide-se em módulos, com a definição dos módulos utilizados, production, que possui a instância do ambiente de produção, e shared, que possui recursos comuns entre ambientes.
- deployment: carrega as roles do ansible
- frontend: possui os arquivos de HTML, CSS e JavaScript relativos ao frontend. Note a ausência da pasta
node_modules
. Ela será inicializada mais a frente.
Durante o desenvolvimento de uma aplicação, é interessante que nos aproximemos o máximo possível de nossa infraestrutura em produção. É, no entanto, impraticável possuir uma infraestrutura dedicada a cada desenvolvedor, pois esta pode ser extremamente cara e demandar muito tempo para provisionamento.
O interessante é que as dependências da aplicação sejam reprodutíveis. Desse modo, podemos assumir que as bibliotecas e aplicações disponíveis ao desenvolvedor durante desenvolvimento serão as mesmas que estarão disponíveis em produção.
Para que isso seja implementado, é utilizado docker. Em desenvolvimento é criado uma imagem base e a pasta local do desenvolvedor é mapeada para dentro do container, de modo que suas modificações se reflitam dentro dele. Para produção, produzimos uma imagem com tudo que a aplicação precisa rodar, todos os artefatos e dependências, e enviamos para o Elastic Container Registry.
Bugs e problemas não antevistos podem ser inseridos durante esse processo de desenvolvimento e escapar para a produção. De nada adianta termos um ambiente próximo da produção em desenvolvimento se não for possível realizar testes e experimentos centralizados, sem interferir com o que está sendo utilizado pelo cliente.
Para isso é necessário possuirmos um ambiente de homologação. Este ambiente precisa ser idêntico ao ambiente de produção, nos mínimos detalhes. Cada configuração de máquina, Load Balancer, certificado, rota, CDN e permissões precisa ser as mesma. Com isso é possível identificar problemas de credenciais e permissões que potencialmente não afetam o desenvolvedor mas podem prejudicar o SLA do negócio.
O Terraform possui o conceito de módulos, unidades que definem um nicho de relações entre recursos em nuvem. Esses módulos podem ser reutilizados e efetivamente servem como abstrações lógicas. Para se obter o resultado de reprodução de infraestrutura, o ambiente de produção é tratado como um módulo. Dessa forma, para obtermos um novo ambiente de homologação, basta instanciar um novo módulo que uma infraestrutura identica é criada.
Para efeitos de apresentação, esse projeto conta apenas com um único módulo de ambiente de produção.
O projeto espera que seja utilizado o Terraform Cloud. Essa é uma ferramenta gratuita produzida e disponibilizada pela HashiCorp para executar planos do Terraform. Ela possui diversas vantagens, como centralização de estado da infraestrura, histórico de logs de aplicação, colaboração entre usuários, integração com pull requests do GitHub e execução em infraestrutura própria, localizada geograficamente próxima da us-east-x
(Ohio ou Virgínia do Norte) da AWS. Sua localização é interessante pois reduz a latência de chamada de API para a AWS, reduzindo sensivelmente o tempo necessário para se atualizar as informações requeridas por um plano.
Para que o container do backend seja executado, optou-se por utilizar o Elastic Container Service. Nele, tarefas são definidas para executar imagens docker. Serviços, que são compostos por uma ou mais tarefas, se encarregam de delegar a execução deles a EC2 registradas no Cluster ECS ao qual pertence, e registrar as devidas portas no Load Balancer. O cluster é composto por duas EC2 em regiões de disponibilidade distintas. Duas instâncias do nosso backend em dois containers separados rodam, dessa forma, em nessas duas EC2. Zonas de disponibilidades separadas são interessantes para reduzir a chance que uma indisponibilidade de serviço numa região possui de interromper o sistema em produçao.
O Elastic Container Service nos dá controle de quais permissões cada container herda, qual estratégia de deploy será empregada (Random, Distribuir por EC2, Distribuir por Zona...), qual proporção de recurso é dedicada para cada container, assim como toda a suite de configurações de um container como variáveis de ambiente, volumes etc...
As EC2 são controladas por um Autoscaling Group. O Autoscaling Group é capaz de definir as zonas de disponibilidades em que essas instancias serão lançadas, a quantidade de máquinas, o tipo e imagem a partir da qual essas máquinas serão lançadas, configurações internas (através do user_data.sh) e tipo de cobrança (on-demand ou spot). Para propósito desta demonstração serão lançadas instâncias tipo t2.micro
que são inclusas no free tier da AWS.
O conteúdo estático do site (JavaScript, CSS e HTML) é servido a partir de um bucket S3 por uma distribuição do CloudFront. O CloudFront é o serviço de distribuição de conteúdo da AWS. Ele é importante para aumentar a velocidade percebida do site, pois distribui o conteúdo a partir de pontos de presença espalhados pelo globo, e para reduzir custos de acesso ao S3, pois ao entregar um conteúdo em cache, o mesmo não é solicitado do S3 e não incorre custos de download e transferência de dados.
Como utilizaremos um único DNS para este projeto, o CloudFront deverá redirecionar por meio de regra de origin requisições para a path /api
para o Load Balancer, que por sua vez entregará essas requisições para os respectivos containers para que ela possa ser tratada. Desse modo, para toda a requisição que não possuir a path /api
, o CloudFront buscará no bucket de arquivos estáticos do frontend, pois esse bucket é seu origin padrão.
Os certificados SSL são gerenciados pelo AWS Certificate Manager e não são retidos em nenhuma máquina que possa sofrer ataque a ter o conteúdo dos certificados vazados. A AWS se encarrega também da renovação e da disponibilidade desses certificados.
A infraestrutura utiliza o Route 53 para definição de DNS que faz referência a nossa distribuição do CloudFront. Ela espera que um domínio exista e produz quatro Name Servers que precisam ser associados a um domínio real por meio de records tipo NS para que, por exemplo, certificados SSL sejam gerados e que o site seja accessível.
A chave para acesso SSH das instâncias do cluster ECS é depositada automaticamente num bucket definido. No entanto, o ECS possui um painel de monitoramento bastante compreensivo, com eventos, métricas e logs que torna acesso direto por terminal às máquinas desnecessário. Por definição de segurança, por meio de Security Groups, as instâncias do cluster só são accessíveis pelo Load Balancer, ou seja somente requisições na Camada de Aplicação chegam nas instâncias, impedindo acesso por SSH remoto. Não contando com as instâncias EC2, resto da aplicação é totalmente gerenciado pela AWS, portanto ficamos seguros quanto a ataques e indisponibilidades pois no Modelo de Responsabilidade Compartilhada da AWS, contanto que tomemos as precauções de segurança de tráfego e dos dados que nossa aplicação manipula, a AWS se encarrega da disponibilidade da infraestrutura e de todos os seus serviços gerenciados.
Abaixo encontra-se um diagrama da topologia da infraestrutura em nuvem:
Note que todos os elementos de infraestrutura podem ser utilizados no free tier:
Nome | Free Tier (mensal) |
---|---|
EC2 | 750 horas |
Load Balancer | 750 horas |
S3 | 5 GB |
CloudFront | 50 GB |
O projeto do Terraform, que se encontra na pasta cloud-infrastrucutre
segue o modelo de composição de infraestrura. Grosso modo, é um modelo que segmenta a infraestrurua, deixando em um mesmo estado apenas os recursos que realmente são necessários. Com isso ganhamos eficiência em deploy, pois em uma infraestrutura segmentada existem menos recursos para serem atualizados em tempo de plan.
Também reduzimos o blast radius (ou raio de "explosão"), que é a lista efetiva de recursos que podem ser afetados por uma intervenção mal planejada. Dessa forma, se a infraestrutura shared, production e outros eventuais ambientes como staging estiverem em seus respectivos workspaces, uma má implementação afetaria primeiramente o ambiente staging, aumentando as chances de ser visto e corrigido antes de afetar o ambiente de produção.
Para se alcançar este efeito, importamos dados compartilhados entre os workspaces. O workspace production depende da infraestrutura de rede criada no workspace shared, que é única, compartilhada entre todos os ambientes. Para isso, é importado o estado de shared através do Data Source terraform_remote_state
, que recebe como argumento o tipo de backend e sua configuração de acesso.
O deploy é feito inteiramente por um playbook Ansible. Esse playbook executa duas roles, uma para popular o ECR e outra para popular o bucket S3 e invalidar o cache do CloudFront.
O ECR funciona como um repositório Docker, como o Docker Hub, porém ele se distigue por ter seu acesso controlado por políticas que podemos definir na AWS. Desse modo, ele funciona efetivamente como um repositório privado. Podemos, portanto, populá-lo com imagens contendo segredos e artefatos sensíveis que os mesmo só poderão ser acessados por recursos e por pessoas com credenciais sobre as quais possuimos total controle.
- docker
- docker-compose
Para executar a aplicação localmente, é necessário compilar a imagem docker do backend e baixar as dependências do npm do frontend. Para isso, um script de conveniência foi escrito. Basta executar install.sh
que esse processo é realizado. Note que não é necessário possuir o npm instalado. O script se vale do docker para baixar uma imagem nodejs alpine e, com ela, mapeando a pasta do frontend para dentro de si, instala o conteúdo necessário.
Uma vez compilada a imagem e baixadas as dependências podemos executar a aplicação. O frontend é servido por um Nginx e o backend é executado pelo gunicorn, que é um servidor HTTP WSGI que o Falcon, nosso framework Python, é compativel. Toda a aplicação é executada em container.
Para executar a aplicação efetivamente, digite no terminal:
$ docker-compose up
Será exibido no terminal algo como:
Recreating devops-challenge_frontend_1 ... done
Starting devops-challenge_backend_1 ... done
Attaching to devops-challenge_backend_1, devops-challenge_frontend_1
frontend_1 | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
frontend_1 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
frontend_1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
frontend_1 | 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
frontend_1 | 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
frontend_1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
frontend_1 | /docker-entrypoint.sh: Configuration complete; ready for start up
backend_1 | [2020-06-09 00:22:19 +0000] [1] [INFO] Starting gunicorn 20.0.4
backend_1 | [2020-06-09 00:22:19 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
backend_1 | [2020-06-09 00:22:19 +0000] [1] [INFO] Using worker: sync
backend_1 | [2020-06-09 00:22:19 +0000] [7] [INFO] Booting worker with pid: 7
backend_1 | [2020-06-09 00:22:19 +0000] [8] [INFO] Booting worker with pid: 8
backend_1 | [2020-06-09 00:22:19 +0000] [9] [INFO] Booting worker with pid: 9
Indicando que o frontend e o backend estão sendo executados. Abra no browser http://localhost
e a página do TodoMVC será exibida. Note que um arquivo default.json
será criado na raíz da pasta backend
. Esse arquivo possui o conteúdo salvo pelo backend. Desse modo você pode interagir com a lista, limpar o cache de sua sessão do browser e quando retornar a mesma lista será exibida com persistência.
O procedimento para execução do projeto em nuvem é um pouco mais envolvido. Precisaremos de:
- Uma conta no Terraform Cloud
- Uma conta na AWS, de preferência com créditos free tier
- Um domínio ativo
- Terraform CLI
- AWS CLI
- Ansible
- Um fork deste repositório em sua conta GitHub
Ao criar uma conta na AWS, você terá apenas a própra conta raíz (root account). Com ela faremos dois usuários IAM, mas primeiro certifique de que sua senha da root account é forte e que o MFA está ativo.
Com a root account, crie dois usuários, um para você entrar pelo console e outro para o terraform e associe aos dois permissão da policy AdministratorFullAccess.
Crie uma senha e uma chave de API para o seu usuário de console para o usuário do terraform crie apenas uma chave de acesso de API. Precisaremos dela no Terraform Cloud.
No terminal, execute aws configure
e preencha com suas credenciais:
$ aws configure
AWS Access Key ID [****************IJ57]: AKIA4RVABCDEFGHIJK
AWS Secret Access Key [****************m3K3]: ABCDLgH6TLTJFSRqBKPBa8xTTCkjmnsbduehEFGH
Default region name [us-east-1]: us-east-2
Default output format [None]:
Configure uma conta no Terraform Cloud. Crie também um token de acesso e configure a CLI do Terraform. Quando terminar, você deverá ter o arquivo ~/.terraformrc
com a seguinte estrutura:
credentials "app.terraform.io" {
token = "xxxxxx.atlasv1.zzzzzzzzzzzzz"
}
Ao logar no Terraform Cloud, crie uma organização chamada devops-challenge
. Ao criar a organização, será pedido para se criar um workspace. Você pode associar agora o fork desse projeto da sua conta GitHub e criar o workspace shared.
No linux, navegue para a pasta cloud-infrastructure/shared
e inicialize o terraform com terraform init
. Faça o mesmo em cloud-infrastructure/production
. Dois workspaces serão criados no Terraform Cloud.
No Terraform Cloud, esses workspaces precisam de três variáves do terraform configuradas e duas variáveis de ambiente. Vá no Terraform Cloud, na organização criada e selecione o workspace shared
. No topo, clique em Variables
. O ambiente necessita que seja configurado com as seguintes variáveis:
- Terraform Variables
Key | Value |
---|---|
aws_region | recomendado: us-east-2 |
zone_name | devops-challenge.seudomínio.com |
project_name | seu-nome-devops-challenge* |
*A variável project_name
é utilizada para dar nome aos buckets estáticos de produção. Como os buckets são regionais, mas são indexados globalmente, seus nomes precisam ser únicos. Escolha um nome, portanto que seja único para o seu projeto.
- Environment Variables
Key | Value | Sensitive |
---|---|---|
AWS_ACCESS_KEY_ID | Access Key ID do usuário IAM Terraform* | Não |
AWS_SECRET_ACCESS_KEY | Secret da Access Key do usuário IAM Terraform* | Sim |
*Esses usuários foram criados na conta AWS no console, no passo anterior.
Vá em Settings > General
e em Terraform Working Directory
preencha com cloud-infrastructure/shared
Para o workspace production
:
- Terraform Variables
Key | Value |
---|---|
aws_region | recomendado: us-east-2 |
domain_name | devops-challenge.seudomínio.com |
environment_name | production |
project_name | seu-nome-devops-challenge |
- Environment Variables
Key | Value | Sensitive |
---|---|---|
AWS_ACCESS_KEY_ID | Access Key ID do usuário IAM Terraform* | Não |
AWS_SECRET_ACCESS_KEY | Secret da Access Key do usuário IAM Terraform* | Sim |
Faça a mesma associação com o GitHub (Settings -> Version Control -> Connect to version control
) e em Terraform Working Directory
preencha com cloud-infrastructure/production
.
Neste momento será possível aplicar a primeira parte da infraestrutura.
Primeiro vá no workspace shared
e se não houver nenhuma execução em processo, execute uma manualmente (Queue plan > Queue plan
). O Terraform Cloud divide cada intervenção de infraestrutura em plan
e apply
. No estágio plan
, uma descrição do que será feito, com todas as configurações de cada recurso a ser criado.
Quando o estágio de plan
for concluído na infraestrutura shared
, o resumo deve mostrar Plan: 19 to add, 0 to change, 0 to destroy
. Aprove o plan (Confirm & apply
), e a infraestrutura base será criada.
Quando o estágio de apply
terminar, um output extenso em letras verdes será impresso na tela. Nele, busque por route53_delegation_set
para que possamos criar no registrar de seu domínio os records tipo NS
. Por exemplo:
route53_delegation_set = {
"id" = "N00862131AUK6YJ3OA5DF"
"name_servers" = [
"ns-1200.awsdns-22.org",
"ns-1643.awsdns-13.co.uk",
"ns-67.awsdns-08.com",
"ns-931.awsdns-52.net",
]
}
Crie no registrar os quatro registros NS com o mesmo nome que a variável zone_name
foi configurada e aguarde a "propagação" do registro. Com isso iremos efetivamente delegar a administração do subdomínio devops-challenge.seudomínio.com
para a AWS para que possamos criar os certificados SSL e direcionar requisições para o CloudFront e Load Balancer.
Agora podemos criar a infraestrutura de produção. No workspace production
execute um run manualmente, caso um não esteja esperando. Aguarde que o plan
conclua com Plan: 33 to add, 0 to change, 0 to destroy.
em seu resumo. Aplique o plan e aguarde a conclusão.
Essa etapa demorará entre 20 e 30 minutos para concluir, pois criará autoscaling groups, load balancers, ECS clusters, certificados IAM, CloudFront e Route53 Records.
Num deploy do zero da infraestrutura, o terraform costuma encontrar problemas com o provider. Na ocasião o provider gerada alguma inconsistência entre plan
e apply
e por via das dúvidas o terraform interrompe o deploy da infraestrutura:
Error: Provider produced inconsistent final plan
When expanding the plan for
module.production_environment.module.ecs_service_backend.aws_ecs_task_definition.this
to include new values learned so far during apply, provider
"registry.terraform.io/-/aws" produced an invalid new value for .volume:
planned set element
cty.ObjectVal(map[string]cty.Value{"docker_volume_configuration":cty.ListValEmpty(cty.Object(map[string]cty.Type{"autoprovision":cty.Bool,
"driver":cty.String, "driver_opts":cty.Map(cty.String),
"labels":cty.Map(cty.String), "scope":cty.String})),
"efs_volume_configuration":cty.ListValEmpty(cty.Object(map[string]cty.Type{"file_system_id":cty.String,
"root_directory":cty.String})), "host_path":cty.UnknownVal(cty.String),
"name":cty.UnknownVal(cty.String)}) does not correlate with any element in
actual.
This is a bug in the provider, which should be reported in the provider's own
issue tracker.
Caso algo semelhante ocorra, isso é esperado. Apenas inicialize um novo run manualmente que a infraestrutura deverá subir normalmente.
Para realizar o deploy com o Ansible, copie o arquivo production-vars.yml.dist
removendo a extensão .dist
. Substitua o conteúdo do arquivo de acordo:
---
ecr_repo_name: "backend" # keep it like this
dockerfile_dir: "backend" # keep it like this
backend_path: "api" # keep it like this
aws_region: "us-east-2"
domain_name: "<domain_name>"
frontend_bucket: "<project_name>-production-website"
<project_name>
e<domain_name>
são variáveis setadas no passo de configuração do workspace production no Terraform Cloud.
Com isso, você precisa apenas de executar ansible-playbook deploy.yml
que as roles do ansible cuidarão do processo de deploy.
O primeiro deploy é o do Docker para o Elastic Container Registry. Nesse passo será compilada a imagem docker do backend e enviada para nosso repositório privativo de imagens. Note que entre o deploy do container e a sua execução no ECS pode existir um delay de 5 a 10 minutos.
O segundo passo garante que as dependencias do NPM estão instaladas e envia os artefatos do frontend para o bucket S3, criando uma invalidação do CloudFront em seguida. Os arquivos são refletidos imediatamente pelo CloudFront.
Terminado, visite devops-challenge.seudomínio.com
e o tudo deverá estar disponível. Observe que um arquivo chamado default.json
será criado em seu-nome-devops-challenge-production-state-storage
. Esse é o arquivo no qual o backend guarda o estado da sua aplicação TodoMVC.
Para destruir a infraestrutura, realize dois plans de destruição, um em cada workspace (Settings > Destruction and Deletion > Queue destroy plan
).
Enquanto os planos estão sendo executados, se dirija o console da AWS. Na parte do S3, esvazie todos os buckets. N parte do EC2, encontre a parte Load Balancers
, selecione o Load Balancer production
criado, vá em Actions > Edit attributes
e desmarque Deletion Protection
, clicando em save
em seguida.
Volte para o Terraform Cloud e aprove os planos de destruição da infraestrutura, começando pelo workspace production e em seguida o shared.
Variável de Ambiente | Significado | Default |
---|---|---|
STORAGE_BUCKET | Bucket S3 para guardar o estado. Guarda em disco caso for None |
None |
- Rota
GET /api/state/{state_name}
- Code 200: Response Body
{"id": Number, "title": String, "completed": Bool}[]
- Code 404 se
{state_name}.json
não for encontrado ou se houver problema de credenciais. Body{"cause":" String}
(cause
é o motivo capturado do 404).
- Code 200: Response Body
- Rota
PUT /api/state/{state_name}
Body:{"id": Number, "title": String, "completed": Bool}[]
- Code 200: caso sucesso
- Code 500: caso falha
O frontend é o TodoMVC em Vue.js, como feito por Evan You. O index.html
foi modificado para exibir loading..
enquanto o backend responde com o estado salvo. O arquivo store.js
foi modificado para, usando o Fetch API, comunicar com o backend.
A rede é composta por:
- VPC com CIDR
10.0.0.0/16
- Duas subnets públicas para produção nas regiões us-east-2a e us-east-2b de CIDR
10.0.0.0/20
e10.0.16.0/20
respectivamente. - Duas subnets públicas para staging nas regiões us-east-2a e us-east-2b de CIDR
10.0.32.0/20
e10.0.48.0/20
respectivamente. - Duas subnets privadas nas regiões us-east-2a e us-east-2b de CIDR
10.0.64.0/20
e10.0.80.0/20
respectivamente, para futuras implementações, como Lambda com acesso a VPC, RDS e banco de dados privativos por exemplo. - Um EIP para o NAT Gateway.
- NAT Gateway para as subnets privadas, associado a subnet publica de produção da região A.
- Um Internet Gateway (IGW) para as subnets públicas.
- Uma rota para as redes públicas, mapeando
0.0.0.0/0
para o gateway padrão de internet. - Uma rota para as redes privadas, mapeando
0.0.0.0/0
para o NAT gateway. - Um subnet group com as redes privadas para posterior utilização em um RDS.
Composto por:
- bucket S3 com a policy necessária para ser acessado pelo CloudFront, configuração de rota opcional e regras de CORS para o domínio solicitado.
- Recebe uma lista de
domain_names
. O primeiro domínio é utilizado como odomain_name
do certificado. O restante é usado comosubject_alternative_names
- Recebe também uma associação entre domain name e zone id
domain_name_by_zone_id
. O módulo itera sobre esse objeto e cruza com o output do certificado acm. Quando um certificado ACM é criado, ele gera também os recursos necessários para validação por DNS Challenge. Essas challenges precisam ser criadas por meio de Route53 Records nas zonas respectivas. O intuito desse argumento é abstrair esse mapa para que o usuário precise se preocupar apenas com os nomes e as zonas do certificado. Essa criação dos records é, no entanto opcional. Se um certificado necessitar ser criado em multiplas zonas, é necessário que apenas um módulo crie os DNS para a challenge. Os outros módulos precisam apenas validar os novos certificados nas novas zonas. - Realiza a validação do certificado.
- Abstrai a criação de um serviço ECS
O ECS é muito flexível, porém igualmente complicado. No intuito de simplificar o processo de criação de múltiplos serviços, escrevi esse módulo que aproxima mais a interface de criação com o que vemos no
docker-compose
:
module "ecs_service_backend" {
source = "../ecs_definition"
service_name = "backend"
cluster_id = aws_ecs_cluster.default.id
aws_region = var.aws_region
environment = var.environment_name
vpc_id = data.terraform_remote_state.shared.outputs.network.vpc.id
desired_count = 2
url = var.domain_name
lb_container_name = "backend"
lb_container_port = 5000
lb_listener = module.load_balancer.https_listener
load_balancer = module.load_balancer.load_balancer
task_role_arn = module.iam_role_ecs_backend_task.role.arn
health_check = {
path = "/status"
matcher = 404
}
containers = [
{
name = "backend"
image = aws_ecr_repository.backend.repository_url
hard-memory-limit = 128
soft-memory-limit = 64
port-mappings = [
{
container-port = 5000
}
]
environment-variables = {
STORAGE_BUCKET = aws_s3_bucket.state_storage.bucket
}
}
]
}
- Com esse módulo precisamos apenas passar o load balancer, porta de comunicação, nome da task principal do serviço, health check e informações dos containers que rodarão nesse serviço.
- É o módulo que define cada ambiente.
- Busca no estado do workspace shared as informações de rede e cria o Load Balancer em Muti AZ.
- Cria um Autoscaling Group, baseada na imagem Amazon Linux 2 ECS Optimized, que ao ser lançada se registra, por meio do
user_data.sh
no cluster ECS, também criado por ele. - Cria dois certificados, um para o CloudFront, na região
us-east-1
e outro para o Load Balancer na zona configurada. - Instancia o módulo do
ecs_definition
, que recebe as configurações do serviçobackend
e cria as devidas regras de redirecionamento para o target group do serviço ECS no Load Balancer. Tudo isso é feito de forma abstraída, sem que o usuário precise se preocupar com o formato da Task Definition, nem onde registrar o health check ou mesmo onde definir as regras de uso de recursos de sistema da EC2 Host. - A Task Definition é compilada usando templates do Terraform. O formato recebido pela AWS é JSON para task definition, porém o arquivo é feito em YAML que possui uma sintaxe compatível com produção dinâmica de texto a partir de templates (JSON, por exemplo, requer que ao fim de uma lista não haja vírgula, o que dificulta o processo de criação de templates)
- Cria uma role com a trust relationship padrão para o EC2.
- Recebe uma lista de policies gerenciadas pela AWS e associa a Role.
- Recebe uma Policy JSON inline e cria uma policy inline para essa role.
- Cria uma chave TLS, cria uma keypair na AWS e armazena essa chave num bucket.
- Abstrai a criação de um load balancer, com subnets, certificados e grupos de segurança passados por argumento, com um listener HTTP que redireciona para HTTPS e cria regras de redirecionamento de acordo com uma lista de regras passadas por argumento, por exemplo:
module "private_load_balancer" {
source = "./modules/load_balancer"
name = "private-load-balancer"
security_groups = [module.security_group_internal_load_balancer.security_group.id]
subnets = module.vpc_prod.private_subnets
internal = true
enable_deletion_protection = true
https_listener_rules = [
{
action = {
type = "forward"
target_group_arn = aws_lb_target_group.some_target_group.arn
}
conditions = [
{
type = "host-header"
values = ["backend.example.com"]
},
{
type = "path-pattern"
values = ["api"]
}
]
}
]
certificates = [
aws_acm_certificate.cert_main.arn,
module.acm_certificate_alternative.arn
]
}
Essa capacidade não foi necessária nesse projeto, mas o módulo é bem versátil.
- Compila a imagem com a tag do ECR, segundo o nome configurado
- Loga no ECR da aws automaticamente
- Realiza push nessa imagem
- Elimina a imagem local
- Instala dependencia NPM
- Envia os artefatos do frontend para o S3
- Realiza uma invalidação no cloudfront utilizando seu alias
devops-challenge.seu-domínio.com
- Converter o ECS para Kubernetes
Apesar do ECS ser interessante para aplicações de docker na AWS, ele restringe o escopo de atuação, forçando o usuário a ficar nesse tipo de infraestrurua. Um trabalho grande precisa ser feito caso um port desse serviço seja necessário para outro tipo de infraestrutura.
- Usar uma database transacional ACID para o backend
Para o Intuito deste exercício é interessante essa abordagem de armazenamento no S3 por ser algo simples e fácil de configurar. Porém a abordagem de abandonar esquema abre portas para corrupção de dados. Outro problema seria a falta de suporte a multiplos usuários. Se mais de uma pessoa entrar na aplicação, ela se comportará de forma errática.
-
Implementar uma autenticação OIDC para o frontend
-
AWS WAF (Web Application Firewall) para proteção da aplicação contra DDoS e possíveis injeções SQL quando o banco de dados transacional for implementado.
-
Criar um Jenkinsfile para pipeline programática do Jenkins
-
Melhorar a documentação dos módulos do terraform