Using Terraform to Automate Infrastructure in VMware Cloud Director

Introduction


This article will teach you how to automate your infrastructure deployments using Terraform and cloud-init in a VMware Cloud Director environment.

In this guide, we will use Terraform to:

  • Create a network segment with subnet 172.16.1.1/24
  • Create a 1:1 NAT rule mapping x.x.x.x to 172.16.1.200 where x.x.x.x is a public IP on your Edge GW.
  • Create a firewall rule allowing ICMP, SSH, and HTTP(S) traffic to 172.16.1.200
  • Create a VM with a static IP of 172.16.1.200
  • Configure cloud-init to bootstrap WordPress on the VM

Prerequisites


We have created the guide specifically for customers deploying virtual machines from GleSYS templates in VMware Cloud Director. To complete this tutorial, you will need the following:

VMware Cloud Director API token

Refer to the official documentation for creating a VMware Cloud Director API token.

NSX Edge Gateway

This tutorial assumes that your VMware Cloud Director environment is configured with an NSX Edge Gateway with a public IP address.

Ensuring that your NSX Edge Gateway has no prior configuration is essential. Any configuration, such as firewall rules, will be overwritten.

If you want to follow this guide without impacting your production environment, email support@glesys.se, and we will configure a temporary environment for testing.

DNS

Set up a DNS record for the FQDN of your WordPress site wp.example.com to point to the public IP address of your NSX Edge Gateway.

set_vcd_vm_extraconfig

Download the set_vcd_vm_extraconfig binary from GitHub and place it in your working directory.

This tool provides a programmatic way of setting extraconfig properties on a VM in VMware Cloud Director. In this example, Terraform applies cloud-init configuration to the VM running WordPress after creation.

Deploying WordPress Using Terraform and cloud-init

Step 1 - Preparing cloud-init configuration


Before delving into the Terraform configuration, let's first create the cloud-init configuration we will use to install and configure WordPress when our VM boots for the first time.

In your working directory, create a file called metadata.yaml and paste the following configuration into it:

instance-id: 00000000-0000-0000-0000-000000000000 # replace with your own id 
local-hostname: wp.example.com # replace with the FQDN of your WordPress site
network:
  version: 2
  ethernets:
    ens192:
      addresses:
      - 172.16.1.200/24
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
      routes:
      - to: 0.0.0.0/0
        via: 172.16.1.1

Create a file called userdata.yaml and paste the following configuration into it. Ensure you replace all instances of the following:

  • glesys with your preferred username
  • wp.example.com with the FQDN of your WordPress site
  • user@example.com with a valid email address for the Let's Encrypt certificate
  • ecdsa-sha2-nistp256 AAAA... with your public SSH key
#cloud-config
users:
- name: glesys
  shell: /bin/bash
  sudo: ALL=(ALL) NOPASSWD:ALL
  lock_passwd: true
  ssh_authorized_keys: 
    - ecdsa-sha2-nistp256 AAAA...
manage_etc_hosts: true
packages:
  - apache2
  - php8.1-fpm
  - curl
  - php8.1-curl
  - php8.1-mysql
  - php8.1-gd
  - certbot
  - python3-certbot-apache
  - mysql-server
  - fail2ban
  - automysqlbackup
write_files:
  -
    content: |
      <VirtualHost *:80>
      ServerName wp.example.com

      DocumentRoot /home/glesys/web/public
        <Directory /home/glesys/web/public>
          Options -Indexes +FollowSymLinks +MultiViews
          AllowOverride All
          Require all granted

          <files xmlrpc.php>
            Require all denied
          </files>

          #PHP-FPM Socket
          <FilesMatch \.php$>
            SetHandler "proxy:unix:/var/run/wordpress.sock|fcgi://localhost/"
          </FilesMatch>
        </Directory>
      </VirtualHost>
    path: /etc/apache2/sites-available/wordpress.conf
  -
    content: |
      [wordpress]
      user = glesys
      group = glesys
      listen = /var/run/wordpress.sock
      listen.owner = www-data
      listen.group = www-data
      pm = ondemand
      pm.max_children = 50
      pm.process_idle_timeout = 10s
      pm.max_requests = 200
      chdir = /
    path: /etc/php/8.1/fpm/pool.d/wordpress.conf
runcmd:
  - sed -i 's/post_max_size \= .M/post_max_size \= 50M/g' /etc/php/8.1/fpm/php.ini
  - sed -i 's/upload_max_filesize \= .M/upload_max_filesize \= 50M/g' /etc/php/8.1/fpm/php.ini
  - 'systemctl restart php8.1-fpm'
  - 'a2enmod rewrite headers expires proxy_fcgi proxy_http'
  - 'a2ensite wordpress.conf'
  - 'a2dissite 000-default-conf'
  - 'systemctl restart apache2'
  - 'certbot --apache -d wp.example.com --agree-tos -m user@example.com --no-eff-email --redirect'
  - 'echo "postfix postfix/mailname        string  $(hostname --fqdn)" | sudo debconf-set-selections'
  - 'echo "postfix postfix/main_mailer_type        select  Internet Site" | sudo debconf-set-selections'
  - 'echo "postfix postfix/destinations    string  localhost" | sudo debconf-set-selections'
  - 'echo "postfix postfix/mynetworks      string  127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128" | sudo debconf-set-selections'
  - 'echo "postfix postfix/mailbox_limit   string  0" | sudo debconf-set-selections'
  - 'echo "postfix postfix/recipient_delim string  +" | sudo debconf-set-selections'
  - 'echo "postfix postfix/protocols       select  all" | sudo debconf-set-selections'
  - 'apt-get install postfix -y'
  - 'curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'
  - 'chmod 755 /usr/local/bin/wp'
  - PASSWORD=`openssl rand -base64 32`
  - mysql -e "create database wordpress;"
  - mysql -e "CREATE USER wordpress@localhost IDENTIFIED BY '$PASSWORD';"
  - mysql -e "GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost';"
  - mysql -e "FLUSH PRIVILEGES;"
  - chmod +x /home/glesys
  - mkdir -p /home/glesys/web/public
  - chown -R glesys:glesys -R /home/glesys/web/
  - 'sudo -u glesys -i -- wp core download --path=/home/glesys/web/public/ --quiet'
  - sudo -u glesys -i -- wp config create --path=/home/glesys/web/public/ --dbprefix=glesys_ --dbname=wordpress --dbuser=wordpress --dbpass="$PASSWORD"
  - 'ufw default deny incoming'
  - 'ufw allow OpenSSH'
  - 'ufw allow http'
  - 'ufw allow https'
  - 'ufw --force enable'

Step 2 - Initializing Terraform


In your working directory, create a file called main.tf and paste the following configuration into it:

terraform {
  required_providers {
    vcd = {
      source  = "vmware/vcd"
      version = "3.11.0"
    }
  }
}

provider "vcd" {
  user      = ""
  password  = ""
  auth_type = "api_token"
  api_token = var.vcd_api_token
  url       = "https://${var.vcd_url}/api"
  org       = var.vcd_org
  vdc       = var.vcd_vdc
}

Next, define the variables your project will use to make the code easier to reuse across environments.

Create a file called variables.tf and paste the following configuration:

variable "vcd_url" {
  type = string
  description = "Cloud Director URL (Example: 'vcd.dc-fbg1.glesys.net')"
}

variable "vcd_org" {
  type = string
  description = "Tenant Organization (Example: 'vdo-xxxxx')"
}

variable "vcd_user" {
  type = string
  description = "User to connect to Cloud Director (Example: 'administrator')"
}

variable "vcd_api_token" {
  type = string
  description = "API Token to authenticate to Cloud Director"
}

variable "vcd_vdc" {
  type = string
  description = "Organization Virtual Datacenter (Example: 'vdc-xxxxx')"
}

variable "vcd_edge" {
  type = string
  description = "Edge Gateway (Example: 't1-vdc-xxxxx-fbg1-01')"
}

Run terraform init to initialize the project and install the required providers:

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding vmware/vcd versions matching "3.11.0"...
- Installing vmware/vcd v3.11.0...
- Installed vmware/vcd v3.11.0 (signed by a HashiCorp partner, key ID 8BF53DB49CDB70B0)

Terraform has been successfully initialized!

Step 3 - Defining network resources


In your working directory, create a file called network.tf and paste the following configuration:

# since the edge gateway is not managed by tf, define a data resource for the edge gateway
data "vcd_nsxt_edgegateway" "my_edge" {
  name = var.vcd_edge
}

# network with subnet 172.16.1.1/24 for the wordpress server
resource "vcd_network_routed_v2" "wp_net" {
  name            = "wp-net"
  edge_gateway_id = data.vcd_nsxt_edgegateway.my_edge.id
  gateway         = "172.16.1.1"
  prefix_length   = 24
  dns1            = "8.8.8.8"
  dns2            = "8.8.4.4"
  static_ip_pool {
    start_address = "172.16.1.200"
    end_address   = "172.16.1.250"
  }
}

# destination nat rule mapping edge gateway public ip to the internal ip of the wordpress server
resource "vcd_nsxt_nat_rule" "wp_inbound" {
  edge_gateway_id  = data.vcd_nsxt_edgegateway.my_edge.id
  name             = "wp_inbound"
  rule_type        = "DNAT"
  external_address = tolist(data.vcd_nsxt_edgegateway.my_edge.subnet)[0].primary_ip
  internal_address = "172.16.1.200"
}

# source nat rule mapping the internal ip of the wordpress server to edge gateway public ip
resource "vcd_nsxt_nat_rule" "wp_outbound" {
  edge_gateway_id  = data.vcd_nsxt_edgegateway.my_edge.id
  name             = "wp_outbound"
  rule_type        = "SNAT"
  external_address = tolist(data.vcd_nsxt_edgegateway.my_edge.subnet)[0].primary_ip
  internal_address = "172.16.1.200"
}

# custom port profile for the wordpress server
resource "vcd_nsxt_app_port_profile" "wp_app_port_profile" {
  name  = "wp-app-port-profile"
  scope = "TENANT"
  app_port {
    protocol = "ICMPv4"
  }
  app_port {
    protocol = "TCP"
    port     = ["22", "80", "443"]
  }
}

# ip set for the wordpress server
resource "vcd_nsxt_ip_set" "wp_ip_set" {
  edge_gateway_id = data.vcd_nsxt_edgegateway.my_edge.id
  name            = "wp-ip-set"
  ip_addresses    = ["172.16.1.200"]
}

resource "vcd_nsxt_firewall" "my_edge_firewall" {
  edge_gateway_id = data.vcd_nsxt_edgegateway.my_edge.id
  # allow icmp, ssh, http, https to the wordpress server
  rule {
    action               = "ALLOW"
    name                 = "Allow ICMPv4, SSH, HTTP, HTTPS with destination to wp-ip-set"
    direction            = "IN"
    ip_protocol          = "IPV4"
    app_port_profile_ids = [vcd_nsxt_app_port_profile.wp_app_port_profile.id]
    destination_ids      = [vcd_nsxt_ip_set.wp_ip_set.id]
  }
  # allow outbound from the wordpress server
  rule {
    action      = "ALLOW"
    name        = "Allow all IPv4 traffic to any destination from wp-ip-set"
    direction   = "OUT"
    ip_protocol = "IPV4"
    source_ids  = [vcd_nsxt_ip_set.wp_ip_set.id]
  }
}

Step 4 - Defining server resources


In your working directory, create a file called server.tf and paste the following configuration:

# since the catalog is not managed by tf, define a data resource for the glesys templates catalog
data "vcd_catalog" "os_templates" {
  org  = "GleSYS"
  name = "GleSYS Templates"
}

# define a data resource for the ubuntu-2204 template
data "vcd_catalog_vapp_template" "ubuntu_2204" {
  catalog_id = data.vcd_catalog.os_templates.id
  name       = "ubuntu-2204"
}

# clone vm from the ubuntu-2204 template, leave powered off so the cloud-init configuration can be applied.
resource "vcd_vm" "wp" {
  # terraform should not power off the vm if it is running
  lifecycle {
    ignore_changes = [power_on]
  }
  name             = "wp"
  computer_name    = "wp"
  vapp_template_id = data.vcd_catalog_vapp_template.ubuntu_2204.id
  memory           = 4096
  cpus             = 2
  cpu_cores        = 1
  power_on         = false
  network {
    type               = "org"
    name               = vcd_network_routed_v2.wp_net.name
    ip_allocation_mode = "MANUAL"
    ip                 = "172.16.1.200"
    connected          = true
  }
  override_template_disk {
    bus_type    = "paravirtual"
    size_in_mb  = "10240"
    bus_number  = 0
    unit_number = 0
  }
}

# run set_vcd_vm_extraconfig command to apply cloud-init metadata and userdata to vm after it has been created.
# poweron flag is used to power on vm after cloud-init has been applied.
resource "null_resource" "wp_cloudinit" {
  depends_on = [vcd_vm.wp]
  # if terraform is running on a linux host, uncomment this block
  provisioner "local-exec" {
    command     = "${path.module}/set_vcd_vm_extraconfig -org ${var.vcd_org} -vdc ${var.vcd_vdc} -vm ${vcd_vm.wp.name} -e guestinfo.userdata=${base64gzip(file("${path.module}/userdata.yaml"))} -e guestinfo.metadata=${base64gzip(file("${path.module}/metadata.yaml"))} -e guestinfo.userdata.encoding=gzip+base64 -e guestinfo.metadata.encoding=gzip+base64 -poweron"
    interpreter = ["bash", "-c"]
    environment = {
      VCD_URL   = "https://${var.vcd_url}"
      VCD_USER  = var.vcd_user
      VCD_TOKEN = var.vcd_api_token
    }
  }
  # if terraform is running on a windows host, uncomment this block
  /*
  provisioner "local-exec" {
    command     = "${path.module}\\set_vcd_vm_extraconfig.exe -org ${var.vcd_org} -vdc ${var.vcd_vdc} -vm ${vcd_vm.wp.name} -e guestinfo.userdata=${base64gzip(file("${path.module}\\userdata.yaml"))} -e guestinfo.metadata=${base64gzip(file("${path.module}\\metadata.yaml"))} -e guestinfo.userdata.encoding=gzip+base64 -e guestinfo.metadata.encoding=gzip+base64 -poweron"
    interpreter = ["PowerShell", "-Command"]
    environment = {
      VCD_URL   = "https://${var.vcd_url}"
      VCD_USER  = var.vcd_user
      VCD_TOKEN = var.vcd_api_token
    }
  }
  */
}

Step 5 - Applying Terraform configuration


Ensure that your working directory resembles this layout:

$ ls -lh
-rw-r--r-- 1 jamesm jamesm  319 Jan 17 09:50 main.tf
-rw-r--r-- 1 jamesm jamesm  284 Jan 17 09:50 metadata.yaml
-rw-r--r-- 1 jamesm jamesm 2.6K Jan 17 09:50 network.tf
-rw-r--r-- 1 jamesm jamesm 2.6K Jan 17 09:50 server.tf
-rwxrwxrwx 1 jamesm jamesm 8.1M Jan 17 09:50 set_vcd_vm_extraconfig
-rw-r--r-- 1 jamesm jamesm  18K Jan 17 00:39 terraform.tfstate
-rw-r--r-- 1 jamesm jamesm 3.5K Jan 17 09:50 userdata.yaml
-rw-r--r-- 1 jamesm jamesm  659 Jan 17 09:50 variables.tf

Run terraform apply to apply your configuration and provision your infrastructure:

$ terraform apply -var vcd_url=vcd.dc-fbg1.glesys.net \
-var vcd_user=demo -var vcd_api_token=ABC12345678 \
-var vcd_org=vdo-##### -var vcd_vdc=vdc-##### -var vcd_edge=t1-vdc-#####-fbg1-01

Plan: 8 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

vcd_nsxt_ip_set.wp_ip_set: Creating...
vcd_nsxt_nat_rule.wp_inbound: Creating...
vcd_network_routed_v2.wp_net: Creating...
vcd_nsxt_nat_rule.wp_outbound: Creating...
vcd_nsxt_app_port_profile.wp_app_port_profile: Creating...
vcd_nsxt_app_port_profile.wp_app_port_profile: Creation complete after 4s vcd_nsxt_ip_set.wp_ip_set: Creation complete after 4s
vcd_nsxt_firewall.my_edge_firewall: Creating...
vcd_nsxt_nat_rule.wp_outbound: Creation complete after 8s
vcd_nsxt_nat_rule.wp_inbound: Creation complete after 11s
vcd_network_routed_v2.wp_net: Creation complete after 21s 
vcd_vm.wp: Creating...
vcd_nsxt_firewall.my_edge_firewall: Creation complete after 21s 
vcd_vm.wp: Creation complete after 1m37s
null_resource.wp_cloudinit: Creating...
null_resource.wp_cloudinit: Provisioning with 'local-exec'...
null_resource.wp_cloudinit: Creation complete after 52s

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

Open your web browser and browse to the FQDN of your site to complete the WordPress installation:

vcd-terraform-wp-install


Step 6 - Destroying Terraform configuration (optional)


Although not commonly used in production environments, Terraform can destroy the infrastructure that it has provisioned. It is particularly useful in lab scenarios such as this when we want to deploy infrastructure for testing or learning purposes and then destroy it as it is no longer needed.

The destroy command may fail when removing the vcd_network_routed_v2 resource, so you may need to run it twice.

$ terraform destroy -var vcd_url=vcd.dc-fbg1.glesys.net \
-var vcd_user=demo -var vcd_api_token=ABC12345678 \
-var vcd_org=vdo-##### -var vcd_vdc=vdc-##### -var vcd_edge=t1-vdc-#####-fbg1-01

Plan: 0 to add, 0 to change, 8 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

null_resource.wp_cloudinit: Destroying... 
null_resource.wp_cloudinit: Destruction complete after 0s
vcd_nsxt_nat_rule.wp_outbound: Destroying... 
vcd_nsxt_nat_rule.wp_inbound: Destroying...
vcd_nsxt_firewall.my_edge_firewall: Destroying... 
vcd_vm.wp: Destroying... 
vcd_nsxt_firewall.my_edge_firewall: Destruction complete after 3s
vcd_nsxt_ip_set.wp_ip_set: Destroying... 
vcd_nsxt_app_port_profile.wp_app_port_profile: Destroying... 
vcd_nsxt_app_port_profile.wp_app_port_profile: Destruction complete after 4s
vcd_nsxt_nat_rule.wp_inbound: Destruction complete after 7s
vcd_nsxt_nat_rule.wp_outbound: Destruction complete after 10s
vcd_nsxt_ip_set.wp_ip_set: Destruction complete after 11s
vcd_vm.wp: Destruction complete after 22s
vcd_network_routed_v2.wp_net: Destroying...
vcd_network_routed_v2.wp_net: Destruction complete after 7s

Destroy complete! Resources: 8 destroyed.

Conclusion


In this tutorial, you have used Terraform to build the infrastructure for running a WordPress server in VMware Cloud Director.

Furthermore, you have used cloud-init to initialize your virtual machine and automate the WordPress installation and configuration.

Now that you understand how Terraform and cloud-init work, you can extend this example to meet your production needs.

Here is a list of some suggestions:

  • Create a WordPress cluster and use Terraform to create an NSX load balancer to balance the traffic between multiple backend servers.
  • Modify cloud-init to deploy your web application on the virtual machine instead of WordPress.
  • Modify cloud-init to install Docker on the virtual machine and deploy Docker containers on the virtual machine.

The possibilities are endless.


Hittar du inte det du söker?

Kontakta oss gärna för mer information. Vi hjälper dig att komma fram till den bästa lösningen för dina behov.

Skicka e-post Ring 0200-23 88 00