Pesquisa de site

Gerencie OpenStack usando Terraform e GitLab


Siga este tutorial para ver como o uso do GitLab pode aprimorar ainda mais a colaboração em seu cluster OpenStack.

Uma virtude do GitOps é a infraestrutura como código. Ele incentiva a colaboração usando um repositório compartilhado de configurações e políticas. Usar o GitLab pode aprimorar ainda mais a colaboração em seu cluster OpenStack. O GitLab CI pode servir como seu centro de controle de origem e orquestração para CI/CD e pode até mesmo gerenciar o estado do Terraform.

Para conseguir isso, você precisa do seguinte:

  1. Conta ou instância do GitLab.
  2. Cluster OpenStack privado. Se você não tiver um, leia meu artigo Configure o OpenStack em um cluster Raspberry Pi.
  3. Um computador (de preferência um host contêiner).

Estado do GitLab e do Terraform

O objetivo é conseguir colaboração através do Terraform, então você precisa ter um arquivo de estado centralizado. GitLab possui um estado gerenciado para Terraform. Com esse recurso, você pode permitir que indivíduos gerenciem o OpenStack de forma colaborativa.

Crie um grupo e projeto no GitLab

Faça login no GitLab, clique no menu hambúrguer e clique em GruposVer todos os grupos.

(AJ Canlas, CC BY-SA 4.0)

Crie um grupo clicando em Novo grupo e depois em Criar grupo.

(AJ Canlas, CC BY-SA 4.0)

Dê um nome ao grupo para gerar um URL de grupo exclusivo e convide sua equipe para trabalhar com você.

(AJ Canlas, CC BY-SA 4.0)

Após criar um grupo, crie um projeto clicando em Criar novo projeto e depois em Criar projeto em branco:

(AJ Canlas, CC BY-SA 4.0)

Dê um nome ao seu projeto. GitLab gera um URL de projeto exclusivo para você. Este projeto contém o repositório para seus scripts e estado do Terraform.

Crie um token de acesso pessoal

O repositório precisa de um token de acesso pessoal para gerenciar este estado do Terraform. No seu perfil, selecione Editar perfil:

(AJ Canlas, CC BY-SA 4.0)

Clique em Token de acesso no painel lateral para acessar um menu para criar um token de acesso. Salve seu token porque você não poderá visualizá-lo novamente.

(AJ Canlas, CC BY-SA 4.0)

Clone o repositório vazio

Em um computador com acesso direto à instalação do OpenStack, clone o repositório e depois mude para o diretório resultante:

$ git clone git@gitlab.com:testgroup2170/testproject.git

$ cd testproject

Crie o arquivo .tf de back-end e o arquivo do provedor

Crie um arquivo de back-end para configurar o GitLab como seu back-end de estado:

$ cat >> backend.tf << EOF
terraform {
  backend "http" {
  }
}
EOF

Este arquivo de provedor extrai o provedor do OpenStack:

$ cat >> provider.tf << EOF
terraform {
  required_version = ">= 0.14.0"
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "1.49.0"
    }
  }
}

provider "openstack" {
  user_name   = var.OS_USERNAME
  tenant_name = var.OS_TENANT
  password    = var.OS_PASSWORD
  auth_url    = var.OS_AUTH_URL
  region      = var.OS_REGION
}
EOF

Como você declarou uma variável no provedor, você deve declará-la em um arquivo de variáveis:

$ cat >> variables.tf << EOF
variable "OS_USERNAME" {
  type        = string
  description = "OpenStack Username"
}

variable "OS_TENANT" {
  type        = string
  description = "OpenStack Tenant/Project Name"
}

variable "OS_PASSWORD" {
  type        = string
  description = "OpenStack Password"
}

variable "OS_AUTH_URL" {
  type        = string
  description = "OpenStack Identitiy/Keystone API for authentication"
}

variable "OS_REGION" {
  type        = string
  description = "OpenStack Region"
}

EOF

Como inicialmente você está trabalhando localmente, você deve definir essas variáveis para que funcione:

$ cat >> terraform.tfvars << EOF
OS_USERNAME = "admin"
OS_TENANT   = "admin"
OS_PASSWORD = "YYYYYYYYYYYYYYYYYYYYYY"
OS_AUTH_URL = "http://X.X.X.X:35357/v3"
OS_REGION   = "RegionOne"
EOF

Esses detalhes estão disponíveis em seu arquivo rc no OpenStack.

Inicialize o projeto no Terraform

Inicializar o projeto é bem diferente porque você precisa dizer ao Terraform para usar o GitLab como back-end de estado:

PROJECT_ID="<gitlab-project-id>"
TF_USERNAME="<gitlab-username>"
TF_PASSWORD="<gitlab-personal-access-token>"
TF_STATE_NAME="<your-unique-state-name>"
TF_ADDRESS="https://gitlab.com/api/v4/projects/${PROJECT_ID}/terraform/state/${TF_STATE_NAME}"

$ terraform init \
  -backend-config=address=${TF_ADDRESS} \
  -backend-config=lock_address=${TF_ADDRESS}/lock \
  -backend-config=unlock_address=${TF_ADDRESS}/lock \
  -backend-config=username=${TF_USERNAME} \
  -backend-config=password=${TF_PASSWORD} \
  -backend-config=lock_method=POST \
  -backend-config=unlock_method=DELETE \
  -backend-config=retry_wait_min=5

Para visualizar o gitlab-project-id, veja os detalhes do projeto logo acima da guia Informações do Projeto no painel lateral. Geralmente é o nome do seu projeto.

(AJ Canlas, CC BY-SA 4.0)

Para mim, é 42580143.

Use seu nome de usuário para gitlab-username. O meu é ajohnsc.

O gitlab-personal-access-token é o token que você criou anteriormente neste exercício. Neste exemplo, eu uso wwwwwwwwwwwwwwwwwwwww. Você pode nomear nome-do-seu-estado-único qualquer coisa. Usei homelab.

Aqui está meu script de inicialização:

PROJECT_ID="42580143"
TF_USERNAME="ajohnsc"
TF_PASSWORD="wwwwwwwwwwwwwwwwwwwww"
TF_STATE_NAME="homelab"
TF_ADDRESS="https://gitlab.com/api/v4/projects/${PROJECT_ID}/terraform/state/${TF_STATE_NAME}"

Para usar o arquivo:

$ terraform init \
  -backend-config=address=${TF_ADDRESS} \
  -backend-config=lock_address=${TF_ADDRESS}/lock \
  -backend-config=unlock_address=${TF_ADDRESS}/lock \
  -backend-config=username=${TF_USERNAME} \
  -backend-config=password=${TF_PASSWORD} \
  -backend-config=lock_method=POST \
  -backend-config=unlock_method=DELETE \
  -backend-config=retry_wait_min=5

A saída é semelhante a esta:

(AJ Canlas, CC BY-SA 4.0)

Teste o script Terraform

Isso define o tamanho das VMs para meus tipos de OpenStack:

$ cat >> flavors.tf << EOF
resource "openstack_compute_flavor_v2" "small-flavor" {
  name      = "small"
  ram       = "4096"
  vcpus     = "1"
  disk      = "0"
  flavor_id = "1"
  is_public = "true"
}

resource "openstack_compute_flavor_v2" "medium-flavor" {
  name      = "medium"
  ram       = "8192"
  vcpus     = "2"
  disk      = "0"
  flavor_id = "2"
  is_public = "true"
}

resource "openstack_compute_flavor_v2" "large-flavor" {
  name      = "large"
  ram       = "16384"
  vcpus     = "4"
  disk      = "0"
  flavor_id = "3"
  is_public = "true"
}

resource "openstack_compute_flavor_v2" "xlarge-flavor" {
  name      = "xlarge"
  ram       = "32768"
  vcpus     = "8"
  disk      = "0"
  flavor_id = "4"
  is_public = "true"
}
EOF

As configurações da minha rede externa são as seguintes:

$ cat >> external-network.tf << EOF
resource "openstack_networking_network_v2" "external-network" {
  name           = "external-network"
  admin_state_up = "true"
  external       = "true"
  segments {
    network_type     = "flat"
    physical_network = "physnet1"
  }
}

resource "openstack_networking_subnet_v2" "external-subnet" {
  name            = "external-subnet"
  network_id      = openstack_networking_network_v2.external-network.id
  cidr            = "10.0.0.0/8"
  gateway_ip      = "10.0.0.1"
  dns_nameservers = ["10.0.0.254", "10.0.0.253"]
  allocation_pool {
    start = "10.0.0.2"
    end   = "10.0.254.254"
  }
}
EOF

As configurações do roteador são assim:

$ cat >> routers.tf << EOF
resource "openstack_networking_router_v2" "external-router" {
  name                = "external-router"
  admin_state_up      = true
  external_network_id = openstack_networking_network_v2.external-network.id
}
EOF

Insira o seguinte para imagens:

$ cat >> images.tf << EOF
resource "openstack_images_image_v2" "cirros" {
  name             = "cirros"
  image_source_url = "https://download.cirros-cloud.net/0.6.1/cirros-0.6.1-x86_64-disk.img"
  container_format = "bare"
  disk_format      = "qcow2"
}
EOF

Aqui está um inquilino de demonstração:

$ cat >> demo-project-user.tf << EOF
resource "openstack_identity_project_v3" "demo-project" {
  name = "Demo"
}

resource "openstack_identity_user_v3" "demo-user" {
  name               = "demo-user"
  default_project_id = openstack_identity_project_v3.demo-project.id
  password = "demo"
}
EOF

Quando concluído, você terá esta estrutura de arquivos:

.
├── backend.tf
├── demo-project-user.tf
├── external-network.tf
├── flavors.tf
├── images.tf
├── provider.tf
├── routers.tf
├── terraform.tfvars
└── variables.tf

Plano de emissão

Após a conclusão dos arquivos, você pode criar os arquivos de plano com o comando terraform plan:

$ terraform plan
Acquiring state lock. This may take a few moments...

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # openstack_compute_flavor_v2.large-flavor will be created
  + resource "openstack_compute_flavor_v2" "large-flavor" {
      + disk         = 0
      + extra_specs  = (known after apply)
      + flavor_id    = "3"
      + id           = (known after apply)
      + is_public    = true
      + name         = "large"
      + ram          = 16384
      + region       = (known after apply)
      + rx_tx_factor = 1
      + vcpus        = 4
    }

[...]

Plan: 10 to add,
Releasing state lock. This may take a few moments...

Após todos os arquivos de plano terem sido criados, aplique-os com o comando terraform apply:

$ terraform apply -auto-approve
Acquiring state lock. This may take a few moments...
[...]
Plan: 10 to add, 0 to change, 0 to destroy.
openstack_compute_flavor_v2.large-flavor: Creating...
openstack_compute_flavor_v2.small-flavor: Creating...
openstack_identity_project_v3.demo-project: Creating...
openstack_networking_network_v2.external-network: Creating...
openstack_compute_flavor_v2.xlarge-flavor: Creating...
openstack_compute_flavor_v2.medium-flavor: Creating...
openstack_images_image_v2.cirros: Creating...
[...]
Releasing state lock. This may take a few moments...

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Após aplicar a infraestrutura, retorne ao GitLab e navegue até o seu projeto. Procure em InfraestruturaTerraform para confirmar se o estado homelab foi criado.

(AJ Canlas, CC BY-SA 4.0)

Destrua o estado para testar o CI

Agora que você criou um estado, tente destruir a infraestrutura para poder aplicar o pipeline de CI mais tarde. Claro, isso é apenas para migrar da CLI do Terraform para um Pipeline. Se você tiver uma infraestrutura existente, poderá pular esta etapa.

$ terraform destroy -auto-approve
Acquiring state lock. This may take a few moments...
openstack_identity_project_v3.demo-project: Refreshing state... [id=5f86d4229003404998dfddc5b9f4aeb0]
openstack_networking_network_v2.external-network: Refreshing state... [id=012c10f3-8a51-4892-a688-aa9b7b43f03d]
[...]
Plan: 0 to add, 0 to change, 10 to destroy.
openstack_compute_flavor_v2.small-flavor: Destroying... [id=1]
openstack_compute_flavor_v2.xlarge-flavor: Destroying... [id=4]
openstack_networking_router_v2.external-router: Destroying... [id=73ece9e7-87d7-431d-ad6f-09736a02844d]
openstack_compute_flavor_v2.large-flavor: Destroying... [id=3]
openstack_identity_user_v3.demo-user: Destroying... [id=96b48752e999424e95bc690f577402ce]
[...]
Destroy complete! Resources: 10 destroyed.

Agora você tem um estado que todos podem usar. Você pode provisionar usando um estado centralizado. Com o pipeline adequado, você pode automatizar tarefas comuns.

Configure um executor GitLab

Seu cluster OpenStack não é voltado ao público e a API OpenStack não está exposta. Você deve ter um executor GitLab para executar pipelines GitLab. Os executores GitLab são serviços ou agentes que executam e executam tarefas no servidor GitLab remoto.

Em um computador em uma rede diferente, crie um contêiner para um executor do GitLab:

$ docker volume create gitlab-runner-config

$ docker run -d --name gitlab-runner --restart always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v gitlab-runner-config:/etc/gitlab-runner \
  gitlab/gitlab-runner:latest

$ docker ps
CONTAINER ID   IMAGE                           COMMAND                  CREATED         STATUS         PORTS                                       NAMES
880e2ed289d3   gitlab/gitlab-runner:latest     "/usr/bin/dumb-init …"   3 seconds ago   Up 2 seconds                                               gitlab-runner-test

Agora registre-o com seu projeto no painel ConfiguraçõesCI/CD do seu projeto GitLab:

(AJ Canlas, CC BY-SA 4.0)

Role para baixo até ExecutoresRecolher:

(AJ Canlas, CC BY-SA 4.0)

O token de registro e o URL do executor do GitLab são obrigatórios. Desative o executor compartilhado no lado direito para garantir que ele funcione apenas no executor. Execute o contêiner gitlab-runner para registrar o executor:

$ docker exec -ti gitlab-runner /usr/bin/gitlab-runner register
Runtime platform                                    arch=amd64 os=linux pid=18 revision=6d480948 version=15.7.1
Running in system-mode.                            
                                                   
Enter the GitLab instance URL (for example, https://gitlab.com/):
https://gitlab.com/
Enter the registration token:
GR1348941S1bVeb1os44ycqsdupRK
Enter a description for the runner:
[880e2ed289d3]: dockerhost
Enter tags for the runner (comma-separated):
homelab
Enter optional maintenance note for the runner:

WARNING: Support for registration tokens and runner parameters in the 'register' command has been deprecated in GitLab Runner 15.6 and will be replaced with support for authentication tokens. For more information, see https://gitlab.com/gitlab-org/gitlab/-/issues/380872 
Registering runner... succeeded                     runner=GR1348941S1bVeb1o
Enter an executor: docker-ssh, shell, virtualbox, instance, kubernetes, custom, docker, parallels, ssh, docker+machine, docker-ssh+machine:
docker
Enter the default Docker image (for example, ruby:2.7):
ajscanlas/homelab-runner:3.17
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
 
Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml" 

Após o sucesso, a interface do GitLab exibe seu executor como válido. Se parece com isso:

(AJ Canlas, CC BY-SA 4.0)

Agora você pode usar esse executor para automatizar o provisionamento com um pipeline de CI/CD no GitLab.

Configure o pipeline do GitLab

Agora você pode configurar um pipeline. Adicione um arquivo chamado .gitlab-ci.yaml em seu repositório para definir suas etapas de CI/CD. Ignore os arquivos desnecessários, como diretórios .terraform e dados confidenciais, como arquivos variáveis.

Aqui está meu arquivo .gitignore:

$ cat .gitignore
*.tfvars
.terraform*

Aqui estão minhas entradas de pipeline de CI em .gitlab-ci.yaml:

$ cat .gitlab-ci.yaml
default:
  tags:
    - homelab

variables:
  TF_ROOT: ${CI_PROJECT_DIR}
  TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/homelab

cache:
  key: homelab
  paths:
    - ${TF_ROOT}/.terraform*

stages:
  - prepare
  - validate
  - build
  - deploy

before_script:
  - cd ${TF_ROOT}

tf-init:
  stage: prepare
  script:
    - terraform --version
    - terraform init -backend-config=address=${BE_REMOTE_STATE_ADDRESS} -backend-config=lock_address=${BE_REMOTE_STATE_ADDRESS}/lock -backend-config=unlock_address=${BE_REMOTE_STATE_ADDRESS}/lock -backend-config=username=${BE_USERNAME} -backend-config=password=${BE_ACCESS_TOKEN} -backend-config=lock_method=POST -backend-config=unlock_method=DELETE -backend-config=retry_wait_min=5

tf-validate:
  stage: validate
  dependencies:
    - tf-init
  variables:
    TF_VAR_OS_AUTH_URL: ${OS_AUTH_URL}
    TF_VAR_OS_PASSWORD: ${OS_PASSWORD}
    TF_VAR_OS_REGION: ${OS_REGION}
    TF_VAR_OS_TENANT: ${OS_TENANT}
    TF_VAR_OS_USERNAME: ${OS_USERNAME}
  script:
    - terraform validate

tf-build:
  stage: build
  dependencies:
    - tf-validate
  variables:
    TF_VAR_OS_AUTH_URL: ${OS_AUTH_URL}
    TF_VAR_OS_PASSWORD: ${OS_PASSWORD}
    TF_VAR_OS_REGION: ${OS_REGION}
    TF_VAR_OS_TENANT: ${OS_TENANT}
    TF_VAR_OS_USERNAME: ${OS_USERNAME}
  script:
    - terraform plan -out "planfile"
  artifacts:
    paths:
      - ${TF_ROOT}/planfile

tf-deploy:
  stage: deploy
  dependencies:
    - tf-build
  variables:
    TF_VAR_OS_AUTH_URL: ${OS_AUTH_URL}
    TF_VAR_OS_PASSWORD: ${OS_PASSWORD}
    TF_VAR_OS_REGION: ${OS_REGION}
    TF_VAR_OS_TENANT: ${OS_TENANT}
    TF_VAR_OS_USERNAME: ${OS_USERNAME}
  script:
    - terraform apply -auto-approve "planfile"

O processo começa declarando que cada etapa e estágio estão sob a tag homelab, permitindo que seu executor do GitLab o execute.

default:
  tags:
    - homelab

Em seguida, as variáveis são definidas no pipeline. As variáveis só estão presentes quando o pipeline está em execução:

variables:
  TF_ROOT: ${CI_PROJECT_DIR}
  TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/homelab

Há um cache que salva arquivos e diretórios específicos durante a execução de um estágio para outro:

cache:
  key: homelab
  paths:
    - ${TF_ROOT}/.terraform*

Estas são as etapas que o pipeline segue:

stages:
  - prepare
  - validate
  - build
  - deploy

Isto declara o que fazer antes de qualquer estágio ser executado:

before_script:
  - cd ${TF_ROOT}

No estágio prepare, o tf-init inicializa os scripts do Terraform, obtém o provedor e define seu back-end para o GitLab. Variáveis que ainda não foram declaradas serão adicionadas posteriormente como variáveis de ambiente.

tf-init:
  stage: prepare
  script:
    - terraform --version
    - terraform init -backend-config=address=${BE_REMOTE_STATE_ADDRESS} -backend-config=lock_address=${BE_REMOTE_STATE_ADDRESS}/lock -backend-config=unlock_address=${BE_REMOTE_STATE_ADDRESS}/lock -backend-config=username=${BE_USERNAME} -backend-config=password=${BE_ACCESS_TOKEN} -backend-config=lock_method=POST -backend-config=unlock_method=DELETE -backend-config=retry_wait_min=5

Nesta parte, o trabalho de CI tf-validate e o estágio validate executam o Terraform para validar se os scripts do Terraform estão livres de erros de sintaxe. Variáveis ainda não declaradas são adicionadas como variáveis de ambiente posteriormente.

tf-validate:
  stage: validate
  dependencies:
    - tf-init
  variables:
    TF_VAR_OS_AUTH_URL: ${OS_AUTH_URL}
    TF_VAR_OS_PASSWORD: ${OS_PASSWORD}
    TF_VAR_OS_REGION: ${OS_REGION}
    TF_VAR_OS_TENANT: ${OS_TENANT}
    TF_VAR_OS_USERNAME: ${OS_USERNAME}
  script:
    - terraform validate

Em seguida, a tarefa de CI tf-build com o estágio build cria o arquivo de plano usando terraform plan e o salva temporariamente usando os artifacts etiqueta .

tf-build:
  stage: build
  dependencies:
    - tf-validate
  variables:
    TF_VAR_OS_AUTH_URL: ${OS_AUTH_URL}
    TF_VAR_OS_PASSWORD: ${OS_PASSWORD}
    TF_VAR_OS_REGION: ${OS_REGION}
    TF_VAR_OS_TENANT: ${OS_TENANT}
    TF_VAR_OS_USERNAME: ${OS_USERNAME}
  script:
    - terraform plan -out "planfile"
  artifacts:
    paths:
      - ${TF_ROOT}/planfile

Na próxima seção, o trabalho de CI tf-deploy com o estágio deploy aplica o arquivo de plano.

tf-deploy:
  stage: deploy
  dependencies:
    - tf-build
  variables:
    TF_VAR_OS_AUTH_URL: ${OS_AUTH_URL}
    TF_VAR_OS_PASSWORD: ${OS_PASSWORD}
    TF_VAR_OS_REGION: ${OS_REGION}
    TF_VAR_OS_TENANT: ${OS_TENANT}
    TF_VAR_OS_USERNAME: ${OS_USERNAME}
  script:
    - terraform apply -auto-approve "planfile"

Existem variáveis, então você deve declará-las em ConfiguraçõesCI/CDVariáveisExpandir.

(AJ Canlas, CC BY-SA 4.0)

Adicione todas as variáveis necessárias:

BE_ACCESS_TOKEN => GitLab Access Token
BE_REMOTE_STATE_ADDRESS => This was the rendered TF_ADDRESS variable
BE_USERNAME => GitLab username
OS_USERNAME => OpenStack Username
OS_TENANT   => OpenStack tenant
OS_PASSWORD => OpenStack User Password
OS_AUTH_URL => Auth URL
OS_REGION   => OpenStack Region

Então, para este exemplo, usei o seguinte:

BE_ACCESS_TOKEN = "wwwwwwwwwwwwwwwwwwwww"
BE_REMOTE_STATE_ADDRESS = https://gitlab.com/api/v4/projects/42580143/terraform/state/homelab
BE_USERNAME = "ajohnsc"
OS_USERNAME = "admin"
OS_TENANT   = "admin"
OS_PASSWORD = "YYYYYYYYYYYYYYYYYYYYYY"
OS_AUTH_URL = "http://X.X.X.X:35357/v3"
OS_REGION   = "RegionOne"

E é mascarado pelo GitLab para sua proteção.

(AJ Canlas, CC BY-SA 4.0)

A última etapa é enviar os novos arquivos para o repositório:

$ git add .

$ git commit -m "First commit"
[main (root-commit) e78f701] First commit
 10 files changed, 194 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .gitlab-ci.yml
 create mode 100644 backend.tf
 create mode 100644 demo-project-user.tf
 create mode 100644 external-network.tf
 create mode 100644 flavors.tf
 create mode 100644 images.tf
 create mode 100644 provider.tf
 create mode 100644 routers.tf
 create mode 100644 variables.tf

$ git push
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 4 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (12/12), 2.34 KiB | 479.00 KiB/s, done.
Total 12 (delta 0), reused 0 (delta 0), pack-reused 0
To gitlab.com:testgroup2170/testproject.git
 * [new branch]      main -> main

Veja os resultados

Veja seus novos pipelines na seção CI/CD do GitLab.

(AJ Canlas, CC BY-SA 4.0)

Do lado do OpenStack, você pode ver os recursos criados pelo Terraform.

As redes:

(AJ Canlas, CC BY-SA 4.0)

Os sabores:

(AJ Canlas, CC BY-SA 4.0)

As imagens:

(AJ Canlas, CC BY-SA 4.0)

O projeto:

(AJ Canlas, CC BY-SA 4.0)

O usuário:

(AJ Canlas, CC BY-SA 4.0)

Próximos passos

Terraform tem muito potencial. Terraform e Ansible são ótimos juntos. No meu próximo artigo, demonstrarei como o Ansible pode trabalhar com OpenStack

Artigos relacionados: