Если вы применяете средства инициализации ресурсов наподобие Terraform для развертывания системных образов Docker или Packer, большинство изменений будет заключаться в создании совершенно новых серверов. Например, чтобы развернуть новую версию OpenSSL, вы включаете ее в новый образ Packer, который развертывается на группе новых узлов, а затем удаляете старые узлы. Поскольку при каждом развертывании используются неизменяемые образы и свежие серверы, этот подход уменьшает вероятность дрейфа конфигурации, упрощает отслеживание того, какое ПО установлено на каждом сервере, и позволяет легко развернуть любую предыдущую версию ПО (любой предыдущий образ) в любой момент. Это также повышает эффективность вашего автоматического тестирования, поскольку неизменяемый образ, прошедший проверку в тестовой среде, скорее всего, будет вести себя аналогично и в промышленных условиях.
Конечно, с помощью средств управления конфигурацией можно выполнять и изменяемые развертывания, но для них такой подход не характерен; в то же время для средств инициализации ресурсов он является вполне естественным. Стоит также отметить, что у неизменяемого подхода есть и недостатки. Например, даже в случае тривиального изменения придется заново собрать образ из шаблона сервера и снова развернуть его на всех ваших узлах, что займет много времени. Более того, неизменяемость сохраняется только до запуска образа. Как только сервер загрузится, он начнет производить запись на жесткий диск и испытывать дрейф конфигурации в той или иной степени (хотя это можно минимизировать, если делать частые развертывания).
Выбор между процедурными и декларативными языками
Chef и Ansible поощряют процедурный стиль — когда код пошагово описывает, как достичь желаемого конечного состояния. А вот Terraform, CloudFormation, SaltStack, Puppet и Open Stack Heat исповедуют более декларативный подход: вы описываете в своем коде нужное вам конечное состояние, а средства IaC сами разбираются с тем, как его достичь.
Чтобы продемонстрировать это различие, рассмотрим пример. Представьте, что вам нужно развернуть десять серверов (экземпляров, или инстансов,EC2 в терминологии AWS) для выполнения AMI с идентификатором ami-0c55b159cbfafe1f0 (Ubuntu 18.04). Так выглядит шаблон Ansible, который делает это в процедурном стиле:
- ec2:
count: 10
image: ami-0c55b159cbfafe1f0
instance_type: t2.micro
А вот упрощенный пример конфигурации Terraform, который делает то же самое, используя декларативный подход:
resource "aws_instance" "example" {
count = 10
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
На первый взгляд подходы похожи, и если выполнить их с помощью Ansible или Terraform, получатся схожие результаты. Но самое интересное начинается тогда, когда нужно что-то поменять.
Представьте, что у вас повысилась нагрузка и вы хотите увеличить количество серверов до 15. В случае с Ansible написанный ранее процедурный код становится бесполезным; если вы просто запустите его снова, поменяв значение на 15, у вас будет развернуто 15 новых серверов, что в сумме даст 25! Таким образом, чтобы добавить пять новых серверов, вам нужно написать совершенно новый процедурный скрипт с учетом того, что у вас уже развернуто:
- ec2:
count: 5
image: ami-0c55b159cbfafe1f0
instance_type: t2.micro
В случае с декларативным кодом нужно лишь описать желаемое конечное состояние, а Terraform разберется с тем, как этого достичь, учитывая любые изменения, сделанные в прошлом. Таким образом, чтобы развернуть еще пять серверов, вам достаточно вернуться к той же конфигурации Terraform и поменять поле count с 10 на 15:
resource "aws_instance" "example" {
count = 15
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
Если вы примените эту конфигурацию, Terraform поймет, что у вас уже есть десять серверов и нужно создать еще пять. Еще до применения конфигурации можно воспользоваться командой Terraform plan, чтобы увидеть, какие изменения будут внесены:
$ terraform plan
# aws_instance.example[11] will be created
+ resource "aws_instance" "example" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t2.micro"
+ (...)
}
# aws_instance.example[12] will be created
+ resource "aws_instance" "example" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t2.micro"
+ (...)
}
# aws_instance.example[13] will be created
+ resource "aws_instance" "example" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t2.micro"