> For the complete documentation index, see [llms.txt](https://docs.glesys.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.glesys.com/products/compute/vmware-cloud-director-as-a-service/how-tos/using-terraform-to-automate-infrastructure-in-vmware-cloud-director.md).

# Using Terraform to automate infrastructure in VMware Cloud Director

***

Here, we'll show 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](https://docs.vmware.com/en/VMware-Cloud-Director/10.4/VMware-Cloud-Director-Tenant-Portal-Guide/GUID-A1B3B2FA-7B2C-4EE1-9D1B-188BE703EEDE.html) 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.

## 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:

{% code title="metadata.yaml" %}

```yaml
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
```

{% endcode %}

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

{% code title="userdata.yaml" %}

```yaml
#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'
```

{% endcode %}

### Step 2 – Initializing Terraform

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

{% code title="main.tf" %}

```
terraform {
  required_providers {
    vcd = {
      source  = "vmware/vcd"
      version = "3.13.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
}

```

{% endcode %}

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:

{% code title="variables.tf" %}

```
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_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')"
}
```

{% endcode %}

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

{% code title="Command" %}

```
terraform init
```

{% endcode %}

This will output something like this:

{% code title="Output" %}

```
Initializing the backend...

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

Terraform has been successfully initialized!
```

{% endcode %}

### Step 3 – Defining network resources

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

{% code title="network.tf" %}

```
# 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]
  }
}
```

{% endcode %}

### Step 4 – Defining server resources

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

{% code title="server.tf" %}

```
# 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
resource "vcd_vm" "wp" {
  name             = "wp"
  computer_name    = "wp"
  vapp_template_id = data.vcd_catalog_vapp_template.ubuntu_2204.id
  memory           = 4096
  cpus             = 2
  cpu_cores        = 1
  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
  }
  set_extra_config {
    key = "guestinfo.userdata"
    value = base64gzip(file("${path.module}/userdata.yaml"))
  }
  set_extra_config {
    key = "guestinfo.metadata"
    value = base64gzip(file("${path.module}/metadata.yaml"))
  }
  set_extra_config {
    key = "guestinfo.userdata.encoding"
    value = "gzip+base64"
  }
  set_extra_config {
    key = "guestinfo.metadata.encoding"
    value = "gzip+base64"
  }
}
```

{% endcode %}

### Step 5 – Applying Terraform configuration

Ensure that your working directory resembles this layout by running `ls -lh`:

{% code title="Output" %}

```
-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
-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
```

{% endcode %}

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

<pre data-title="Commands, output and input. Commands and inputs are highlighted."><code><strong>terraform apply -var vcd_url=vcd.dc-fbg1.glesys.net \
</strong><strong>-var vcd_api_token=ABC12345678 \
</strong><strong>-var vcd_org=vdo-##### -var vcd_vdc=vdc-##### -var vcd_edge=t1-vdc-#####-fbg1-01
</strong>
Plan: 7 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.

<strong>  Enter a value: yes
</strong>
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

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
</code></pre>

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

<figure><img src="/files/1xCSNRqmgebiBhEZqwWb" alt=""><figcaption></figcaption></figure>

### 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.

<pre data-title="Commands, output and input. Commands and inputs are highlighted."><code><strong>terraform destroy -var vcd_url=vcd.dc-fbg1.glesys.net \
</strong><strong>-var vcd_api_token=ABC12345678 \
</strong><strong>-var vcd_org=vdo-##### -var vcd_vdc=vdc-##### -var vcd_edge=t1-vdc-#####-fbg1-01
</strong>
Plan: 0 to add, 0 to change, 7 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.

<strong>  Enter a value: yes
</strong>
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: 7 destroyed.
</code></pre>

## Conclusion

In this how-to, 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.glesys.com/products/compute/vmware-cloud-director-as-a-service/how-tos/using-terraform-to-automate-infrastructure-in-vmware-cloud-director.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
