Citrix ADC – Automation via Terraform

Im Blogbeitrag über Cloud Native Networking mit Citrix ADC haben wir uns den Citrix ADC (Application Delivery Controller) im Cloud-Native-Umfeld angesehen. Durch die automatische Konfiguration des ADC’s aus dem Kubernetes Cluster heraus, ist die dynamische Skalierung der Microservices ADC-seitig also kein Thema mehr. Die Automatisierung muss hier allerdings noch nicht zu Ende sein, denn es macht durchaus Sinn, das ADC-Deployment von Grund auf zu automatisieren. In diesem Beitrag sehen wir uns deshalb die automatisierte Bereitstellung des ADC’s via Terraform an – mit allen Vorteilen, die Infrastructure as Code (IaC) mit sich bringt. Mehr zu IaC hier im Blog bei uns.

Was macht der Citrix ADC?

Der Citrix ADC (ehemals Citrix Netscaler) ist eine Lösung, mit der sich sowohl monolithische Anwendungen als auch Applikationen, die auf Microservices basieren, bereitstellen lassen. Als zentrale Netzwerkkomponente sorgt er zum einen für ein ausgeglichenes Load Balancing und zum anderen für die gesicherte Bereitstellung der Dienste. Folgende Aufgaben können über den ADC abgebildet werden:

  • Load Balancing
  • Content Switching
  • Web Application Firewall (WAF)
  • Bot Management
  • Citrix Gateway
  • Authentication, authorization and auditing (AAA)
  • Reverse Proxy

Schlussendlich kümmert sich der ADC um die durchgängige und sichere Bereitstellung der Applikation – und das über die unterschiedlichsten Plattformen (Private/Public Clouds) hinweg. Dabei fühlt er sich im Layer 4 genauso zuhause wie im Layer 7.

Terraform – Ein grober Überblick

Mit Terraform von HashiCorp wird der Zustand einer Infrastruktur definiert. Unabhängig von Cloud oder der Private Cloud werden Ressourcen, die angelegt werden sollen, als Code definiert. Über Provider werden die „Ziele“ angesprochen – etwa Azure, Google Cloud, AWS, VMware und Co. Die Provider können ebenso wie Module von der Terraform Registry verwendet werden – mehr dazu aber später. Apropos Module: Module sind kleine wiederverwertbare Codesnippets.

Wie passen Citrix ADC und Terraform jetzt zusammen?

Dank der Vorteile von IaC wird die Bereitstellung von Ressourcen unabhängiger und schneller. So können Unternehmen auf neue Releases und Sicherheitsupdates schneller reagieren – beispielsweise durch eine Pipeline, die automatisch gestartet wird, wenn eine neue ADC-Firmware im Software-Repository liegt.

Auch die unterschiedlichen Stufen der Betriebsumgebungen (Entwicklung/Test/Prod) können einfacher abgedeckt werden. Die Definition der Ressourcen ist im Code bereits vorhanden, wodurch die Bereitstellung an die unterschiedlichen Betriebsumgebungen einfacher angepasst werden kann.

Die Dokumentation der ADC-Systeme und der darauf liegenden Konfigurationen ist ebenfalls vereinfacht, denn alle Informationen sind im Code und bestenfalls in einem Git hinterlegt. So können Änderungen direkt nachvollzogen werden.

Eine Beispielkonfiguration mit Terraform und ADC

Um dem Beispiel folgen zu können, sollten Sie über das Wissen verfügen, wie eine Terraform-Konfiguration ausgeführt wird und wie Terraform sich grundsätzlich verhält. Eine Einführung in Terraform erhalten Sie über HashiCorp Learn – Terraform Tutorials. Die benötigten Passwörter werden in diesem Beispiel in HashiCorp Vault abgelegt bzw. aus Vault abgerufen. Auch hier gibt es von im HashiCorp Learn Tutorials dafür.

Citrix ADC Deployment auf VMware vSphere

Im Folgenden stellen wir einen Citrix ADC automatisiert auf VMware vSphere bereit. Die dazugehörigen Secrets befinden sich in einem Vault – ebenfalls von HashiCorp. Ist kein Vault vorhanden, können die Secrets auch über Environment-Variablen definiert werden.


Disclaimer: Die Secrets werden von Terraform im State abgelegt. Der Zugang zum State (terraform.tfstate) sollte also abgesichert werden!


Provider Configuration – provider.tf

In der provider.tf definieren wir, welche Ziele angesprochen werden sollen, um Informationen abzurufen oder die Ressourcen anzulegen. Über die Datablocks werden die Secrets von Vault angesprochen und im „vSphere Provider“ über data.vault_generic_secret aufgerufen.

provider "vsphere" {
  user           = data.vault_generic_secret.vsphere.data["vsphere_user"]
  password       = data.vault_generic_secret.vsphere.data["vsphere_pw"]
  vsphere_server = var.vsphere_server
  # If you have a self-signed cert
  allow_unverified_ssl = true
}

provider "vault" {
  address         = "https://var.vault-url"
  ca_cert_file    = var.vault_ca_cert
  skip_tls_verify = true
}

provider "citrixadc" {
  endpoint             = "http://${var.citrix_adc_nsip}"
  username             = data.vault_generic_secret.citrixadc.data["username"]
  password             = data.vault_generic_secret.citrixadc.data["automation-passwd"]
  insecure_skip_verify = true
}

data "vault_generic_secret" "vsphere" {
  path = "secret/labul"
}

data "vault_generic_secret" "citrixadc" {
  path = "secret/citrix-adc/credential"
}

Festlegen der Versionen – versions.tf

Mehr Informationen über die Versionierung: Link zur Hersteller-Doku

terraform {
  required_providers {
    vsphere = {
      source  = "hashicorp/vsphere"
      version = "2.2.0"
    }
    vault = {
      source  = "hashicorp/vault"
      version = "3.8.2"
    }
    citrixadc = {
      source  = "citrix/citrixadc"
      version = "1.20.0"
    }
  }
}

Definition der Umgebung – citrix-adc.tf

An dieser Stelle (oftmals auch unter „main.tf“ zu finden) wird unsere Umgebung schlussendlich definiert. Es werden ebenfalls über „Datablocks“ Informationen abgerufen, auf die in der Ressource zurückgegriffen wird.

Der erste Ressourcen-Block definiert die virtuelle Maschine: resource "vsphere_virtual_machine" "vm_citrix_adc". Hier wird der VMware vSphere Provider verwendet, eine importierte OVA als Template genutzt und zum neuen ADC geklont. Durch die Möglichkeit des Auto-Provisionings kann man im ESXi über Configuration Parameters Startparameter übergeben. Damit entfällt die typische Initialkonfiguration des ADC’s: extra_config = { "machine.id" = "ip=${var.citrix_adc_nsip}&netmask=${var.citrix_adc_mask}&gateway=${var.citrix_adc_gw}" }

Anschließend wird im Block resource "time_sleep" "wait_90_seconds" ein Timeout gesetzt, um sicherzugehen, dass der ADC auch gebootet ist.

Die erste Konfiguration auf dem ADC setzt ein neues Passwort; ab hier wird der Citrix Provider verwendet. Das Standard-nsroot-Passwort wird zu dem Wert geändert, den Terraform aus dem Vault gelesen hat. –> Bitte Disclaimer beachten: Dieser Wert steht im Klartext im terraform.tfstate….

Im Anschluss setzen wir eine SNIP (resource "citrixadc_nsip" "snip") und verbinden den ADC mit dem Application Delivery Manager, um eine sogenannte Pooled Licence zu erhalten: resource "citrixadc_nscapacity" "tf_pooled".

Wenn keine Pooled-Capacity-Lizenzen zur Verfügung stehen, können die Lizenzen auch statisch übermittelt werden. Dazu einfach in der VMware Resource die MAC-Adresse mitgeben und über „Systemfile“ das Lizenzfile auf den ADC bringen; hierzu wiederum das Lizenzfile als Variable ablegen und im filecontent = var.licfile z.b angeben.

Die Konfiguration wird zwischendurch immer wieder über den Block resource "citrixadc_nsconfig_save" … gesichert.

Im letzten Block resource "citrixadc_installer" wird der ADC noch auf die aktuelle Version gebracht.

Über depends_on kann sichergestellt werden, dass die Reihenfolge der Ressourcen eingehalten wird.

data "vsphere_datacenter" "dc" {
  name = var.vsphere_dc
}

data "vsphere_datastore" "datastore" {
  name          = var.vm_datastore
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_compute_cluster" "cluster" {
  name          = var.vsphere_cluster
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_network" "network" {
  name          = var.vm_network
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_virtual_machine" "template" {
  name          = var.vm_template
  datacenter_id = data.vsphere_datacenter.dc.id
}

resource "vsphere_virtual_machine" "vm_citrix_adc" {
  name                       = var.citrix_adc_name
  annotation                 = var.citrix_adc_anno
  firmware                   = data.vsphere_virtual_machine.template.firmware
  resource_pool_id           = data.vsphere_compute_cluster.cluster.resource_pool_id
  datastore_id               = data.vsphere_datastore.datastore.id
  folder                     = var.userfolder
  num_cpus                   = 2
  memory                     = 4096
  guest_id                   = data.vsphere_virtual_machine.template.guest_id
  wait_for_guest_net_timeout = 0
  wait_for_guest_ip_timeout  = 0

  scsi_type = data.vsphere_virtual_machine.template.scsi_type

  extra_config = {
    "machine.id" = "ip=${var.citrix_adc_nsip}&netmask=${var.citrix_adc_mask}&gateway=${var.citrix_adc_gw}"
  }

  network_interface {
    network_id   = data.vsphere_network.network.id
    adapter_type = data.vsphere_virtual_machine.template.network_interface_types[0]
  }

  disk {
    label            = "BaseDisk0"
    size             = data.vsphere_virtual_machine.template.disks.0.size
    eagerly_scrub    = false
    thin_provisioned = true
  }

  clone {
    template_uuid = data.vsphere_virtual_machine.template.id
    linked_clone  = false
  }
}

resource "time_sleep" "wait_90_seconds" {
  depends_on      = [vsphere_virtual_machine.vm_citrix_adc]
  create_duration = "90s"
}

resource "citrixadc_password_resetter" "pw-resetter" {
  depends_on   = [time_sleep.wait_90_seconds]
  username     = "nsroot"
  password     = "nsroot"
  new_password = data.vault_generic_secret.citrixadc.data["automation-passwd"]
}

resource "citrixadc_nsconfig_save" "save_pw-reset_conf" {
  depends_on = [
    citrixadc_password_resetter.pw-resetter
  ]
  all       = true
  timestamp = "at deployment"
}


resource "citrixadc_nsip" "snip" {
  depends_on = [
    citrixadc_nsconfig_save.save_pw-reset_conf
  ]

  ipaddress = var.citrix_adc_snip
  type      = "SNIP"
  netmask   = var.citrix_adc_mask
}

resource "citrixadc_nsconfig_save" "save_snip_conf" {
  depends_on = [
    citrixadc_nsip.snip
  ]
  all       = true
  timestamp = "at deployment"
}

resource "citrixadc_nslicenseserver" "tf_licenseserver" {
  depends_on = [
    citrixadc_nsconfig_save.save_snip_conf
  ]
  servername = var.adm-ip
  port       = 27000
}

resource "citrixadc_nscapacity" "tf_pooled" {
  depends_on = [
    citrixadc_nslicenseserver.tf_licenseserver
  ]
  bandwidth = 1000
  unit      = "Mbps"
  edition   = "Platinum"
}


resource "citrixadc_nsconfig_save" "save_lic_conf" {
  depends_on = [
    citrixadc_nscapacity.tf_pooled
  ]
  all       = true
  timestamp = "at deployment"
}

resource "citrixadc_rebooter" "tf_rebooter" {
  depends_on = [
    citrixadc_nsconfig_save.save_lic_conf
  ]
  timestamp            = "license reboot"
  warm                 = true
  wait_until_reachable = true
}

resource "time_sleep" "wait_for_lic" {
  depends_on = [
    citrixadc_rebooter.tf_rebooter
  ]
  create_duration = "30s"
}

resource "citrixadc_nsconfig_save" "save_ns_conf" {
  depends_on = [
    resource.time_sleep.wait_for_lic
  ]
  all       = true
  timestamp = "finished deployment"
}

resource "citrixadc_installer" "avpx_update" {
  depends_on = [
    citrixadc_nsconfig_save.save_ns_conf
  ]
  url                  = "http://${var.sfdepot}/${var.tgzversion}"
  y                    = true
  l                    = false
  wait_until_reachable = true
}

Variablen definieren – variables.tf

Innerhalb der variables.tf werden alle Variablen definiert. Gefüllt werden diese später über eine separate Datei. An dieser Stelle wäre es bereits möglich, defaults zu setzen.

variable "vault_ca_cert" {
  type = string
}
variable "vsphere_server" {
  type = string
}
variable "vsphere_dc" {
  type = string
}
variable "vsphere_cluster" {
  type = string
}
variable "userfolder" {
  type = string
}
variable "vm_datastore" {
  type = string
}
variable "vm_network" {
  type = string
}
variable "adm-ip" {
  type = string
}
variable "vm_template" {
  type = string
}
variable "citrix_adc_name" {
  type = string
}
variable "citrix_adc_anno" {
  type = string
}
variable "citrix_adc_nsip" {
  type = string
}
variable "citrix_adc_mask" {
  type = string
}
variable "citrix_adc_gw" {
  type = string
}
variable "citrix_adc_snip" {
  type = string
}
variable "citrix_adc_VIP1" {
  type = string
}
variable "tgzversion" {
    type = string
}
variable "sfdepot" {
  type = string
}

Variablen mit Werten belegen – terraform.tfvars

Die definierten Variablen werden jetzt in diesem File mit Werten hinterlegt.

vault_ca_cert   = "Root-CA für Vault"
vsphere_server  = "http/s für das vcenter"
vsphere_dc      = "Datacenter innerhalb vSphere"
vsphere_cluster = "VMvware Cluster"
userfolder      = "Ordner in VMware"
vm_datastore    = "Welcher Datastore"
vm_network      = "VM Netzwerk"
vm_template     = "Templatename für ADC"
adm-ip          = "ADM IP für pooled license"
citrix_adc_name = "VM Name"
citrix_adc_anno = "Annotation für die VM"
citrix_adc_nsip = "ADC NSIP (Netscaler IP)"
citrix_adc_mask = "255.255.255.0"
citrix_adc_gw   = "Gatewayadresse"
citrix_adc_snip = "Subnet IP"
sfdepot         = "Webserver für das Softwarerepo"
tgzversion      = "build-13.1-27.59_nc_64.tgz"

What’s next?

Der ADC wird nun automatisiert bereitgestellt und auf die gewünschte Version gebracht. Der nächste Schritt wäre nun, die auf dem ADC bereitgestellten Applikationen zu automatisieren – wahlweise über Module oder über Stylebooks via Citrix Application Delivery Manager (ADM). Dieser kann ebenfalls über Provider angesprochen werden.

Fazit

Durch die Tatsache, dass die Konfiguration mit Variablen bestückt ist, können wir die unterschiedlichen Umgebungen sehr einfach bedienen – wiederhol- und berechenbar! Pro Umgebung (Stage) wird eine Umgebung.tfvars erzeugt und mit den tatsächlichen Werten gefüllt.
Über den Aufruf terraform apply -var-file="/Pfad/Umgebung.tfvars" kann die jeweilige Zielumgebung (Entwicklungs-, Test- oder Produktionsumgebung) bereitgestellt werden. Die bessere Alternative ist jedoch, direkt auf Terraform Workspaces zu setzen. Der Automatisierung des NetScalers via IaC-Tools steht somit nichts mehr im Wege.