Pedro H. Santos
Posted on June 22, 2022
Introdução
Com a difusão da cultura DevOps nos últimos anos algumas práticas para entrega de novos produtos ou novas funcionalidades no ambiente produtivo mudaram bastante. Nesse artigo vou exemplificar em um pequeno laboratório um processo de pipeline CD/CD para entrega de uma infraestrutura utilizando Terraform, GitHub Actions e AWS como cloud pública.
Terraform 🌎
Terraform é uma ferramenta de infraestrutura como código (IaC) que permite criarmos recursos em clouds públicas ou privadas utilizando uma linguagem simples e declarativa, podendo assim reutilizar e versionar o código da sua infraestrutura assim como é feito com a aplicação. Você pode então usar um workflow consistente para provisionar e gerenciar toda a sua infraestrutura ao longo de seu ciclo de vida.
GitHub Actions 🤖
GitHub Actions é uma plataforma de integração contínua e entrega contínua (CI/CD) que permite automatizar sua compilação de código, testar e entregar de forma simples e rápida.
Mão na massa! 💻
A ideia do laboratório é provisionar uma instância EC2 com ip público executando um webserver nginx rodando o famoso jogo da cobrinha 🐍.
Preparando o GitHub para acessar a AWS com o Terraform
A primeira etapa que precisa ser feita é configura as secrets AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY para que o GitHub Actions consiga acessar esses valores durante a execução do workflow e ter acesso a sua conta da AWS.
Dentro do repositório vá em: Settings -> Secrets -> Actions e adicionei as chaves de acesso da AWS.
Criando os workflows do GitHub Actions
Para termos as actions do GitHub rodando de acordo com cada evento precisamos criar o diretório .github/workflows
na raiz do projeto. Neste caso vamos criar 3 arquivos de workflow, um para o executar o terraform plan
quando for criado um pull request, outro para executar o terraform apply
quando o merge for feito na branch main e o último e não menos importante para executar o terraform destroy
, esse será executado manualmente quando for necessário destruir a infraestrutura.
Eu utilizei como base para a criação dos workflows a action oficial da HashiCorp - Setup Terraform
💡Dica: No site HashiCorp Learn temos um tutorial de como criar uma pipeline com GitHub Actions utilizando Terraform Cloud.
plan.yml
name: "Plan"
on:
pull_request:
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
terraform:
name: "Terraform"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 0.15.5
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -no-color -input=false
continue-on-error: true
- uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`\n
${process.env.PLAN}
\`\`\`
</details>
*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
apply.yml
name: "Apply"
on:
push:
branches:
- main
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
terraform:
name: "Terraform"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 0.15.5
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -no-color -input=false
continue-on-error: true
- uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`\n
${process.env.PLAN}
\`\`\`
</details>
*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
destroy.yml
name: "Destroy"
on:
workflow_dispatch:
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
destroy:
name: "terraform destroy"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 0.15.5
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Destroy
run: terraform destroy -auto-approve
Código da infra no Terraform
Não vou me aprofundar muito nos detalhes do código do Terraform, isso pode ficar para outro artigo. Basicamente temos a criação de 3 recursos dentro da AWS são eles: VPC, Security Group e a Instância EC2.
Para a criação da VPC já com a subnet pública, zonas de disponibilidades (AZs) e NAT Gateway estou utilizando o módulo vpc. Abaixo o trecho de código:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = "snake-vpc"
cidr = "10.200.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.200.101.0/24", "10.200.102.0/24"]
enable_nat_gateway = true
tags = {
Terraform = "true"
Environment = "prod"
}
}
Para a criação do Security Group estou utilizando o resource aws_security_group, onde configuramos a liberação geral na porta 80, padrão HTTP. Abaixo o trecho de código:
resource "aws_security_group" "game_snake_sg" {
name = "instances-snake-sg"
description = "SG for Instances Snake Security Group"
vpc_id = module.vpc.vpc_id
ingress {
description = "Game Snake port 80"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Terraform = "true"
Environment = "prod"
}
}
Para finalizar criamos a instância EC2 utilizando o módulo ec2_instance, o servidor será provisionado com a imagem Amazon Linux 2, tipo t1.micro, anexamos o Security Group criando anteriormente, anexamos também a instância dentro da subnet pública que foi criada junto com a VPC e por fim passamos um arquivo no userdata, que basicamente é um shell script que será executando quando a instância for iniciada.
module "ec2_instance" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "snake-game"
ami = "ami-0cff7528ff583bf9a"
instance_type = "t1.micro"
vpc_security_group_ids = [aws_security_group.game_snake_sg.id]
subnet_id = module.vpc.public_subnets[0]
user_data = file("userdata.sh")
tags = {
Name = "snake-game-ec2"
Terraform = "true"
Environment = "prod"
Team = "gamer-development"
Application = "snake-game"
Language = "javascript"
}
}
Desta forma o arquivo do terraform finalizado fica da seguinte maneira:
main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = "snake-vpc"
cidr = "10.200.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.200.101.0/24", "10.200.102.0/24"]
enable_nat_gateway = true
tags = {
Terraform = "true"
Environment = "prod"
}
}
resource "aws_security_group" "game_snake_sg" {
name = "instances-snake-sg"
description = "SG for Instances Snake Security Group"
vpc_id = module.vpc.vpc_id
ingress {
description = "Game Snake port 80"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Terraform = "true"
Environment = "prod"
}
}
module "ec2_instance" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "snake-game"
ami = "ami-0cff7528ff583bf9a"
instance_type = "t1.micro"
vpc_security_group_ids = [aws_security_group.game_snake_sg.id]
subnet_id = module.vpc.public_subnets[0]
user_data = file("userdata.sh")
tags = {
Name = "snake-game-ec2"
Terraform = "true"
Environment = "prod"
Team = "gamer-development"
Application = "snake-game"
Language = "javascript"
}
}
Enviando um Pull Request, fazendo o Merge e executando o deploy da infraestrutura 🚀
Vamos criar uma nova branch no projeto chamada deployment
fazer uma alteração no arquivo README.md
e criar um Pull Request. Assim que Pull Request for criado o workflow plan.yml será disparado e poderemos verificar o que o Terraform irá provisionar na AWS.
Pull Request
Veja que após a execução do workflow plan.yml dentro dos comentários da Pull Request o GitHub Actions informa o resultado na execução, incrível né?! 🤩
Merge
Agora vamos aprovar o Merge da Pull Request para a branch main
. Com essa estratégia é possível revisar o que exatamente o Terraform irá aplicar antes ser executado.
Após a execução podemos verificar no workflow apply.yml que tudo foi executado com sucesso.
Validando a infraestrutura na AWS
Verificando a conta AWS vemos que toda a infraestrutura foi provisionada com sucesso e a aplicação do jogo da cobrinha 🐍 está online.
Executando o Destroy 🗑
Caso seja necessário deletar toda a infraestrutura que foi provisionada devemos executar o workflow destroy.yml manualmente.
Após a execução todos recursos na AWS foram deletados.
Conclusão
Utilizando pipelines CD/CD é possível entregar ambientes completos de forma automática e rápida. Se você notou criamos todos os recurso dentro da AWS sem precisar dar ao menos um clique dentro do painel administrativo. Use e abuse da infraestrutura como código (IaC). Espero que tenha gostado desse hands-on, até a próxima! ✌🏼
Posted on June 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.