Интеграция Jira-AWX

«Деплой» виртуальных машин путем «тикета» в Jira

Всем привет, меня зовут Денис, я хотел бы поделиться опытом использования AWX в рамках одной из наших потребностей. Статья может быть полезна ребятам с «инфры», если в компании используется vmware и подобное cloud решение для частого деплоя, а для всяческой бюрократии и запросов вы обращайтесь в Jira.

Недавно @kuksepa выкладывала отличную статью про AWX по этому я не стану много описывать конкретно его, а постараюсь кратко описать процесс. Прошу не обращать внимание на замазанные элементы в них нет повествовательной нагрузки.

Схема использования данного решения

Схема использования данного решения

Jira

Для реализации в Jira создан новый проект (Deploy VM), пользовательские поля, 2 типа запроса (cloud и ЦОД) и 2 скрипта в scriprunner. Заранее говорю, что слабо знаком с динамикой полей и форм в jira и ввиду этого логика заполнения полей «тикета» пока что громоздкая

  • Проект Deploy VM имеет очередь заданий, созданы ассоциированные с ним поля, а также привязаны 2 типа запроса (Deploy и DeployCloud).

  • Скрипты (ScriptRunner) внутри jira запускаются при переводе «тикета» в статус «в работе» и посылают api запрос в AWX. Содержание скриптов в основном — это обработка полей и отправка POST запроса в требуемом AWX формате.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.user.ApplicationUser


						//Далее идет мапинг пользовательских полей
                                  ....
                                  


def apiUrl
switch (field2Value) {
    case "Ubuntu":
        apiUrl = "https://Ваш url/v2/workflow_job_templates/121/launch/"
        break
    case "WindowsServer2019":            //тут ендпоинт в зависимости от выбора ОС  
        apiUrl = "https://Ваш url/v2/workflow_job_templates/122/launch/"
        break
    default:
        log.error("Неверное значение для customfield_21402: ${field2Value}")
        throw new IllegalArgumentException("Не удалось определить URL для значения: ${field2Value}")
}

def requestBody = """{         //преобразование полей в тело запроса
    "extra_vars":{

        "computer_name": "${field3Value}",
		"VDC": "${field4Value}",
		"vapp_name": "${field5Value}",
	    "network": "${field6Value}",
		"ip": "${field7Value}",
		"gw": "${field8Value}",
		"ip_network": "${field9Value}",
        "mask": "${field10Value}",
		"cpu": "${field11Value}",
		"memory": "${field12Value}",
		"disk": "${field13Value}",
		"group_access": "${field14Value}",
        "disk2": "${field15Value}",
        "size": "${field16Value}",
        "mount_folder": "${field17Value}",
        "description": "${field18Value}",
        "JiraTicket": "${ticketNumber}"
	}
}"""

								//Далее формируем POST
                                  .....

Варианты запроса

Варианты запроса

На данный момент поля заполнения cloud (слева) и vcenter (справа) выглядит следующим образом. Где существует выбор из предоставляемых ресурсов, выполнен каскадный список с единичным выбором.

Terraform-Git

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

Начну с примеров шаблона, которые выполняет непосредственно terraform.

1) Шаблон по деплою ubuntu в cloud, сеть и имя хоста передаем через скрипт, т.к. опции кастомизации не работают.

variable "vcd_url" {} 
variable "org_name" {}
variable "vcd_max_retry_timeout" { default = "1800" }
variable "vcd_allow_unverified_ssl" { default = "true" }
terraform {
   backend "local" { path = "/home/dsoldatov/terraformstate/terraform.tfstate" }
   required_providers {
      vcd = {
      source  = "Ваше/vmware/vcd" 
	        version = ">=3.10.0"
    }
  }
}
provider "vcd" {
  user                  = "none"
  password              = "none"
  auth_type             = "api_token_file"
  api_token_file        = "token.json"
  allow_api_token_file  = true
  org                   = var.org_name
  vdc                   = "Ваш тенант-{{VDC}}"
  url                   = var.vcd_url
  max_retry_timeout     = var.vcd_max_retry_timeout
  allow_unverified_ssl  = var.vcd_allow_unverified_ssl
}
variable "attach_additional_disk" {
  type = bool
  default = "{{disk2}}"
}
resource "vcd_vapp_vm" "{{computer_name}}" {
  vapp_name         = "{{vapp_name}}"
  name              = "{{computer_name}}"
  catalog_name      = "Ubuntu_new"
  template_name     = "Ваш шаблон"
  memory            = "{{memory}}"
  cpus              = "{{cpu}}"
  accept_all_eulas  = true
  description       =  "{{description}}"
  storage_profile   = "{{disk}}"
  computer_name     = "{{computer_name}}"

network {
    type               = "org"
    name               = "{{network}}"
    ip                 = "{{ip_network}}"
    ip_allocation_mode = "MANUAL"
    is_primary         = true
}
  customization {
    allow_local_admin_password = "true"
    auto_generate_password = "false"
    admin_password = "Ваше"
    enabled = true
    initscript = <<-SCRIPT
      #!/bin/bash
      sudo awk '{gsub("Ваш IP шаблона", "{{ip}}")}1' /etc/netplan/00-installer-config.yaml > tmpfile && sudo mv tmpfile /etc/netplan/00-installer-config.yaml
      sudo awk '{gsub("Ваш шлюз шаблона", "{{gw}}")}1' /etc/netplan/00-installer-config.yaml > tmpfile && sudo mv tmpfile /etc/netplan/00-installer-config.yaml
      sudo awk '{gsub("Ваш шаблон шаблона", "{{computer_name}}")}1' /etc/hostname > tmpfile && sudo mv tmpfile /etc/hostname
      netplan apply
      reboot
    SCRIPT
  } 
}
resource "time_sleep" "wait_for_vm" {
create_duration = "30s"
}
resource "vcd_vm_internal_disk" "additional_disk" {
count         = var.attach_additional_disk ? 1 : 0
depends_on = [time_sleep.wait_for_vm]
vapp_name     = "{{vapp_name}}"
vm_name         = "{{computer_name}}"
allow_vm_reboot = "true"
bus_number        = 0
unit_number       = 1
bus_type        = "parallel"
size_in_mb = "{{size}}"
}

2) Шаблон по деплою windows server в vsphere используются 2 файла. В этом случае опции кастомизации работают и позволяют сменить сеть и авторизовать в домен. Также тут имеет место быть выбор между datastore и datacluster

provider "vsphere" {
  user           = "${var.vsphere_user}"
  password       = "${var.vsphere_password}"
  vsphere_server = "${var.vsphere_server}"
 
  allow_unverified_ssl = true
}
 
data "vsphere_datacenter" "dc" {
  name = "{{datacenter}}"
}
data "vsphere_datastore" "datastore" {
count         = var.use_datastore_cluster ? 0 : 1
name         = "{{datastore}}"
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_datastore_cluster" "datastore_cluster" {
count         = var.use_datastore_cluster ? 1 : 0
name         = "{{datastore}}"
datacenter_id = data.vsphere_datacenter.dc.id
}
 
data "vsphere_compute_cluster" "cluster" {
  name          = "{{cluster}}"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
 
data "vsphere_network" "network" {
  name          = "{{env}}_vlan{{vlan}}"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
 
data "vsphere_virtual_machine" "template" {
 name          = "Ваш шаблон"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
#data "vsphere_custom_attribute" "attribute" {
#  name = "Описание"
#}
terraform {
  backend "local" { path = "/home/dsoldatov/terraformstate/terraform.tfstate" }
  required_providers {
    Ваше = {
      source  = "Ваше"
    }
  }
}
variable "use_datastore_cluster" {
description = "use datastore cluster"
type        = bool
default     = "{{datacluster}}"
}
locals {
add_extra_disk = "{{disk2}}"
selected_datastore = var.use_datastore_cluster ? data.vsphere_datastore_cluster.datastore_cluster[0].id : data.vsphere_datastore.datastore[0].id
}
resource "vsphere_virtual_machine" "vm1" {
  name             = "{{computer_name}}"
  resource_pool_id = "${data.vsphere_compute_cluster.cluster.resource_pool_id}"
  datastore_id     =   local.selected_datastore
  folder = "{{folder}}"
#  custom_attributes = tomap({"${data.vsphere_custom_attribute.attribute.id}" = "{{description}}"})	
   
  num_cpus = "{{cpu}}"
  memory   = "{{memory}}"
  firmware = "efi"
  guest_id = "${data.vsphere_virtual_machine.template.guest_id}"
 
  scsi_type = "${data.vsphere_virtual_machine.template.scsi_type}"
  	
 
  network_interface {
    network_id   = "${data.vsphere_network.network.id}"
    adapter_type = "${data.vsphere_virtual_machine.template.network_interface_types[0]}"
  }
  
  disk {
    label            = "disk0"
    size             = "${data.vsphere_virtual_machine.template.disks.0.size}"
    eagerly_scrub    = "${data.vsphere_virtual_machine.template.disks.0.eagerly_scrub}"
    thin_provisioned = "${data.vsphere_virtual_machine.template.disks.0.thin_provisioned}"
}
  
  dynamic "disk" {
    for_each = local.add_extra_disk ? [1] : []
    content {
       label            = "disk1"
       size             = "{{size}}"
       eagerly_scrub    = false
       thin_provisioned = false
       unit_number      = 1
       controller_type = "scsi"
     }
}

  
  clone {
    template_uuid = "${data.vsphere_virtual_machine.template.id}"
 

  customize {
      windows_options {
        computer_name         = "{{computer_name}}"
        join_domain           = "Ваш домен"
        domain_admin_user     = "{{login}}"
        domain_admin_password = "{{pass}}"
#        admin_password        = var.local_adminpass
        run_once_command_list = [
          "C:\\Distr\\installer.exe /s",
          "msiexec /i C:\\Distr\\Ваше /quiet"
        ]
       }
      
      network_interface {
        ipv4_address = "{{ip_network}}"
        ipv4_netmask = "{{mask}}"
        dns_server_list     = ["Ваши dns"]
      }
 
      ipv4_gateway = "{{gw}}"
}
  }
}

Запуск задания и передача данных в terraform инициируется ansible шаблоном. Пример шаблона.

  gather_facts: false
  tasks:   
  - name: Wait   #Ожидание в случае параллельно работающего процесса деплоя
    ansible.builtin.wait_for:
      path: /home/dsoldatov/terraformstate/.terraform.tfstate.lock.info
      state: absent

  - name: Run state.sh script #Запуск скрипта на сброс состояния
    command: /home/dsoldatov/templates_cloud_windows/state.sh
    ignore_errors: yes
    args:
     chdir: /home/dsoldatov/templates_cloud_windows/    
 
  - name: Copy #Замена шаблона на деплой
    template:
      src: ../Playbooks/main_windows.j2
      dest: /home/dsoldatov/templates_cloud_windows/main.tf
    
  - name: Init
    command: "terraform init"
    args:
      chdir: /home/dsoldatov/templates_cloud_windows
 
  - name: Plan                                             #Инит и запуск деплоя
    command: terraform plan -out=plan.out
    args:
      chdir: /home/dsoldatov/templates_cloud_windows

  - name: Apply
    command: terraform apply -input=false plan.out
    args:
      chdir: /home/dsoldatov/templates_cloud_windows

  - name: Pause #Пауза для последеплойного состояния
    ansible.builtin.pause:
      seconds: 60

AWX

Для реализации получения на основании заполненного «тикета» в Jira готовой ВМ, авторизованной в домене, возможностью автоматической разметки второго диска с «мапингом» необходимого каталога, а также для автоматической смены статуса «тикета» в Jira, потребовалось сделать несколько дополнительных решений.

Этапы workflow деплоя ubuntu с добавлением в

Этапы workflow деплоя ubuntu с добавлением в «инвентори» AWX, авторизацией в домене и добавлением и разметкой доп. диска.

Для каждого из деплоев в AWX созданы workflow с цепочкой из ряда job_template, т.к. после деплоя ВМ нам необходимо, например, добавить ВМ в домен, a для этого после деплоя (terraform) потребуется выполнение ansible шаблона (кроме windows server в vsphere) с AWX (job_template) на авторизацию в домен, но мы не сможем его выполнить, если данного хоста нет в «инвентори». Поэтому первым делом во всех workflow мы выполняем именно этот ansible шаблон.

- hosts: localhost
  tasks:
  - name: Add new host
    uri:
       url: "ваш url"
       method: POST
       headers:
         Content-Type: "application/json"
         Authorization: "Bearer Ваш токен"
       body: >
             {
             "name": "{{ computer_name }}",
             "variables": "{\"ansible_host\": \"{{ ip_network }}\"}"
             }
       body_format: json    
       status_code: 201
    register: result
  - name: Debug result
    debug:
      var: result

Добавление хоста в «инвентори» на основании переданных Jira переменных из «тикета». Токен заранее прописан от технической jira УЗ с определенными правами в AWX.

Авторизация в домен реализована в рамках job_template ansible шаблонов идущих после основного деплоя.

- name: enter domain
  hosts: "{{computer_name}}"
  gather_facts: false
  vars_files:
    - ../Playbooks/credentials_win.yml
  tasks:
     - name: Pause
       ansible.builtin.pause:
        seconds: 90

     - name: Join domain
       win_domain_membership:
        dns_domain_name: "Ваше домен"
        hostname: "{{computer_name}}"
        domain_admin_user: "{{login}}"
        domain_admin_password: "{{pass}}"
        state: domain

«Креды» для авторизации в домен подставляются из файла credentials_win.yml записанные в ansible vault формате. Без таймаута winrm не способен выполнить шаблон.

Для удобства использования «последеплойной» Ubuntu c доп. диском, в workflow добавлен шаблон ansible с процедурой разметки

- name: add_seconddisk
  hosts: "{{ computer_name }}"
  become: yes
  vars:
    disk2: "{{ lookup('env', 'DISK2') | default(false) }}"  #Переводится в true если потребовался второй диск и при составлении заявки вы выбрали true
  tasks:
    - name: reboot                   #Проводим ребут и ждем когда сервер перезапустится
      shell: |
       #!/bin/bash
            /sbin/shutdown -r now     
       sleep 45
      async: 1
      poll: 0
    - name: Wait
      wait_for_connection:
        delay: 30
        timeout: 45 
    - name: Resize
      block:
        - name: Copy #Проводими создание LVM, разметку и мапинг папки к диску
          copy:
            dest: /home/ansible_usr/disk2.sh
            content: |
              #!/bin/bash
              sudo echo -e "nn\np\n1\n\n\nw\nq" | sudo fdisk /dev/sdb
              sudo pvcreate /dev/sdb1
              sudo vgcreate datavolume /dev/sdb1
              sudo lvcreate -l +100%FREE -n data datavolume
              sudo mkfs.ext4 /dev/mapper/datavolume-data
              sudo mkdir -p "{{ mount_folder }}"
              sudo echo "/dev/datavolume/data {{ mount_folder }}     ext4    rw     1 1" >> /etc/fstab
              sudo mount -a
            mode: '0755'

        - name: Execute
          shell: /home/ansible_usr/disk2.sh

        - name: Remove
          file:
            path: /home/ansible_usr/disk2.sh
            state: absent
      when: disk2 | bool
    - name: Skip disk2             #Пропускаем процедуру, если false
      debug:
        msg: "Disk skipped."
      when: not disk2 | bool
f4f3b08615ed867aa5e543171d1c3233.png

В данном случае mount_folder был заполнен как /var/postgres1

Так же реализована отправка webhook после окончания выполнения workflow

Настроенное уведомление в AWX

Настроенное уведомление в AWX

Уведомление отработает в случае успеха/провала выполнения workflow и отправит статус на локально стоящий flask сервер, который забирает значение статуса и смотрит на заполненную переменную JiraTicket. Данная переменная при создании «тикета» в jira заполняется автоматический и передается в AWX, чтобы отправился issue_key и сделал новый статус заявки.

49b86910fc16bd72a856866919d1b580.png

По итогу, данный способ предоставления инфраструктурных мощностей вполне оправдан, особенно, если ваши рабочие процессы постоянно связаны с деплоем ВМ, а второе по трате времени — это jira с ее insight и прочими вещами. Остается сделать динамику полей и провести логические ассоциации с проектами.

В целом, даже если jira не входит в ваш рабочий стек, можно использовать web-интерфейс AWX с его Survey.

?utm_source=habrahabr&utm_medium=rss&utm

© Habrahabr.ru