В мире DevOps, где автоматизация играет ключевую роль, управление ресурсами и процессами обновления инфраструктуры в облаке является критически важной задачей. Во многих современных проектах, особенно тех, что развертываются в облачной среде AWS, используется механизм Auto Scaling Groups (ASG) с целью достижения трех основных задач: балансировки нагрузки, повышения надежности сервиса и оптимизации стоимости эксплуатации.

Представьте себе: вы работаете в компании, развертывающей свои приложения на ресурсах Amazon. И чтобы ускорить процесс развертывания и упростить управление конфигурацией, вы используете предварительно подготовленные AMI образы. Эти образы создаются с помощью инструментов типа HashiCorp Packer (или других аналогичных) и содержат все необходимое для того, чтобы ваше приложение стартовало быстро и без сбоев. Для разворачивания самой инфраструктуры вы используете Terraform, который стал стандартом de facto во многих крупных компаниях, управляющих облачными ресурсами и использующими подход IaC (Infrastructure as Code).

Но иногда вы сталкиваетесь с необходимостью обновить версии инстансов с новой версией AMI, будь то из-за установки последних обновлений безопасности или добавления новой функциональности. И вот тут начинаются сложности. Как обновить уже работающий ASG без простоя? Как гарантировать, что новый AMI будет работать так же хорошо, как и старый?

К счастью, ASG предоставляет решение в виде instance refresh, мощного инструмента, который позволяет обновить инстансы внутри группы, минимизируя простои и обеспечивая высокую доступность. Все бы ничего, но как удостовериться, что обновление прошло успешно, особенно если речь идет о больших и сложных системах?

К сожалению, ресурсы Terraform (например тот же aws_autoscaling_group) не позволяют отслеживать прогресс и успешность выполнения операции обновления ASG в рамках instance refresh, а могут лишь запустить его. Если какие-то другие части инфраструктуры (например, обновления сертификатов или dns-записей) каким-то образом зависят от состояния и версии запущенных инстансов, то желательно проконтролировать завершение процесса обновления для получения корректного состояния инфраструктуры после завершения работы terraform.

Чтобы решить данную проблему, вводим в игру Ansible. Этот инструмент, который уже давно зарекомендовал себя в управлении конфигурацией и автоматизации, может помочь и здесь. Именно Ansible позволит нам контролировать процесс обновления и удостовериться в его успешном завершении. Таким образом, объединив Terraform и Ansible, можно создать мощное и гибкое решение для управления и обновления ASG в AWS.

1. Подготовка Terraform

Первым шагом будет создание конфигурации Terraform, которая обеспечивает необходимую структуру и процесс для обновления ASG.

resource "aws_autoscaling_group" "example" {
  desired_capacity     = 3
  max_size             = 5
  min_size             = 2
  vpc_zone_identifier  = ["subnet-0bb1c79de3EXAMPLE"]

  launch_template {
    id      = aws_launch_template.example.id
    version = aws_launch_template.example.latest_version
  }
  
  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 100
      instance_warmup        = 120
    }
    triggers = ["tag"]
  }

  health_check_type          = "EC2"
  force_delete               = true
  wait_for_capacity_timeout  = "0"
}

Детальный разбор блока instance_refresh:

Этот блок важен, так как он задает параметры для обновления инстансов в ASG:

  • strategy = "Rolling": Эта стратегия гарантирует, что обновление будет выполняться пошагово, что минимизирует возможные проблемы с доступностью сервисов.

  • preferences: Этот блок содержит две ключевые настройки:

    • min_healthy_percentage = 100: Указывает, что в процессе обновления должен сохраняться 100%-ный уровень здоровья группы, что крайне важно для поддержания надежности сервиса.

    • instance_warmup = 120: Это время в секундах, которое позволяет новым инстансам "прогреться" перед тем, как они будут введены в эксплуатацию.

  • triggers = ["tag"]: Это триггеры, которые инициируют обновление инстансов при изменении указанных атрибутов. Это полезно, например, при изменении тегов ресурсов.

Также следует обратить особое внимание на блок launch_template. Часто там ставят просто `version = "$Latest"`. Так делать не нужно. Если вы установите значение $Latest для version, это означает, что autoscaling-группа всегда будет использовать последнюю версию шаблона запуска при создании новых экземпляров EC2. Однако это не вызовет автоматического обновления уже запущенных экземпляров, даже если шаблон измен��тся.

Для того чтобы инициировать процесс обновления экземпляров (instance refresh) при изменении шаблона, вы должны использовать значение latest_version от ресурса aws_launch_template в качестве версии шаблона. Таким образом, при каждом изменении шаблона и последующем применении Terraform он будет видеть изменения в версии шаблона и инициировать обновление экземпляров.

Теперь добавим в Terraform вызов Ansible, который нам нужен для контроля процесса обновления. Для этого используем специальный ресурс Terraform, известный под именем null_resource:

resource "null_resource" "ansible_run" {
  triggers = {
    template_version = aws_autoscaling_group.example.launch_template[0].version
  }

  provisioner "local-exec" {
    command = join(" ",
      [
        "ansible-playbook ${path.module}/asg_refresh_handler.yml -i 'localhost,'",
        "-e asg_name=${aws_autoscaling_group.example.name}"
      ]
    )
}

В Terraform, null_resource — это способ выполнения действий, которые не связаны с каким-либо фактическим ресурсом облачного провайдера. Этот ресурс идеален для интеграции внешних инструментов, таких как Ansible.

  1. Triggers: triggers — это конструкция в Terraform, которая указывает, при каких условиях ресурс должен быть пересоздан. В нашем случае, каждый раз, когда версия launch_template в aws_autoscaling_group.example изменяется, Terraform запустит Ansible playbook. Это гарантирует, что после каждого обновления ASG Ansible будет вызван для отслеживания статуса instance refresh.

  2. Provisioner "local-exec": Этот provisioner указывает Terraform выполнить команду на локальной машине. В данном случае мы запускаем Ansible playbook.

    • ansible-playbook ${path.module}/asg_refresh_waiter.yml

      указывает путь к нашему playbook.

    • -i 'localhost,' задает Ansible раб��тать на локальной машине.

    • -e asg_name=${aws_autoscaling_group.example.name} передает Ansible имя autoscaling группы, с которой нужно работать.

Таким образом, каждый раз, когда Terraform обновляет ASG из-за изменений в launch_template, он автоматически вызывает Ansible для отслеживания процесса instance refresh.

2. Составляем Ansible Playbook

Давайте теперь перейдем к разработке Ansible Playbook, который будет отслеживать процесс обновления, исходя из данных, полученных от AWS. Как можем увидеть из кода выше, нам нужен файл с именем asg_refresh_waiter.yml,который мы расположим в той же директории, что и код нашего модуля для terraform.

---
- name: ASG Refresh Handler
  hosts: localhost
  gather_facts: false
  connection: local
  tasks:

    - name: Obtain ASG Information
      amazon.aws.ec2_asg_info:
        name: '{{ asg_name }}'
      register: asg_status

    - name: Display ASG Instances
      debug:
        msg: '{{ asg_status.results[0].instances }}'

    - name: Display ASG Launch Template Info
      debug:
        msg: '{{ asg_status.results[0].launch_template }}'

    - name: Await Instance Refresh Completion
      amazon.aws.ec2_asg_info:
        name: '{{ asg_name }}'
      register: updated_asg_status
      retries: 300
      until:
        - >-
          updated_asg_status.results[0].instances
            | map(attribute='launch_template.version')
            | union([updated_asg_status.results[0].launch_template.version])
            | length == 1
        - >-
          updated_asg_status.results[0].instances
            | map(attribute='launch_template.version')
            | unique
            | length == 1
      when: asg_status.results[0].launch_template.version is defined

    - name: Display Updated Instances
      debug:
        msg: '{{ updated_asg_status.results[0].instances }}'

Разберем детали:

  • Obtain ASG Information: Этот таск извлекает текущую информацию о ASG, что позволяет оценить, требуется ли обновление и возможно ли его выполнение.

  • Display ASG Instances и Display ASG Launch Template Info: Эти задачи помогают в отладке, выводя текущую информацию о состоянии инстансов и шаблона запуска.

  • Await Instance Refresh Completion: Это сердце нашего playbook. Здесь мы используем механизм retries/until, что позволяет нам отслеживать процесс обновления до его завершения:

    • retries: 300 указывает, что таск будет повторяться до 300 раз, пока условие until не выполнится.

    • Этот таск использует условие until с двумя условиями для определения завершения процесса обновления.

Разбор условий в блоке until:

В задаче Await Instance Refresh Completion, в блоке until представлены две проверки. Эти проверки нужны для удостоверения, что все инстансы были обновлены до последней версии Launch Template.

  1. Первая проверка:

updated_asg_status.results[0].instances
  | map(attribute='launch_template.version')
  | union([updated_asg_status.results[0].launch_template.version])
  | length == 1

Эта проверка выполняет следующие действия:

  • Извлекает версии шаблонов запуска всех инстансов в ASG.

  • Соединяет полученный список версий с версией шаблона запуска ASG.

  • Проверяет, что все версии совпадают, то есть список содержит только одну уникальную версию.

  1. Вторая проверка:

updated_asg_status.results[0].instances
  | map(attribute='launch_template.version')
  | unique
  | length == 1

Вторая проверка убеждается в отсутствии различий между версиями Launch Templates среди инстансов, гарантируя, что все инстансы обновлены до последней версии.

Заключение

Если все сделано верно, то при запуске кода Terraform и появлении новой версии AMI будет обновлена версия launch_template для autoscaling группы, и автоматически будет запущен процесс instance refresh. После чего Terraform запустит Ansible playbook с указанными нами параметрами, передав плейбуку значение имени autoscaling группы.

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

Приведенный пример кода Ansible плейбука довольно универсален и зависит от единственного входного параметра - имени autoscaling группы. Поэтому может быть легко использован практически в любом окружении и с любым terraform кодом без изменений.

Надеюсь, что кому-то данный пример сочетания Terraform и Ansible поможет построить более эффективную и надежную систему обновления сервисов.