Интеграция 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 с добавлением в «инвентори» 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
В данном случае mount_folder был заполнен как /var/postgres1
Так же реализована отправка webhook после окончания выполнения workflow
Настроенное уведомление в AWX
Уведомление отработает в случае успеха/провала выполнения workflow и отправит статус на локально стоящий flask сервер, который забирает значение статуса и смотрит на заполненную переменную JiraTicket. Данная переменная при создании «тикета» в jira заполняется автоматический и передается в AWX, чтобы отправился issue_key и сделал новый статус заявки.
По итогу, данный способ предоставления инфраструктурных мощностей вполне оправдан, особенно, если ваши рабочие процессы постоянно связаны с деплоем ВМ, а второе по трате времени — это jira с ее insight и прочими вещами. Остается сделать динамику полей и провести логические ассоциации с проектами.
В целом, даже если jira не входит в ваш рабочий стек, можно использовать web-интерфейс AWX с его Survey.