Ir para o conteúdo

Terraform template

Backend submodule

Após Configurar o Azure para Terraform, vamos configurar o backend do Terraform, que será o local onde ele irá salvar todos os "Terraform States".

Podemos configurar diferentes tipos de backend, tal como documentado aqui: https://developer.hashicorp.com/terraform/language/backend , é possivel usar mais do que um provider em simultaneo.

De momento vamos configurar apenas backend no Microsoft Azure, mas como mais tarde poderemos querer alterar este provider, foi criado o projecto Terraform_Backend no gitlab onde apenas está configurado o backend, e todos os projectos de Terraform devem importar este projecto como um git submodule, isso facilitará uma alteração futura, apenas actualizando esse repositório, e executando git submodule update --init && git submodule update --remote em todos os restantes repositórios para actualizar o git submodule.

Este projecto apenas contem 3 ficheiros, um por cada ambiente, fica aqui o exemplo do ficheiro para o ambiente de "desenvolvimento", em que indica ao Terraform para salvar os ficheiros de state entro de uma storage account:

backend-config-dev.tfvars
1
2
3
4
resource_group_name   = "terraform-state-dev"
storage_account_name  = "primetagterraformdev"
container_name        = "terraform-state-dev"
key                   = "terraform_state_file-dev.tfstate"

Template generico para todos os projectos

O projecto Terraform_Backend com o template que dá origem a todos os projectos de Terraform, sempre que for necessário criar um novo repositório, é clonado a partir deste e de seguida é adicionado os modulos que são para ser criados na infraestrutura.

O projecto contem os seguintes ficheiros:

  • .terraform-version: Indica qual a versão de terraform a usar (tanto localmente, como quando as pipelines de CI/CD forem executadas)

    .terraform-version
    1.1.9
    

  • .auto.tfvars: É onde são colocadas todas as váriaveis transversais a todos os ambientes (não inclui secrets)

    .auto.tfvars
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /*
    Default variables to be injected inside terraform when terraform starts.
    
    "Terraform automatically loads all files in the current directory with the exact name of terraform.tfvars or
    any variation of *.auto.tfvars"
    In: https://learn.hashicorp.com/tutorials/terraform/aws-variables#from-a-file
    */
    
    location = "westeurope"
    

  • .env-dev.tfvars, .env-staging.tfvars, .env-prod.tfvars: É onde são colocadas cada variável de cada ambiente especifíco (não inclui secrets)

    .env-dev.tfvars
    resource_group_name = "primetag-dev"
    environment         = "Dev"
    

  • providers.tf: É onde são colocadas cada variável de cada ambiente especifíco (não inclui secrets)

    .env-dev.tfvars
    resource_group_name = "primetag-dev"
    environment         = "Dev"
    

  • variables.tf: É onde são definidas todas as váriáveis que o Terraform vai ler, incluíndo os secrets que serão injectados pelas pipelines durante a execução

    variables.tf
    variable location {}
    variable resource_group_name {}
    variable environment {
      type = string
      description = "\"prod\" | \"staging\"" | \"dev\""
      # TODO(Rui): Insert a `validation {}` entry with an regex validation and an error_message
    }
    
    # THESE ARE EXAMPLES, DELETE THE ONES THAT YOU DON'T NEED:
    
    /*
    variable gitlab_token {
      type = string
      description = "Gitlab user token that allows terraform provider write GitLab variables"
    }
    */
    
    /*
    variable gitlab_auth_json {
      type = string
      description = "File with json like '{ \"registry.gitlab.com\": { \"username\": \"ruimartins-prime\", \"password\": \"<token>\", \"email\": \"[email protected]\" } }'"
    }
    */
    
    /*
    variable kubernetes_ip_to_firewall {
      type = string
      description = "IP(s) of cluster DEV or PROD that is going to connect to databases. Put IPs like 'x.x.x.x,y.y.y.y,z.z.z.z'."
    }
    */
    
    
    #variable service_principal_client_id {
    #  type = string
    #  description = "Azure Service Principal ID to Manage azure resources through command line or terraform"
    #}
    #variable service_principal_client_secret {
    #  type = string
    #  description = "Azure Service Principal Password to Manage azure resources through command line or terraform"
    #}
    
    #variable mysql_server_2_root_username {
    #  type = string
    #  description = "Username for administrator of MySQL-Server-2"
    #}
    #variable mysql_server_2_root_password {
    #  type = string
    #  description = "Password for administrator of MySQL-Server-2"
    #}
    
    #variable cloudflare_api_token {
    #  # https://dash.cloudflare.com/profile/api-tokens
    #  # https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs#api_token
    #  type = string
    #  description = "Cloudflare token that allows terraform provider write in Cloudflare settings"
    #}
    
    #variable api_client_logging {
    #  # https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs#api_client_logging
    #  type = string
    #  default = "true"
    #  description = "Whether to print logs from the API client (using the default log library logger)"
    #}
    
    #variable cloudflare_zone_id_primetag_com {
    #  # Get for example here: https://dash.cloudflare.com/*********/primetag.com
    #  type = string
    #}
    
    #variable cloudflare_zone_id_primetag_net {
    #  # Get for example here: https://dash.cloudflare.com/******/primetag.net
    #  type = string
    #}
    

  • .gitlab-ci.yml: Ficheiro de pipelines do Gitlab

    .gitlab-ci.yml
    image:
      # Check last version here: https://hub.docker.com/_/alpine
      # The version to use in all pipelines is centralized in Gitlab Environment Variables
      name: alpine:$ALPINE_DOCKER_TAG
    
    variables:
     WORKSPACE: "TERRAFORM-PROJECT-TEMPLATE" # << REPLACE THIS FIELD <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<---- UPDATE THIS FIELD
     TF_VAR_gitlab_token: "${GL_TOKEN}"
    
     GIT_DOCKER_TAG: "2.36.0"  # https://hub.docker.com/r/bitnami/git
    
    cache:
      key: primetag_gitlab_cache
      paths:
        - terraform-plugin-cache
    
    stages:
      - PrepareVersions
      - MergeRequest
      - GenerateBuildNumber
      - Plan
      - Apply
      - TagBuildNumber
      - TagVersion
    
    PrepareVersionsMR:
      stage: PrepareVersions
      only:
        - merge_requests
      script:
        # https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html#artifactsreportsdotenv
        # The .env file can’t have empty lines or comments (starting with #).
        # Key values in the env file cannot have spaces or newline characters (\n), including when using single or double quotes.
        - echo "TERRAFORM_DOCKER_TAG=$(head -1 .terraform-version)" >> versions.env
        - cat versions.env
      artifacts:
        reports:
          dotenv: versions.env
    
    PrepareVersionsMaster:
      stage: PrepareVersions
      only:
        - main
      script:
        # https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html#artifactsreportsdotenv
        # The .env file can’t have empty lines or comments (starting with #).
        # Key values in the env file cannot have spaces or newline characters (\n), including when using single or double quotes.
        - echo "TERRAFORM_DOCKER_TAG=$(head -1 .terraform-version)" >> versions.env
        - cat versions.env
      artifacts:
        reports:
          dotenv: versions.env
    
    PrepareVersionsDev:
      stage: PrepareVersions
      only:
        - /build/
      except:
        - branches
      script:
        # https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html#artifactsreportsdotenv
        # The .env file can’t have empty lines or comments (starting with #).
        # Key values in the env file cannot have spaces or newline characters (\n), including when using single or double quotes.
        - echo "TERRAFORM_DOCKER_TAG=$(head -1 .terraform-version)" >> versions.env
        - cat versions.env
      artifacts:
        reports:
          dotenv: versions.env
    
    PrepareVersionsProd:
      stage: PrepareVersions
      only:
        - /releases/
      except:
        - branches
      script:
        # https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html#artifactsreportsdotenv
        # The .env file can’t have empty lines or comments (starting with #).
        # Key values in the env file cannot have spaces or newline characters (\n), including when using single or double quotes.
        - echo "TERRAFORM_DOCKER_TAG=$(head -1 .terraform-version)" >> versions.env
        - cat versions.env
      artifacts:
        reports:
          dotenv: versions.env
    
    .job_template: &job_init
      terraform init -backend-config=terraform-backend/backend-config-"${ENV}".tfvars -var-file=".env-${ENV}.tfvars" &&
        (terraform workspace select ${WORKSPACE}-${ENV} || terraform workspace new ${WORKSPACE}-${ENV})
    
    .job_template: &job_validate
      terraform validate
    
    .job_template: &job_refresh
      terraform refresh -input=false -var-file=".env-${ENV}.tfvars"
    
    .job_template: &job_plan
      terraform plan -input=false  -var-file=".env-${ENV}.tfvars" -out "planfile-${ENV}"
    #  terraform plan -var "gcloud_project=youclap-$ENV" -var-file=$SECRETS_FILE -var "global_environment=$ENV" -input=false -out "planfile-$ENV"
    
    .job_template: &job_apply
      terraform apply
        -auto-approve
        -input=false "planfile-${ENV}"
    
    .job_template: &job_show
      terraform show
    
    PlanMRDev:
      image:
        name: hashicorp/terraform:$TERRAFORM_DOCKER_TAG
        entrypoint: [ "" ]
      stage: MergeRequest
      only:
        - merge_requests
      variables:
        ENV: "dev"
        ARM_CLIENT_ID: $ARM_CLIENT_ID_DEV
        ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET_DEV
        ARM_SUBSCRIPTION_ID: $ARM_SUBSCRIPTION_ID_DEV
        ARM_TENANT_ID: $ARM_TENANT_ID_DEV
      script:
        - *job_init
        - *job_validate
        - *job_refresh
        - *job_plan
      artifacts:
        reports:
          terraform: planfile-${ENV}
    
    PlanMRProd:
      image:
        name: hashicorp/terraform:$TERRAFORM_DOCKER_TAG
        entrypoint: [ "" ]
      stage: MergeRequest
      only:
        - merge_requests
      # environment:
      #   name: production
      variables:
        ENV: "prod"
        ARM_CLIENT_ID: $ARM_CLIENT_ID_PROD
        ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET_PROD
        ARM_SUBSCRIPTION_ID: $ARM_SUBSCRIPTION_ID_PROD
        ARM_TENANT_ID: $ARM_TENANT_ID_PROD
      script:
        - *job_init
        - *job_validate
        - *job_refresh
        - *job_plan
      artifacts:
        reports:
          terraform: planfile-${ENV}
    
    GenerateBuildNumber:
      stage: GenerateBuildNumber
      image: bitnami/git:$GIT_DOCKER_TAG
      # environment:
      #   name: development
      only:
        - main
      script:
        - git rev-list --count origin/main > .build_number
        - echo "Build number:"$(cat .build_number)
      artifacts:
        paths:
          - .build_number
        expire_in: 1 year
    
    PlanDev:
      image:
        name: hashicorp/terraform:$TERRAFORM_DOCKER_TAG
        entrypoint: [ "" ]
      stage: Plan
      # environment:
      #   name: development
      variables:
        ENV: "dev"
        ARM_CLIENT_ID: $ARM_CLIENT_ID_DEV
        ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET_DEV
        ARM_SUBSCRIPTION_ID: $ARM_SUBSCRIPTION_ID_DEV
        ARM_TENANT_ID: $ARM_TENANT_ID_DEV
      only:
        - main
      script:
        - *job_init
        - *job_validate
        - *job_refresh
        - *job_plan
      artifacts:
        reports:
          terraform: planfile-${ENV}
    
    PlanProd:
      image:
        name: hashicorp/terraform:$TERRAFORM_DOCKER_TAG
        entrypoint: [ "" ]
      stage: Plan
      variables:
        ENV: "prod"
        ARM_CLIENT_ID: $ARM_CLIENT_ID_PROD
        ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET_PROD
        ARM_SUBSCRIPTION_ID: $ARM_SUBSCRIPTION_ID_PROD
        ARM_TENANT_ID: $ARM_TENANT_ID_PROD
      only:
        - main
      script:
        - *job_init
        - *job_validate
        - *job_refresh
        - *job_plan
      artifacts:
        reports:
          terraform: planfile-${ENV}
    
    ApplyDEV:
      image:
        name: hashicorp/terraform:$TERRAFORM_DOCKER_TAG
        entrypoint: [ "" ]
      stage: Apply
      # allow_failure: false
      environment:
        name: development
      variables:
        ENV: "dev"
        ARM_CLIENT_ID: $ARM_CLIENT_ID_DEV
        ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET_DEV
        ARM_SUBSCRIPTION_ID: $ARM_SUBSCRIPTION_ID_DEV
        ARM_TENANT_ID: $ARM_TENANT_ID_DEV
    
        TF_VAR_service_principal_client_id: $ARM_CLIENT_ID_DEV
        TF_VAR_service_principal_client_secret: $ARM_CLIENT_SECRET_DEV
        TF_VAR_mysql_server_2_root_username: $MYSQL_SERVER_2_ROOT_USERNAME_DEV
        TF_VAR_mysql_server_2_root_password: $MYSQL_SERVER_2_ROOT_PASSWORD_DEV
    #  when: manual
      only:
        - /build/
      except:
        - branches
      script:
        - *job_init
        - *job_refresh
        - *job_plan
        - *job_apply
        - *job_show
    
    ApplyPROD:
      image:
        name: hashicorp/terraform:$TERRAFORM_DOCKER_TAG
        entrypoint: [ "" ]
      stage: Apply
      allow_failure: false
      # when: manual
      #environment:
      #  name: production
      variables:
        ENV: "prod"
        ARM_CLIENT_ID: $ARM_CLIENT_ID_PROD
        ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET_PROD
        ARM_SUBSCRIPTION_ID: $ARM_SUBSCRIPTION_ID_PROD
        ARM_TENANT_ID: $ARM_TENANT_ID_PROD
      only:
        - /releases/
      except:
        - branches
      script:
        - *job_init
        - *job_refresh
        - *job_plan
        - *job_apply
        - *job_show
    
    
    TagVersionProd:
      stage: TagVersion
      image: registry.gitlab.com/juhani/go-semrel-gitlab:$GSG_DOCKER_TAG
      allow_failure: false
      when: manual
      variables:
        GSG_TAG_PREFIX: "releases/"
      only:
        - main
      script:
        - release next-version
        - release tag
    
    TagBuildNumberDev:
      stage: TagBuildNumber
      image: bitnami/git:$GIT_DOCKER_TAG
      # environment:
      #  name: development
      only:
        - main
      script:
        - BUILD_NUMBER=`cat .build_number`
        - echo "Build number $BUILD_NUMBER"
    
        - git tag builds/$BUILD_NUMBER
        - git remote set-url origin https://gitlab-ci-token:[email protected]/$CI_PROJECT_PATH.git/
        - git push origin builds/$BUILD_NUMBER
    

  • .gitmodules e terraform-backend: Submodule explicado no ponto anterior, responsavel pordefinir as configurações do backend do Terraform

    .gitmodules
    1
    2
    3
    4
    [submodule "terraform-backend"]
        path = terraform-backend
        url = ../terraform-backend.git
        branch = main
    

Pipelines de CI/CD

As pipelines de CI/CD em todos os projectos de infraestrutura passam pelos seguintes estados:

  • Quando é aberto um Merge Request por um programador correm os seguintes jobs:
    • CodeSmellGlitch: Exporta um artefacto com o relatório do Glitch (code smell)
    • TerraformValidate: Valida os ficheiros de Terraform para confirmar que são válidos (semelhante a um linter)
    • PlanMRDev: Planeia que alterações vão ser aplicadas em DEV, caso este MR seja merged
      • O ficheiro de planning é exportado para o Gitlab, de forma a dar feedback imediato aos reviewers
    • PlanMRStaging: Só disponivel em alguns projectos, vamos ignorar por agora
    • PlanMRProd: Planeia que alterações vão ser aplicadas em PROD, caso este MR seja merged, (e posteriorment seja enviado para produção)
      • O ficheiro de planning é exportado para o Gitlab, de forma a dar feedback imediato aos reviewers
  • Depois de um Merge Request ser aprovado e ser feito o Merge para o branch 'main', correm os seguintes jobs:
    • GenerateBuildNumber: É gerado um build number (Exemplo: 1.2.3-rc1)
    • PlanDev: Planeia novamente que alterações vão ser aplicadas em DEV, pois desde que o MR foi aberto até ter sido merged, podem ter ocorrido outras alterações à infraestrutura
    • PlanProd: Planeia novamente que alterações vão ser aplicadas em DEV, pois desde que o MR foi aberto até ter sido merged, podem ter ocorrido outras alterações à infraestrutura
    • TagBuildNumberDev: Adiciona automáticamente uma git tag ao código com a versão calculada no job GenerateBuildNumber (irá despoletar uma release automática para o ambiente de desenvolvimento)
    • TagVersionProd: Este job é manual, fica à espera que um developer venha manualmente clicar num botão para ser calculada a proxima versão de produçào e adicionada essa tag ao código, despoletando uma release para produção automáticamente
  • Sempre que for adicionada um tag com versão de DEV, executa os seguintes jobs:
    • ApplyDEV: Aplica as alterrações no ambiente de desenvolvimento
  • Sempre que for adicionada um tag com versão de PROD, executa os seguintes jobs:
    • ApplyPROD: Aplica as alterrações no ambiente de produção
MergeRequest GenerateBuildNumber Plan Apply TagBuildNumber TagVersion
Merge Request -TerraformValidate

-PlanMRDev

-PlanMRProd
After Merge -GenerateBuildNumber -PlanDev

-PlanProd
-TagBuildNumberDev -TagVersionProd
TagBuildNumber -ApplyDEV
TagVersion -ApplyPROD