В предыдущей статье мы рассмотрели основы языка HCL, используемого Terraform для описания требуемых конфигураций. Также мы подготовили небольшое описание для создания экземпляра EC2 в AWS. Однако, в представленном описании у на присутствуют только основные параметры, необходимые для создания узла, но отсутствуют, к примеру параметры для настройки сети.

Для полноценной автоматизации нам было бы неплохо прописать все необходимые для работы сетевые интерфейсы. Для этого объявим две переменные net_primary и net_ad для двух сетевых интерфейсов. Если вам не требуется второй интерфейс, net_ad можно не указывать, однако в большинстве случаев для серверов требуется скорее большее количество сетевых портов.

variable "net_primary" {
  type    = string
  default = "subnet-3155417b"
}
variable "net_ad" {
  type    = string
  default = "subnet-3155417a"
}

В строковых значениях этих переменных мы указали, к каким сетям будут подключаться сетевые интерфейсы, которым будут присвоены net_primary и net_add. В данном примере указаны дефолтные подсети облака Amazon.

Далее добавим в наш конфигурационный файл ресурсы, описывающие сетевые интерфейсы:

resource "aws_network_interface" "primary" {
  subnet_id         = var.net_primary
  source_dest_check = false
}
resource "aws_network_interface" "ad" {
  subnet_id         = var.net_ad
  source_dest_check = false
}  

Указанный в примере параметр source-dest-check отвечает за проверку каждого пакета, проходящего через интерфейс. Когда source-dest-check включен, каждый IP-пакет, проходящий через этот интерфейс, должен быть отправлен, либо предназначаться IP-адресу этого интерфейса.

Но в случае, если вам необходимо настроить маршрутизацию либо NAT на экземпляре, нужно отключить source-dest-check для интерфейса данного экземпляра. Также, в случае, если нам необходимо автоматически назначить IP адрес с помощью DHCP, нужно добавить следующую строку: 

associate_public_ip_address = true 

Давайте добавим эти параметры в наш конфигурационный файл:

terraform {
 required_providers {
   aws = {
     source  = "hashicorp/aws"
     version = "~> 3.0"
   }
 }
}

provider "aws" {
 region = var.region
}
variable "region" {
 default = "us-west-1"
 description = "AWS Region"
}

variable "ami" {
 default = "ami-00831fc7c1e3ddc60"
 description = "Amazon Machine Image ID for Ubuntu Server 20.04"
}

variable "type" {
 default = "t2.micro"
 description = "Size of VM"
}

variable "net_primary" {
  type    = string
  default = "subnet-3155417b"
}

variable "net_ad" {
  type    = string
  default = "subnet-3155417a"
}

resource "aws_instance" "demo" {
 ami = var.ami
 instance_type = var.type
resource "aws_network_interface" "primary" {
  subnet_id         = var.net_primary
  source_dest_check = false
}

resource "aws_network_interface" "ad" {
  subnet_id         = var.net_ad
  source_dest_check = false
}  

 tags = {
   name = "Demo System"
 }
} 

output "instance_id" {
 instance = aws_instance.demo.id
}

Таким образом, мы дополнили настройки нашего экземпляра сетевыми параметрами. Теперь давайте рассмотрим различные функции Terraform, позволяющие обрабатывать числовые и строковые значения. Собственно, наименования этих функций аналогичны используемым в других языках программирования, поэтому тем, кто знаком с любым языком высокого уровня разобраться в этих функциях будет несложно.

Функции Terraform

Основными числовыми функциями, которые могут потребоваться в работе являются min и max, вычисляющие минимальное и максимальное значения из предложенного списка.

max(12, 54, 3)

54

При этом, если список значений вложен в другой список, функция также найдет нужное значение

min([12, 54, 3]...)

3

Строковые функции более востребованы, так как с их помощью можно генерировать различные наименования, тэги и другие строковые значения, необходимые в дальнейшей работе.

Если мы получаем строковые значения из файла , там могут присутствовать символы окончания строки (\n, \r). Для того, чтобы их корректно удалить, можно воспользоваться функцией chomp

chomp("hello\n")

hello

chomp("hello\r\n")

hello

chomp("hello\n\n")

hello 

Еще одна функция, предназначенная для удаления лишних символов по краям строк, это trim. Здесь мы сами передаем функции те символы, которые необходимо удалить. Вот несколько примеров

trim("?!hello?!", "!?")

"hello"

trim("foobar", "far")

"oob"

trim("   hello! world.!  ", "! ")

"hello! world."

 У этой функции есть несколько вариаций, так trimprefix и trimsuffix удаляют символы с начала и с конца строки соответственно, а trimspace удаляет все пробелы в терминах Юникода, то есть помимо самих пробелов также табуляцию, перенос строк и другие значения.

Для поиска строк в конфигурациях мы можем воспользоваться регулярными выражениями с помощью функции regex. На просторах Хабра можно найти массу статей, посвященных работе с регулярными выражениями, поэтому здесь мы не будем на этом останавливаться.

Еще несколько практически полезных функций позволяют работать со списками, состоящими из наборов различных значений. Начнем с функции element, которая извлекает отдельный элемент из списка:

element(["a", "b", "c"], 1)

b 

В этом примере мы получили первый элемент из списка. Напомним, что нумерация в списках Terraform начинается с 0. Но, если мы попросим у этой функции извлечь элемент с индексом, который больше длины списка, функция выполнит “оборот”, то есть присвоит индексу в качестве результата остаток от целочисленного деления (mod).   

element(["a", "b", "c"], 3)

a

Так что будьте готовы к такому “странному” но абсолютно корректному поведению данной функции.

Еще одна интересная функция Terraform это merge. Эта функция получает на вход произвольное количество объектов в формате ключ-значение, у которых ключи могут быть не уникальны. Например:

{a="b", c="d"}, {e="f", c="z"}

Здесь c имеет значения d и z. Функция merge присвоит с значение z, потому-что приоритет имеет тот ключ, который находится позже в последовательности аргументов. Если типы аргументов не совпадают, результирующий тип будет объектом, соответствующим структуре типов атрибутов после применения правил объединения.

Чтобы стало понятнее посмотрим примеры:

merge({a="b", c="d"}, {e="f", c="z"})

{

  "a" = "b"

  "c" = "z"

  "e" = "f"

}

Здесь ключу с соответствует значение z, первое присвоение a=”b” в итоге потеряно. Другой пример:

merge({a="b"}, {a=[1,2], c="z"}, {d=3})
{
  "a" = [
    1,
    2,
  ]
  "c" = "z"
  "d" = 3
}

Здесь мы сначала объединили объекты, в результате чего a был присвоен массив [1,2]. 

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

Рассмотрим пример практического применения данных функций. Ниже приведен блок кода HCL, в котором мы берем номер нашего экземпляра (у нас же развернуто несколько экземпляров EC2, иначе какой смысл во всей автоматизации и IaC ???? ) из значения переменной var.instance.count и используем его в качестве индекса для дальнейшей работы со списками.

 resource "aws_eip" "my_static_ip" {
  count = var.instance_count
  network_interface  = element(aws_network_interface.elastic.*.id,count.index)
  tags  = merge(var.common_tags, { Name = "${var.description}-${count.index+1}" })
}

Если нам требуется извлечь из имеющихся в AWS сетевых интерфейсов нужный номер и назначить его переменной network_interfaces,  это можно сделать с помощью функции element. В приведенном ниже примере номером элемента списка является значение count.index, которое мы назначили ранее, а aws_network_interface это список, в котором мы ищем нужное значение.

network_interface  = element(aws_network_interface.elastic.*.id,count.index)

Ну а если затем нам потребовалось назначить тегам имена интерфейсов, мы можем воспользоваться функцией merge.

tags  = merge(var.common_tags, { Name = "${var.description}-${count.index+1}" })

Как видно из этого примера язык Terraform позволяет нам обрабатывать значения переменных внутри списка ("${var.description}-${count.index+1}").   

Далее давайте добавим параметры о которых мы сейчас говорили в наш конфигурационный файл:

terraform {
 required_providers {
   aws = {
     source  = "hashicorp/aws"
     version = "~> 3.0"
   }
 }
} 

provider "aws" {
 region = var.region
}

variable "region" {
 default = "us-west-1"
 description = "AWS Region"
}

variable "ami" {
 default = "ami-00831fc7c1e3ddc60"
 description = "Amazon Machine Image ID for Ubuntu Server 20.04"
}
 
variable "type" {
 default = "t2.micro"
 description = "Size of VM"
}

variable "net_primary" {
  type    = string
  default = "subnet-3155417b"
}

variable "net_ad" {
  type    = string
  default = "subnet-3155417a"
}

variable "instance_count" {
  default = "1"
}
 
resource "aws_eip" "my_static_ip" {
  count = var.instance_count
  network_interface  = element(aws_network_interface.elastic.*.id,count.index)
  tags  = merge(var.common_tags, { Name = "${var.description}-${count.index+1}" })
}

resource "aws_instance" "demo" {
 ami = var.ami
 instance_type = var.type

resource "aws_network_interface" "primary" {
  subnet_id         = var.net_primary
  source_dest_check = false
}

resource "aws_network_interface" "ad" {
  subnet_id         = var.net_ad
  source_dest_check = false
}   

 tags = {
   name = "Demo System"
 }
}

Жизненный цикл

Ресурсы Terraform имеют свой жизненный цикл, который можно описать посредством языка HCL. Когда Terraform создает новый объект инфраструктуры, представленный блоком resource, идентификатор для этого реального объекта сохраняется в системе, что позволяет обновлять и уничтожать его в случае изменений. Для блоков ресурсов, у которых уже есть связанный объект, Terraform сравнивает фактическую конфигурацию объекта с аргументами, указанными в конфигурации, и, при необходимости, обновляет объект в соответствии с конфигурацией. Таким образом, применение конфигурации Terraform приведет к одному из следующих состояний:

-  Create - ресурсы, которые существуют в конфигурации, но не связаны с реальным объектом инфраструктуры в состоянии.

-  Destroy - ресурсы, которые существуют в состоянии, но больше не существуют в конфигурации.

-  Update - ресурсы на месте, аргументы которых изменились.

-  Destroy and re-create, аргументы которых изменились, но которые не могут быть обновлены на месте из-за ограничений удаленного API.

Некоторые детали работы с этими состояниями можно настроить с помощью специального вложенного блока жизненного цикла lifecycle в теле блока ресурсов:

resource "azurerm_resource_group" "example" {
  # ...

  lifecycle {
               …

  }
}

Внутри блока lifecycle имеются следующие аргументы create_before_destroy, prevent_destroy, ignore_changes, и replace_triggered_by.

Начнем с create_before_destroy. Этот аргумент может иметь значения истина или ложь. В случае, когда у ресурса изменились аргументы, но его нельзя обновить из-за ограничений удаленного API, Terraform удаляет существующий объект и создает новый с уже измененными аргументами. Мета аргумент create_before_destroy позволяет сначала создать объект с новыми параметрами, а уже потом удалить старый. Такое поведение не всегда можно использовать, так как некоторые объекты могут иметь уникальные имена и нам необходимо учитывать существование как нового, так и старого объекта при работе с данным аргументом.

 Еще один аргумент булевского типа это prevent_destroy. Значение true для данного аргумента приведет к тому, что Terraform отклонит с ошибкой применение любой конфигурации, которая уничтожит объект инфраструктуры, связанный с ресурсом, до тех пор, пока аргумент остается присутствующим в конфигурации. Такой механизм является своего рода защитой от ошибочного уничтожения объектов, воспроизведение которых является дорогостоящим мероприятием, в плане использования ресурсов облака. Примерами таких объектов могут быть различные тяжеловесные базы данных и вычислительные приложения.

Функция ignore_changes предназначена для использования в случае, если у нас создается ресурс со ссылками на данные, которые могут измениться в будущем, но не должны влиять на указанный ресурс после его создания. В настройках по умолчанию Terraform обнаруживает любые различия в текущих параметрах реального объекта инфраструктуры и планирует обновить удаленный объект в соответствии с новой конфигурацией. Аргументы (относительный адрес атрибутов в ресурсе), соответствующие указанным именам атрибутов, учитываются при планировании операции создания, но игнорируются при планировании обновления.

Для того, чтобы предыдущее описание было понятнее, рассмотрим следующие фрагменты кода:

ebs_block_device {
          delete_on_termination = true
          encrypted             = false
          device_name           = "disk1"
          volume_type           = "gp2"
        }

lifecycle {
     ignore_changes = [
     user_data,ebs_block_device
     ]
   }

В первом фрагменте мы определили параметры для устройства ebs_block_device: удаление диска при удалении экземпляра, отключение шифрования, имя диска и тип раздела.

А во втором фрагменте мы указали, что нам необходимо игнорировать изменения, в частности, изменения пользовательских данных и значений переменной ebs_block_device.   

В завершении приведем полный код нашей получившейся конфигурации:

terraform {
 required_providers {
   aws = {
     source  = "hashicorp/aws"
     version = "~> 3.0"
   }
 }
}
 
provider "aws" {
 region = var.region
} 

variable "region" {
 default = "us-west-1"
 description = "AWS Region"
}

variable "ami" {
 default = "ami-00831fc7c1e3ddc60"
 description = "Amazon Machine Image ID for Ubuntu Server 20.04"
} 

variable "type" {
 default = "t2.micro"
 description = "Size of VM"
}

variable "net_primary" {
  type    = string
  default = "subnet-3155417b"
}

variable "net_ad" {
  type    = string
  default = "subnet-3155417a"
}

variable "instance_count" {
  default = "1"
} 

resource "aws_eip" "my_static_ip" {
  count = var.instance_count
  network_interface  = element(aws_network_interface.elastic.*.id,count.index)
  tags  = merge(var.common_tags, { Name = "${var.description}-${count.index+1}" })
}

resource "aws_instance" "demo" {
 ami = var.ami
 instance_type = var.type 

resource "aws_network_interface" "primary" {
  subnet_id         = var.net_primary
  source_dest_check = false
}
resource "aws_network_interface" "ad" {
  subnet_id         = var.net_ad
  source_dest_check = false
}  

 tags = {
   name = "Demo System"
 }
     ebs_block_device {
          delete_on_termination = true
          encrypted             = false
          device_name           = "disk1"
          volume_type           = "gp2"
        }
lifecycle {
     ignore_changes = [
     user_data,ebs_block_device
     ]
   }
} 

Заключение

Несмотря на то, что статья получилась достаточно объемная, в ней отражена только малая часть тех возможностей, которые предоставляет функционал Terraform для реализации Infrastructure as a Code. Однако, общее представление об этих возможностях, полагаю из статьи можно получить. Ну а тем, кого заинтересовала данная тема я рекомендую ознакомиться с полным описанием языка HCL на сайте Terraform.   

Также приглашаем всех на бесплатный урок, где мы рассмотрим из каких основных и вспомогательных компонентов состоит Kubernetes-кластер и то, как эти компоненты взаимодействуют между собой

Комментарии (0)