Terraform

Данное практическое занятие посвящено основам управления инфраструктурой с использованием terraform. В качестве инфраструктурного провайдера будет использоваться docker.

Vagrant

Для работы с terraform воспользуемся следующим Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.define "node" do |c|
    c.vm.box = "ubuntu/lunar64"
    c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq docker.io
usermod -a -G docker vagrant
curl -L https://hashicorp-releases.yandexcloud.net/terraform/1.7.3/terraform_1.7.3_linux_amd64.zip \
  | zcat > /usr/local/bin/terraform
chmod +x /usr/local/bin/terraform
cat > /home/vagrant/.terraformrc <<EOF
provider_installation {
    network_mirror {
        url = "https://terraform-mirror.yandexcloud.net/"
        include = ["registry.terraform.io/*/*"]
}
    direct {
        exclude = ["registry.terraform.io/*/*"]
    }
}
EOF
    SHELL
  end
end

Init

Состояние инфраструктуры, управляемое с помощью terraform, описывается с помощью языка конфигурации terraform. Опишем состояние ресурсов, которые мы хотим получить в файле main.tf:

terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 3.0.2"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

В данной конфигурации мы указываем провайдера ресурсов - docker, образ и описание контейнера, которые мы хотим получить. Подробности о конфигурации ресурсов можно получить в документации провайдера. С помощью команды terraform validate можно проверить описанную конфигурацию:

$ terraform validate
Success! The configuration is valid.

После описания конфигурации запустим команду terraform init для инициализации рабочей директории:

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0.2"...
- Installing kreuzwerker/docker v3.0.2...
- Installed kreuzwerker/docker v3.0.2 (unauthenticated)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

Как видно, данная команда также установила необходимый плагин провайдера и внесла информацию о нем в файл .terraform.lock.hcl.

Plan

Для просмотра вносимых изменений в инфраструктуру, описанных в конфигурации, до непосредственного изменения существует команда terraform plan:

$ terraform plan

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:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach                                      = false
      + bridge                                      = (known after apply)
      + command                                     = (known after apply)
      + container_logs                              = (known after apply)
      + container_read_refresh_timeout_milliseconds = 15000
      + entrypoint                                  = (known after apply)
      + env                                         = (known after apply)
      + exit_code                                   = (known after apply)
      + hostname                                    = (known after apply)
      + id                                          = (known after apply)
      + image                                       = (known after apply)
      + init                                        = (known after apply)
      + ipc_mode                                    = (known after apply)
      + log_driver                                  = (known after apply)
      + logs                                        = false
      + must_run                                    = true
      + name                                        = "tutorial"
      + network_data                                = (known after apply)
      + read_only                                   = false
      + remove_volumes                              = true
      + restart                                     = "no"
      + rm                                          = false
      + runtime                                     = (known after apply)
      + security_opts                               = (known after apply)
      + shm_size                                    = (known after apply)
      + start                                       = true
      + stdin_open                                  = false
      + stop_signal                                 = (known after apply)
      + stop_timeout                                = (known after apply)
      + tty                                         = false
      + wait                                        = false
      + wait_timeout                                = 60

      + ports {
          + external = 8000
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id          = (known after apply)
      + image_id    = (known after apply)
      + name        = "nginx:latest"
      + repo_digest = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Данная команда выводит все изменения, которые произойдут с ресурсами при применении данной конфигурации.

Apply

Применить текущую конфигурацию можно командой terraform apply:

$ terraform apply --auto-approve

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:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach                                      = false
      + bridge                                      = (known after apply)
      + command                                     = (known after apply)
      + container_logs                              = (known after apply)
      + container_read_refresh_timeout_milliseconds = 15000
      + entrypoint                                  = (known after apply)
      + env                                         = (known after apply)
      + exit_code                                   = (known after apply)
      + hostname                                    = (known after apply)
      + id                                          = (known after apply)
      + image                                       = (known after apply)
      + init                                        = (known after apply)
      + ipc_mode                                    = (known after apply)
      + log_driver                                  = (known after apply)
      + logs                                        = false
      + must_run                                    = true
      + name                                        = "tutorial"
      + network_data                                = (known after apply)
      + read_only                                   = false
      + remove_volumes                              = true
      + restart                                     = "no"
      + rm                                          = false
      + runtime                                     = (known after apply)
      + security_opts                               = (known after apply)
      + shm_size                                    = (known after apply)
      + start                                       = true
      + stdin_open                                  = false
      + stop_signal                                 = (known after apply)
      + stop_timeout                                = (known after apply)
      + tty                                         = false
      + wait                                        = false
      + wait_timeout                                = 60

      + ports {
          + external = 8000
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id          = (known after apply)
      + image_id    = (known after apply)
      + name        = "nginx:latest"
      + repo_digest = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.
docker_image.nginx: Creating...
docker_image.nginx: Still creating... [10s elapsed]
docker_image.nginx: Creation complete after 10s [id=sha256:247f7abff9f7097bbdab57df76fedd124d1e24a6ec4944fb5ef0ad128997ce05nginx:latest]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 0s [id=6c830b003efd1939725e78490013225267a625a429780d1c1b918e6ac3cf7650]

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

После выполнения данной команды мы получим инфраструктурные ресурсы в том состоянии, в котором они описаны в файле main.tf, а также файл состояния terraform.tfstate, в котором terraform будет хранить текущее состояние инфраструктуры:

$ ls
main.tf  terraform.tfstate
$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
nginx        latest    247f7abff9f7   3 months ago   187MB
$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                  NAMES
6c830b003efd   247f7abff9f7   "/docker-entrypoint.…"   5 minutes ago   Up 5 minutes   0.0.0.0:8000->80/tcp   tutorial
$ curl -I localhost:8000
HTTP/1.1 200 OK

Change

Внесем изменения в конфигурационный файл main.tf, например, изменив тег образа и порт контейнера:

terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 3.0.2"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:alpine"
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "tutorial"
  ports {
    internal = 80
    external = 8001
  }
}

И попробуем применить данную конфигурацию. Команда terraform apply без аргументов покажет вносимые изменения и запросит подтверждение перед применением:

$ terraform apply
docker_image.nginx: Refreshing state... [id=sha256:247f7abff9f7097bbdab57df76fedd124d1e24a6ec4944fb5ef0ad128997ce05nginx:latest]
docker_container.nginx: Refreshing state... [id=6c830b003efd1939725e78490013225267a625a429780d1c1b918e6ac3cf7650]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # docker_container.nginx must be replaced
-/+ resource "docker_container" "nginx" {
      + bridge                                      = (known after apply)
      ~ command                                     = [
          - "nginx",
          - "-g",
          - "daemon off;",
        ] -> (known after apply)
      + container_logs                              = (known after apply)
      - cpu_shares                                  = 0 -> null
      - dns                                         = [] -> null
      - dns_opts                                    = [] -> null
      - dns_search                                  = [] -> null
      ~ entrypoint                                  = [
          - "/docker-entrypoint.sh",
        ] -> (known after apply)
      ~ env                                         = [] -> (known after apply)
      + exit_code                                   = (known after apply)
      - group_add                                   = [] -> null
      ~ hostname                                    = "6c830b003efd" -> (known after apply)
      ~ id                                          = "6c830b003efd1939725e78490013225267a625a429780d1c1b918e6ac3cf7650" -> (known after apply)
      ~ image                                       = "sha256:247f7abff9f7097bbdab57df76fedd124d1e24a6ec4944fb5ef0ad128997ce05" # forces replacement -> (known after apply) # forces replacement
      ~ init                                        = false -> (known after apply)
      ~ ipc_mode                                    = "private" -> (known after apply)
      ~ log_driver                                  = "json-file" -> (known after apply)
      - log_opts                                    = {} -> null
      - max_retry_count                             = 0 -> null
      - memory                                      = 0 -> null
      - memory_swap                                 = 0 -> null
        name                                        = "tutorial"
      ~ network_data                                = [
          - {
              - gateway                   = "172.17.0.1"
              - global_ipv6_address       = ""
              - global_ipv6_prefix_length = 0
              - ip_address                = "172.17.0.2"
              - ip_prefix_length          = 16
              - ipv6_gateway              = ""
              - mac_address               = "02:42:ac:11:00:02"
              - network_name              = "bridge"
            },
        ] -> (known after apply)
      - network_mode                                = "default" -> null
      - privileged                                  = false -> null
      - publish_all_ports                           = false -> null
      ~ runtime                                     = "runc" -> (known after apply)
      ~ security_opts                               = [] -> (known after apply)
      ~ shm_size                                    = 64 -> (known after apply)
      ~ stop_signal                                 = "SIGQUIT" -> (known after apply)
      ~ stop_timeout                                = 0 -> (known after apply)
      - storage_opts                                = {} -> null
      - sysctls                                     = {} -> null
      - tmpfs                                       = {} -> null
        # (13 unchanged attributes hidden)

      ~ ports {
          ~ external = 8000 -> 8001 # forces replacement
            # (3 unchanged attributes hidden)
        }
    }

  # docker_image.nginx must be replaced
-/+ resource "docker_image" "nginx" {
      ~ id          = "sha256:247f7abff9f7097bbdab57df76fedd124d1e24a6ec4944fb5ef0ad128997ce05nginx:latest" -> (known after apply)
      ~ image_id    = "sha256:247f7abff9f7097bbdab57df76fedd124d1e24a6ec4944fb5ef0ad128997ce05" -> (known after apply)
      ~ name        = "nginx:latest" -> "nginx:alpine" # forces replacement
      ~ repo_digest = "nginx@sha256:ea97e6aace270d82c73da382ea1a8c42d44b9dc11b55159104e21c49c687e7fb" -> (known after apply)
    }

Plan: 2 to add, 0 to change, 2 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

docker_container.nginx: Destroying... [id=6c830b003efd1939725e78490013225267a625a429780d1c1b918e6ac3cf7650]
docker_container.nginx: Destruction complete after 0s
docker_image.nginx: Destroying... [id=sha256:247f7abff9f7097bbdab57df76fedd124d1e24a6ec4944fb5ef0ad128997ce05nginx:latest]
docker_image.nginx: Destruction complete after 1s
docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 5s [id=sha256:2b70e4aaac6b5370bf3a556f5e13156692351696dd5d7c5530d117aa21772748nginx:alpine]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 0s [id=c9346b76c28117d69383fbaf27523b1185def2dde87b823fe299333aee38469c]

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

Как видно, оба ресурса были пересозданы:

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
nginx        alpine    2b70e4aaac6b   3 months ago   42.6MB
$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED              STATUS              PORTS                  NAMES
c9346b76c281   2b70e4aaac6b   "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:8001->80/tcp   tutorial
$ curl -I localhost:8001
HTTP/1.1 200 OK

Destroy

Для удаления всех инфраструктурных ресурсов есть команда terraform destroy:

$ terraform destroy
docker_image.nginx: Refreshing state... [id=sha256:2b70e4aaac6b5370bf3a556f5e13156692351696dd5d7c5530d117aa21772748nginx:alpine]
docker_container.nginx: Refreshing state... [id=c9346b76c28117d69383fbaf27523b1185def2dde87b823fe299333aee38469c]

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

Terraform will perform the following actions:

  # docker_container.nginx will be destroyed
  - resource "docker_container" "nginx" {
      - attach                                      = false -> null
      - command                                     = [
          - "nginx",
          - "-g",
          - "daemon off;",
        ] -> null
      - container_read_refresh_timeout_milliseconds = 15000 -> null
      - cpu_shares                                  = 0 -> null
      - dns                                         = [] -> null
      - dns_opts                                    = [] -> null
      - dns_search                                  = [] -> null
      - entrypoint                                  = [
          - "/docker-entrypoint.sh",
        ] -> null
      - env                                         = [] -> null
      - group_add                                   = [] -> null
      - hostname                                    = "c9346b76c281" -> null
      - id                                          = "c9346b76c28117d69383fbaf27523b1185def2dde87b823fe299333aee38469c" -> null
      - image                                       = "sha256:2b70e4aaac6b5370bf3a556f5e13156692351696dd5d7c5530d117aa21772748" -> null
      - init                                        = false -> null
      - ipc_mode                                    = "private" -> null
      - log_driver                                  = "json-file" -> null
      - log_opts                                    = {} -> null
      - logs                                        = false -> null
      - max_retry_count                             = 0 -> null
      - memory                                      = 0 -> null
      - memory_swap                                 = 0 -> null
      - must_run                                    = true -> null
      - name                                        = "tutorial" -> null
      - network_data                                = [
          - {
              - gateway                   = "172.17.0.1"
              - global_ipv6_address       = ""
              - global_ipv6_prefix_length = 0
              - ip_address                = "172.17.0.2"
              - ip_prefix_length          = 16
              - ipv6_gateway              = ""
              - mac_address               = "02:42:ac:11:00:02"
              - network_name              = "bridge"
            },
        ] -> null
      - network_mode                                = "default" -> null
      - privileged                                  = false -> null
      - publish_all_ports                           = false -> null
      - read_only                                   = false -> null
      - remove_volumes                              = true -> null
      - restart                                     = "no" -> null
      - rm                                          = false -> null
      - runtime                                     = "runc" -> null
      - security_opts                               = [] -> null
      - shm_size                                    = 64 -> null
      - start                                       = true -> null
      - stdin_open                                  = false -> null
      - stop_signal                                 = "SIGQUIT" -> null
      - stop_timeout                                = 0 -> null
      - storage_opts                                = {} -> null
      - sysctls                                     = {} -> null
      - tmpfs                                       = {} -> null
      - tty                                         = false -> null
      - wait                                        = false -> null
      - wait_timeout                                = 60 -> null

      - ports {
          - external = 8001 -> null
          - internal = 80 -> null
          - ip       = "0.0.0.0" -> null
          - protocol = "tcp" -> null
        }
    }

  # docker_image.nginx will be destroyed
  - resource "docker_image" "nginx" {
      - id          = "sha256:2b70e4aaac6b5370bf3a556f5e13156692351696dd5d7c5530d117aa21772748nginx:alpine" -> null
      - image_id    = "sha256:2b70e4aaac6b5370bf3a556f5e13156692351696dd5d7c5530d117aa21772748" -> null
      - name        = "nginx:alpine" -> null
      - repo_digest = "nginx@sha256:f2802c2a9d09c7aa3ace27445dfc5656ff24355da28e7b958074a0111e3fc076" -> null
    }

Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

docker_container.nginx: Destroying... [id=c9346b76c28117d69383fbaf27523b1185def2dde87b823fe299333aee38469c]
docker_container.nginx: Destruction complete after 1s
docker_image.nginx: Destroying... [id=sha256:2b70e4aaac6b5370bf3a556f5e13156692351696dd5d7c5530d117aa21772748nginx:alpine]
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.

После выполнения - все ресурсы, управляемые terraform будут удалены:

$ docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES