├── fullcycle-infra-main └── fullcycle-infra-main │ ├── .github │ └── workflows │ │ ├── 0-feature.yml │ │ ├── 1-main.yml │ │ ├── cd-terraform.yml │ │ └── ci-terraform.yml │ ├── README.md │ ├── main.tf │ └── settings.yml ├── post-morten.md └── python-image-main └── python-image-main ├── .github └── workflows │ ├── 0-feature.yml │ ├── 1-main.yml │ ├── publish.yml │ └── python-ci.yml ├── Dockerfile ├── settings.yml ├── sonar-project.properties └── src ├── app.py ├── requirements.txt └── test_app.py /fullcycle-infra-main/fullcycle-infra-main/.github/workflows/0-feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | on: 3 | push: 4 | branches: 5 | - 'feature**' 6 | - 'fix**' 7 | 8 | jobs: 9 | ci-terraform: 10 | uses: ./.github/workflows/ci-terraform.yml 11 | secrets: inherit -------------------------------------------------------------------------------- /fullcycle-infra-main/fullcycle-infra-main/.github/workflows/1-main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | branches: 7 | - 'main' 8 | 9 | jobs: 10 | terraform-apply: 11 | if: github.event.pull_request.merged == true 12 | uses: ./.github/workflows/cd-terraform.yml 13 | secrets: inherit -------------------------------------------------------------------------------- /fullcycle-infra-main/fullcycle-infra-main/.github/workflows/cd-terraform.yml: -------------------------------------------------------------------------------- 1 | name: 'CD Terraform' 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | actions: write 7 | pull-requests: write 8 | packages: read 9 | 10 | on: 11 | workflow_call: 12 | inputs: 13 | settings-file: 14 | description: 'Path to the settings file' 15 | required: false 16 | type: string 17 | default: 'settings.yml' 18 | runs-on: 19 | description: 'Runner label to use' 20 | required: false 21 | type: string 22 | default: 'ubuntu-latest' 23 | terraform-destroy: 24 | required: false 25 | type: string 26 | default: 'false' 27 | 28 | jobs: 29 | cd-terraform: 30 | runs-on: ${{ inputs.runs-on }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Run YAML to Github Output Action 35 | id: settings-parser 36 | uses: christian-ci/action-yaml-github-output@v2 37 | with: 38 | file_path: './${{ inputs.settings-file }}' 39 | 40 | - uses: aws-actions/configure-aws-credentials@v4 41 | with: 42 | aws-region: ${{ steps.settings-parser.outputs.aws-region }} 43 | role-to-assume: "arn:aws:iam::${{ steps.settings-parser.outputs.account-id }}:role/${{ github.event.repository.name }}" 44 | mask-aws-account-id: false 45 | role-skip-session-tagging: true 46 | role-duration-seconds: 3600 47 | 48 | - uses: hashicorp/setup-terraform@v3 49 | with: 50 | terraform_version: ${{ steps.settings-parser.outputs.terraform-version }} 51 | 52 | - name: Terraform Init 53 | id: init 54 | run: terraform init 55 | 56 | - name: Terraform Plan 57 | id: plan 58 | run: terraform plan -no-color 59 | 60 | - name: Terraform Apply 61 | run: terraform apply -auto-approve -input=false -------------------------------------------------------------------------------- /fullcycle-infra-main/fullcycle-infra-main/.github/workflows/ci-terraform.yml: -------------------------------------------------------------------------------- 1 | name: 'CI Terraform' 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | actions: write 7 | pull-requests: write 8 | packages: read 9 | 10 | on: 11 | workflow_call: 12 | inputs: 13 | settings-file: 14 | description: 'Path to the settings file' 15 | required: false 16 | type: string 17 | default: 'settings.yml' 18 | runs-on: 19 | description: 'Runner label to use' 20 | required: false 21 | type: string 22 | default: 'ubuntu-latest' 23 | 24 | jobs: 25 | ci-terraform: 26 | runs-on: ${{ inputs.runs-on }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Run YAML to Github Output Action 31 | id: settings-parser 32 | uses: christian-ci/action-yaml-github-output@v2 33 | with: 34 | file_path: './${{ inputs.settings-file }}' 35 | 36 | - uses: aws-actions/configure-aws-credentials@v4 37 | with: 38 | aws-region: ${{ steps.settings-parser.outputs.aws-region }} 39 | role-to-assume: "arn:aws:iam::${{ steps.settings-parser.outputs.account-id }}:role/${{ github.event.repository.name }}" 40 | mask-aws-account-id: false 41 | role-skip-session-tagging: true 42 | role-duration-seconds: 3600 43 | 44 | - uses: hashicorp/setup-terraform@v3 45 | with: 46 | terraform_version: ${{ steps.settings-parser.outputs.terraform-version }} 47 | 48 | - name: Terraform fmt 49 | id: fmt 50 | run: terraform fmt -check 51 | continue-on-error: true 52 | 53 | - name: Terraform Init 54 | id: init 55 | run: terraform init 56 | 57 | - name: Terraform Validate 58 | id: validate 59 | run: terraform validate -no-color 60 | 61 | - name: Terraform Plan 62 | id: plan 63 | run: terraform plan -no-color 64 | continue-on-error: true 65 | 66 | create-pr: 67 | name: Create PR to Main 68 | runs-on: ${{ inputs.runs-on }} 69 | needs: 70 | - ci-terraform 71 | steps: 72 | - uses: actions/checkout@v4 73 | 74 | - name: pull-request-action 75 | uses: vsoch/pull-request-action@master 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | PULL_REQUEST_BRANCH: "main" -------------------------------------------------------------------------------- /fullcycle-infra-main/fullcycle-infra-main/README.md: -------------------------------------------------------------------------------- 1 | # fullcycle-infra -------------------------------------------------------------------------------- /fullcycle-infra-main/fullcycle-infra-main/main.tf: -------------------------------------------------------------------------------- 1 | module "ec2_instance" { 2 | source = "terraform-aws-modules/ec2-instance/aws" 3 | 4 | name = "single-instance" 5 | 6 | instance_type = "t2.micro" 7 | monitoring = false 8 | vpc_security_group_ids = ["sg-06f76a53f08a418d4"] 9 | subnet_id = "subnet-0bfb6070cab586136" 10 | 11 | tags = { 12 | Terraform = "true" 13 | Environment = "dev" 14 | Name = "Teste pipeline" 15 | } 16 | } 17 | 18 | terraform { 19 | backend "s3" { 20 | bucket = "teste-repo-fullcycle" 21 | key = "teste" 22 | region = "sa-east-1" 23 | } 24 | } -------------------------------------------------------------------------------- /fullcycle-infra-main/fullcycle-infra-main/settings.yml: -------------------------------------------------------------------------------- 1 | aws-region: 'sa-east-1' 2 | terraform-version: '1.1.7' 3 | account-id: '137068236554' 4 | -------------------------------------------------------------------------------- /post-morten.md: -------------------------------------------------------------------------------- 1 | # Incidente aumento de custos nas contas AWS 2 | 3 | Essa página contém o post mortem do problema do microsserviço `XPTO` relacionado a finops ocorrido na release 1 durante a sprint 1 do ano de 2024. 4 | 5 | ## Informações Importantes 6 | 7 | **Data:** 30/08/2024 8 | 9 | **Autores:** 10 | 11 | Squad `Nome Squad back-end`: Danilo 12 | 13 | Squad `Nome Squad SRE`: João 14 | 15 | **Status:** Concluído, itens de ação em andamento 16 | 17 | **Resumo:** 18 | Geração de custos excessivos nas contas que suportam os microsserviço `XPTO` devido a alta geração de logs sendo ingeridos pelo cloud watch. 19 | 20 | **Duração do problema:** 21 | 22 | Os custos aumentaram desde o dia 09/08/2024 nas contas de desenvolvimento, homologação e produção e normalizaram dia 30/08/2024. 23 | 24 | **Impacto:** 25 | 26 | Impacto no finops, o custo diário com Cloud Watch considerando um cenário normal de ingestão é de 100 dólares, durante o período de impacto os custos ficaram com uma média de 250 dólares totalizando 3.150 doláres adicionais. Durante o problema não houve impacto em funcionalidades apra o cliente final. 27 | 28 | **Causa Raiz:** 29 | 30 | Identificado que após o dia 09/08/2024 foi implantado a versão `2.0.0` do microsserviço `XPTO` no qual foi inserido um bug no ambiente. 31 | 32 | **O que desencadeou:** 33 | 34 | Foi desencadeado por um deploy no dia 09/08/24 (`ID da Change`) onde foi realizado o rollout do microsserviço `XPTO` para a versão `2.0.0`. 35 | 36 | **Detecção:** 37 | 38 | Detecção manual através do time de SRE estava buscando por oportunidades de redução de custos durante o On-Call. 39 | 40 | **Resolução:** 41 | 42 | _Pontual_ 43 | 44 | Após identificar que o microsserviço `XPTO` estava gerando logs de forma atípica foi implementado um filtro no fluentbit (addon que faz shipping de logs) para que os logs gerados não fossem enviados para o backend que nesse caso era o serviço AWS Cloud Watch. 45 | 46 | _Definitiva_ 47 | 48 | Será implementado um fix no microsserviço `XPTO` para implementação de sampling de logs através da lib zap utilizada para geração dos logs, sendo assim sempre será enviado apenas uma amostragem dos logs gerados, em caso de bugs similares não será gerado o mesmo impacto nos ambientes. 49 | 50 | ## Lições aprendidas 51 | 52 | **Coisas que correram bem** 53 | 54 | - Aplicação de uma medida paliativa com tempestividade uma vez que o problema foi identificado 55 | 56 | **Coisas que correram mal** 57 | 58 | - Não houve um processo de validação de change efetivo pós implementação 59 | - Não haviam alertas de custos nas contas AWS 60 | - Não houve um processo de validação nos ambientes de desenvolvimento e homologação 61 | - Falta de testes automatizados 62 | - Não havia alerta para detecção de anomalia nas contas AWS 63 | - Não havia alertas para alta ingestão de logs no Cloud Watch 64 | 65 | **Onde tivemos sorte?** 66 | 67 | - Detecção manual através investigação de oportunidades de finops realizadas de forma periódica 68 | 69 | **Itens de ação:** 70 | 71 | **Prevenção/Mitigação de riscos** 72 | 73 | | **Ação** | **Tipo** | **Componente** | **Prioridade** | **Proprietário** | 74 | | ------------------------------------- | -------- | -------------- | -------------- | ---------------- | 75 | | Implementação de testes automatizados | mitigar | `XPTO` | P2 | Danilo | 76 | | Implementar probes do kubernetes | mitigar | `XPTO` | P1 | Danilo | 77 | 78 | **Monitoramento/Alertas** 79 | 80 | | **Ação** | **Tipo** | **Componente** | **Prioridade** | **Proprietário** | 81 | | ------------------------------------------------------- | -------- | -------------- | -------------- | ---------------- | 82 | | Criar alertas de custos nas contas AWS | evitar | monitoria | P0 | João | 83 | | Criar alertas para alta ingestão de logs do Cloud Watch | evitar | monitoria | P0 | João | 84 | | Criar alertas para detecção de anomalia nas contas AWS | mitigar | monitoria | P1 | João | 85 | 86 | **Processos/Resposta de incidentes** 87 | 88 | | **Ação** | **Tipo** | **Componente** | **Prioridade** | **Proprietário** | 89 | | ----------------------------------------------------------------- | -------- | -------------- | -------------- | ---------------- | 90 | | criar runbooks operacionais para validação do ambiente pós change | mitigar | N/A | P0 | João / Danilo | 91 | 92 | ## Timeline 93 | 94 | 09/08/2024 95 | 96 | - Executado a change `ID da Change` para atualização do microsserviço `XPTO` para versão `2.0.0` 97 | 98 | 30/08/2024 99 | Manhã: 100 | 101 | - João da squad `squad SRE` estava buscando por oportunidades de finops nas contas AWS 102 | - João percebeu a anomalia nos custos de Cloud Watch nas contas de produção 103 | - João encontrou uma query nas documentações da AWS para localizar os log groups com maior volume de ingestão diária 104 | - João acionou o Danilo da squad `squad backend` para iniciar um troubleshooting mais focado no microsserviço `XPTO` 105 | - Identificado que o alto volume de ingestão de logs iniciou após a change `ID da Change` 106 | - João adicionou um filtro no fluentbit para exclusão do envio dos logs do microsserviço `XPTO` para o Cloud Watch 107 | - Identificado que o impacto foi mitigado pois os logs deixaram de ser registrados no log group 108 | 109 | ## Evidências 110 | 111 | Hisórico de consumo do Cloud Watch na conta de produção 112 | 113 | `Adicionar prints` 114 | 115 | Hisórico de consumo do Cloud Watch na conta de homologação 116 | 117 | `Adicionar prints` 118 | 119 | Hisórico de consumo do Cloud Watch na conta de desenvolvimento 120 | 121 | `Adicionar prints` 122 | -------------------------------------------------------------------------------- /python-image-main/python-image-main/.github/workflows/0-feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature / Fix branch 2 | on: 3 | push: 4 | branches: 5 | - 'feature**' 6 | - 'fix**' 7 | 8 | jobs: 9 | python-ci: 10 | uses: ./.github/workflows/python-ci.yml 11 | secrets: inherit -------------------------------------------------------------------------------- /python-image-main/python-image-main/.github/workflows/1-main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | branches: 7 | - 'main' 8 | 9 | jobs: 10 | publish-image: 11 | if: github.event.pull_request.merged == true 12 | uses: ./.github/workflows/publish.yml 13 | secrets: inherit -------------------------------------------------------------------------------- /python-image-main/python-image-main/.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish Image' 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | actions: write 7 | pull-requests: write 8 | packages: read 9 | 10 | on: 11 | workflow_call: 12 | inputs: 13 | settings-file: 14 | description: 'Path to the settings file' 15 | required: false 16 | type: string 17 | default: 'settings.yml' 18 | docker-file-path: 19 | description: 'Path to the Dockerfile' 20 | required: false 21 | type: string 22 | default: '.' 23 | runs-on: 24 | description: 'Runner label to use' 25 | required: false 26 | type: string 27 | default: 'ubuntu-latest' 28 | 29 | jobs: 30 | build-image: 31 | name: Build Image 32 | runs-on: ${{ inputs.runs-on }} 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Run YAML to Github Output Action 37 | id: settings-parser 38 | uses: christian-ci/action-yaml-github-output@v2 39 | with: 40 | file_path: './${{ inputs.settings-file }}' 41 | 42 | - name: SonarCloud Scan 43 | uses: SonarSource/sonarcloud-github-action@master 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 47 | 48 | - name: Login to Docker Hub 49 | uses: docker/login-action@v3 50 | with: 51 | username: ${{ secrets.DOCKERHUB_USERNAME }} 52 | password: ${{ secrets.DOCKERHUB_TOKEN }} 53 | 54 | - name: Build Docker Image 55 | working-directory: '${{ inputs.docker-file-path }}' 56 | run: | 57 | docker build -t ${{ steps.settings-parser.outputs.registry}}/${{ steps.settings-parser.outputs.repository}}:${GITHUB_SHA:0:7} . 58 | 59 | - name: Push Docker Image 60 | run: | 61 | docker push ${{ steps.settings-parser.outputs.registry}}/${{ steps.settings-parser.outputs.repository}}:${GITHUB_SHA:0:7} 62 | working-directory: '${{ inputs.docker-file-path }}' -------------------------------------------------------------------------------- /python-image-main/python-image-main/.github/workflows/python-ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Python CI' 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | actions: write 7 | pull-requests: write 8 | packages: read 9 | 10 | on: 11 | workflow_call: 12 | inputs: 13 | settings-file: 14 | description: 'Path to the settings file' 15 | required: false 16 | type: string 17 | default: 'settings.yml' 18 | docker-file-path: 19 | description: 'Path to the Dockerfile' 20 | required: false 21 | type: string 22 | default: '.' 23 | runs-on: 24 | description: 'Runner label to use' 25 | required: false 26 | type: string 27 | default: 'ubuntu-latest' 28 | 29 | jobs: 30 | ci-image: 31 | runs-on: ${{ inputs.runs-on }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Run YAML to Github Output Action 36 | id: settings-parser 37 | uses: christian-ci/action-yaml-github-output@v2 38 | with: 39 | file_path: './${{ inputs.settings-file }}' 40 | 41 | - name: Set up Python 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: '3.12' 45 | 46 | - name: Install dependencies 47 | run: | 48 | python -m pip install --upgrade pip 49 | pip install --no-cache -r ${{ steps.settings-parser.outputs.dependencies-path }} 50 | 51 | - name: Linting 52 | run: | 53 | pip install pylint 54 | pylint --errors-only src/*.py 55 | 56 | - name: Pytest 57 | run: | 58 | pip install pytest 59 | pytest src 60 | 61 | - name: Upload pytest test results 62 | uses: actions/upload-artifact@v4 63 | # Use always() to always run this step to publish test results when there are test failures 64 | if: ${{ always() }} 65 | with: 66 | name: pytest-results 67 | path: test-results.xml 68 | 69 | create-pr: 70 | name: Create PR to Main 71 | runs-on: ${{ inputs.runs-on }} 72 | needs: 73 | - ci-image 74 | steps: 75 | - uses: actions/checkout@v4 76 | 77 | - name: pull-request-action 78 | uses: vsoch/pull-request-action@master 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | PULL_REQUEST_BRANCH: "main" -------------------------------------------------------------------------------- /python-image-main/python-image-main/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY src . 6 | 7 | RUN pip install --no-cache -r requirements.txt 8 | 9 | EXPOSE 9000 10 | 11 | CMD ["python", "app.py"] -------------------------------------------------------------------------------- /python-image-main/python-image-main/settings.yml: -------------------------------------------------------------------------------- 1 | registry: 'manoelmineiro' 2 | repository: 'python-image' 3 | dependencies-path: 'src/requirements.txt' -------------------------------------------------------------------------------- /python-image-main/python-image-main/sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=manoelmineiro_python-image 2 | sonar.organization=manoelmineiro 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | sonar.projectName=python-image 6 | sonar.projectVersion=1.0 7 | 8 | 9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 10 | #sonar.sources=. 11 | 12 | # Encoding of the source code. Default is default system encoding 13 | #sonar.sourceEncoding=UTF-8 14 | 15 | sonar.python.version=3.12 -------------------------------------------------------------------------------- /python-image-main/python-image-main/src/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | times = [] 6 | campeonatos = [] 7 | 8 | @app.route('/') 9 | def index(): 10 | return "Seja bem-vindo!" 11 | 12 | @app.route('/times', methods=['GET', 'POST']) 13 | def handle_times(): 14 | if request.method == 'GET': 15 | return jsonify(times) 16 | elif request.method == 'POST': 17 | time = request.json 18 | times.append(time) 19 | return jsonify({'message': 'Time adicionado com sucesso!'}) 20 | 21 | @app.route('/campeonatos', methods=['GET', 'POST']) 22 | def handle_campeonatos(): 23 | if request.method == 'GET': 24 | return jsonify(campeonatos) 25 | elif request.method == 'POST': 26 | campeonato = request.json 27 | campeonatos.append(campeonato) 28 | return jsonify({'message': 'Campeonato adicionado com sucesso!'}) 29 | 30 | @app.errorhandler(404) 31 | def page_not_found(e): 32 | return jsonify({'error': 'Página não encontrada'}), 404 33 | 34 | if __name__ == '__main__': 35 | app.run(debug=False, host="0.0.0.0", port='9000') -------------------------------------------------------------------------------- /python-image-main/python-image-main/src/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.3 2 | Werkzeug==3.0.3 3 | pytest==8.3.2 4 | pytest-cov==5.0.0 -------------------------------------------------------------------------------- /python-image-main/python-image-main/src/test_app.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import Flask 3 | from app import app 4 | 5 | @pytest.fixture 6 | def client(): 7 | return app.test_client() 8 | 9 | def test_index(client): 10 | response = client.get('/') 11 | assert response.status_code == 200 12 | assert response.data.decode('utf-8') == 'Seja bem-vindo!' 13 | 14 | def test_get_times(client): 15 | response = client.get('/times') 16 | assert response.status_code == 200 17 | assert response.json == [] 18 | 19 | def test_create_time(client): 20 | response = client.post('/times', json={"nome": "Time A"}) 21 | assert response.status_code == 200 22 | assert response.json['message'] == 'Time adicionado com sucesso!' 23 | 24 | response = client.get('/times') 25 | assert response.json == [{"nome": "Time A"}] 26 | 27 | def test_get_campeonatos(client): 28 | response = client.get('/campeonatos') 29 | assert response.status_code == 200 30 | assert response.json == [] 31 | 32 | def test_create_campeonato(client): 33 | response = client.post('/campeonatos', json={"nome": "Campeonato A"}) 34 | assert response.status_code == 200 35 | assert response.json['message'] == 'Campeonato adicionado com sucesso!' 36 | 37 | response = client.get('/campeonatos') 38 | assert response.json == [{"nome": "Campeonato A"}] 39 | 40 | def test_404_error(client): 41 | response = client.get('/nonexistent') 42 | assert response.status_code == 404 43 | assert response.json['error'] == 'Página não encontrada' 44 | 45 | if __name__ == '__main__': 46 | pytest.main() 47 | --------------------------------------------------------------------------------