Deploy Web Server on Google Compute Engine (GCE) with Terraform
Tags: terraform, devops, infrastructure, googlecloudplatform
Published: 2020-01-13 21:26:08

*Originally published at [pbhadani.com](https://pbhadani.com/posts/deploy-webserver-compute-instance/)*
<br>
In this blog, I will show how to deploy a Web Server (Nginx) using Terraform on Google Compute Engine(GCE).
There are many ways to deploy `Nginx` server on GCP (like on GKE, App Engine, GCE etc.) but for this post I will use GCE to illustrate its usage.
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/zw69pz9csjo2ov9gu0yy.jpg)
<center><font size="1"> Photo by [Markus Spiske](https://unsplash.com/@markusspiske) on [Unsplash](https://unsplash.com)</font></center>

# Goal
Deploy a Web Server on Google Compute Engine (GCE) using Terraform.

## What we will explore?
* Deploying a Google Compute VM Instance using Terraform.
* Use of Compute Instance `startup` script.
* Rendering a template in terraform.

# Prerequisites
This post assumes the following:  
1. We already have a GCP Project with a network. By default, every GCP Project comes with a `default` network.  
2. Google Cloud SDK (`gcloud`) and `Terraform` is setup on your workstation. If you don't have, then refer to my previous blogs - [Getting started with Terraform](https://pbhadani.com/posts/getting-started-with-terraform/) and [Getting started with Google Cloud SDK](https://pbhadani.com/posts/getting-started-wth-google-cloud-sdk/).

# Create a Compute VM Instance

**Step 1:** Create a unix directory for the Terraform project.
  ```bash
  mkdir ~/terraform-webserver
  cd ~/terraform-webserver
  ```

**Step 2:** Define Terraform Google Provider.
  ```bash
    vi provider.tf
  ```
    This file has the following content
  ```hcl
    # Specify the GCP Provider
    provider "google" {
      project = var.project_id
      region  = var.region
    }
  ```

**Step 3:** Write below terraform code to create a Google Compute VM Instance.
  ```bash
    vi vm.tf
  ```
    To use the latest `debian` disk, we can use the data source
  ```hcl
    data "google_compute_image" "debian" {
      family  = "ubuntu-1804-lts"
      project = "gce-uefi-images"
    }
  ```

  ```hcl
    # Creates a GCP VM Instance.
    resource "google_compute_instance" "vm" {
      name         = var.name
      machine_type = var.machine_type
      zone         = var.zone
      tags         = ["http-server"]
      labels       = var.labels

      boot_disk {
        initialize_params {
          image = data.google_compute_image.debian.self_link
        }
      }

      network_interface {
        network = "default"
        access_config {
          // Ephemeral IP
        }
      }

      metadata_startup_script = data.template_file.nginx.rendered
    }
  ```
   **Note:** To allow `HTTP` connection to VM instance, we put `http-server` tag on the VM as `tags = ["http-server"]`.

**Step 4:** Now, let's define a template file which has script to install `Nginx` server and create a simple webpage `index.html`
  ```bash
    mkdir template
    vi template/install_nginx.tpl
  ```
  ```bash
    #!/bin/bash
    set -e
    echo "*****    Installing Nginx    *****"
    apt update
    apt install -y nginx
    ufw allow '${ufw_allow_nginx}'
    systemctl enable nginx
    systemctl restart nginx

    echo "*****   Installation Complteted!!   *****"

    echo "Welcome to Google Compute VM Instance deployed using Terraform!!!" > /var/www/html

    echo "*****   Startup script completes!!    *****"
  ```
  **Note:** We pass the value of `'${ufw_allow_nginx}'` from terraform code during template rendering.

**Step 5:** Let's, render the above template.
  ```bash
    vi vm.tf
  ```
  Append the following code.
  ```hcl
    data "template_file" "nginx" {
      template = "${file("${path.module}/template/install_nginx.tpl")}"

      vars = {
        ufw_allow_nginx = "Nginx HTTP"
      }
    }
  ```

**Step 6:** Once the instance comes up, we want to know its public IP so that we can browse the webpage. To do this, we can use terraform outputs.
 ```bash
  vi outputs.tf
 ```
 ```hcl
  output "webserver_ip" {
    value = google_compute_instance.vm.network_interface.0.access_config.0.nat_ip
  }
 ```

**Step 7:** Now, define all the variables in a file.
  ```bash
    vi variables.tf
  ```
  ```hcl
    variable "project_id" {
      description = "Google Cloud Platform (GCP) Project ID."
      type        = string
    }

    variable "region" {
      description = "GCP region name."
      type        = string
      default     = "europe-west1"
    }

    variable "zone" {
      description = "GCP zone name."
      type        = string
      default     = "europe-west1-b"
    }

    variable "name" {
      description = "Web server name."
      type        = string
      default     = "my-webserver"
    }

    variable "machine_type" {
      description = "GCP VM instance machine type."
      type        = string
      default     = "f1-micro"
    }

    variable "labels" {
      description = "List of labels to attach to the VM instance."
      type        = map
    }
  ```

**Step 8:** Define require variables value in `tfvars` file.
  ```bash
    vi terraform.tfvars
  ```
  ```hcl
      project_id = "gcp-project-id"
      labels     = {
        "environment" = "test"
        "team"        = "devops"
        "application" = "webserver"
      }
  ```


**Step 9:** We now have all the required terraform configuration. So, let's initialize the terraform project.
  ```bash
  terraform init
  ```
  **Output**
  ```bash
  Initializing the backend...

  Initializing provider plugins...
  - Checking for available provider plugins...
  - Downloading plugin for provider "google" (hashicorp/google) 3.4.0...
  - Downloading plugin for provider "template" (hashicorp/template) 2.1.2...

  The following providers do not have any version constraints in configuration,
  so the latest version was installed.

  To prevent automatic upgrades to new major versions that may contain breaking
  changes, it is recommended to add version = "..." constraints to the
  corresponding provider blocks in configuration, with the constraint strings
  suggested below.

  * provider.google: version = "~> 3.4"
  * provider.template: version = "~> 2.1"

  Terraform has been successfully initialized!

  You may now begin working with Terraform. Try running "terraform plan" to see
  any changes that are required for your infrastructure. All Terraform commands
  should now work.

  If you ever set or change modules or backend configuration for Terraform,
  rerun this command to reinitialize your working directory. If you forget, other
  commands will detect it and remind you to do so if necessary.
  ```

**Step 10:** After successful initialization, run plan and save plan in a file.
  ```bash
  terraform plan --out 1.plan
  ```
  **Output**
  ```bash
  Refreshing Terraform state in-memory prior to plan...
  The refreshed state will be used to calculate this plan, but will not be
  persisted to local or remote state storage.

  data.template_file.nginx: Refreshing state...
  data.google_compute_image.debian: Refreshing state...

  ------------------------------------------------------------------------

  An execution plan has been generated and is shown below.
  Resource actions are indicated with the following symbols:
    + create

  Terraform will perform the following actions:

    # google_compute_instance.vm will be created
    + resource "google_compute_instance" "vm" {
        + can_ip_forward          = false
        + cpu_platform            = (known after apply)
        + deletion_protection     = false
        + guest_accelerator       = (known after apply)
        + id                      = (known after apply)
        + instance_id             = (known after apply)
        + label_fingerprint       = (known after apply)
        + labels                  = {
            + "application" = "webserver"
            + "environment" = "test"
            + "team"        = "devops"
          }
        + machine_type            = "f1-micro"
        + metadata_fingerprint    = (known after apply)
        + metadata_startup_script = "#!/bin/bash\nset -e\necho \"*****    Installing Nginx    *****\"\napt update\napt install -y nginx\nufw allow 'Nginx HTTP'\nsystemctl enable nginx\nsystemctl restart nginx\n \necho \"*****   Installation Complteted!!   *****\"\n \necho \"Welcome to Google Compute VM Instance deployed using Terraform!!!\" > /var/www/html/index.html\n \necho \"*****   Startup script completes!!    *****\"\n"
        + min_cpu_platform        = (known after apply)
        + name                    = "my-webserver"
        + project                 = (known after apply)
        + self_link               = (known after apply)
        + tags                    = [
            + "http-server",
          ]
        + tags_fingerprint        = (known after apply)
        + zone                    = "europe-west1-b"

        + boot_disk {
            + auto_delete                = true
            + device_name                = (known after apply)
            + disk_encryption_key_sha256 = (known after apply)
            + kms_key_self_link          = (known after apply)
            + mode                       = "READ_WRITE"
            + source                     = (known after apply)

            + initialize_params {
                + image  = "https://www.googleapis.com/compute/v1/projects/gce-uefi-images/global/images/ubuntu-1804-bionic-v20191113"
                + labels = (known after apply)
                + size   = (known after apply)
                + type   = (known after apply)
              }
          }

        + network_interface {
            + name               = (known after apply)
            + network            = "default"
            + network_ip         = (known after apply)
            + subnetwork         = (known after apply)
            + subnetwork_project = (known after apply)

            + access_config {
                + nat_ip       = (known after apply)
                + network_tier = (known after apply)
              }
          }

        + scheduling {
            + automatic_restart   = (known after apply)
            + on_host_maintenance = (known after apply)
            + preemptible         = (known after apply)

            + node_affinities {
                + key      = (known after apply)
                + operator = (known after apply)
                + values   = (known after apply)
              }
          }
      }

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

  ------------------------------------------------------------------------

  This plan was saved to: 1.plan

  To perform exactly these actions, run the following command to apply:
      terraform apply "1.plan"
  ```

**Step 11:** Plan shows to create a VM instance and use `install_nginx.tpl` as startup script. So, let's go ahead and apply the plan.
  ```bash
  terraform apply 1.plan
  ```
  **Output**
  ```bash
  google_compute_instance.vm: Creating...
  google_compute_instance.vm: Still creating... [10s elapsed]
  google_compute_instance.vm: Creation complete after 15s [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver]

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

  The state of your infrastructure has been saved to the path
  below. This state is required to modify and destroy your
  infrastructure, so keep it safe. To inspect the complete state
  use the `terraform show` command.

  State path: terraform.tfstate

  Outputs:

  webserver_ip = 35.240.104.9
  ```

**Step 12:** Now if you navigate to [Google Console](https://console.cloud.google.com) and navigate to `Compute Engine --> VM Instance`, you will see an instance coming up. Once the instance is up successfully, browse the `webserver_ip`. In this case, go to `http://35.240.104.9`
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/96hrqiz5jdqrdnltz95x.png)


**Step 13:** For cleanup, run terraform destroy.
 ```bash
  terraform destroy
 ```
  **Output**
 ```bash
  data.template_file.nginx: Refreshing state...
  data.google_compute_image.debian: Refreshing state...
  google_compute_instance.vm: Refreshing state... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver]

  An execution plan has been generated and is shown below.
  Resource actions are indicated with the following symbols:
    - destroy

  Terraform will perform the following actions:

    # google_compute_instance.vm will be destroyed
    - resource "google_compute_instance" "vm" {
        - can_ip_forward          = false -> null
        - cpu_platform            = "Intel Haswell" -> null
        - deletion_protection     = false -> null
        - enable_display          = false -> null
        - guest_accelerator       = [] -> null
        - id                      = "projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver" -> null
        - instance_id             = "3519528545052665512" -> null
        - label_fingerprint       = "k3pYoTAUZq4=" -> null
        - labels                  = {
            - "application" = "webserver"
            - "environment" = "test"
            - "team"        = "devops"
          } -> null
        - machine_type            = "f1-micro" -> null
        - metadata                = {} -> null
        - metadata_fingerprint    = "mE2Cwt2znPk=" -> null
        - metadata_startup_script = "#!/bin/bash\nset -e\necho \"*****    Installing Nginx    *****\"\napt update\napt install -y nginx\nufw allow 'Nginx HTTP'\nsystemctl enable nginx\nsystemctl restart nginx\n\necho \"*****   Installation Complteted!!   *****\"\n\necho \"Welcome to Google Compute VM Instance deployed using Terraform!!!\" > /var/www/html/index.html\n\necho \"*****   Startup script completes!!    *****\"\n" -> null
        - name                    = "my-webserver" -> null
        - project                 = "workshop-demo-34293" -> null
        - self_link               = "https://www.googleapis.com/compute/v1/projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver" -> null
        - tags                    = [
            - "http-server",
          ] -> null
        - tags_fingerprint        = "FYLDgkTKlA4=" -> null
        - zone                    = "europe-west1-b" -> null

        - boot_disk {
            - auto_delete = true -> null
            - device_name = "persistent-disk-0" -> null
            - mode        = "READ_WRITE" -> null
            - source      = "https://www.googleapis.com/compute/v1/projects/workshop-demo-34293/zones/europe-west1-b/disks/my-webserver" -> null

            - initialize_params {
                - image  = "https://www.googleapis.com/compute/v1/projects/gce-uefi-images/global/images/ubuntu-1804-bionic-v20191113" -> null
                - labels = {} -> null
                - size   = 10 -> null
                - type   = "pd-standard" -> null
              }
          }

        - network_interface {
            - name               = "nic0" -> null
            - network            = "https://www.googleapis.com/compute/v1/projects/workshop-demo-34293/global/networks/default" -> null
            - network_ip         = "10.132.0.13" -> null
            - subnetwork         = "https://www.googleapis.com/compute/v1/projects/workshop-demo-34293/regions/europe-west1/subnetworks/default" -> null
            - subnetwork_project = "workshop-demo-34293" -> null

            - access_config {
                - nat_ip       = "35.240.104.9" -> null
                - network_tier = "PREMIUM" -> null
              }
          }

        - scheduling {
            - automatic_restart   = true -> null
            - on_host_maintenance = "MIGRATE" -> null
            - preemptible         = false -> null
          }

        - shielded_instance_config {
            - enable_integrity_monitoring = true -> null
            - enable_secure_boot          = false -> null
            - enable_vtpm                 = true -> null
          }
      }

  Plan: 0 to add, 0 to change, 1 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

  google_compute_instance.vm: Destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver]
  google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver, 10s elapsed]
  google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver, 20s elapsed]

  google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-webserver, 2m30s elapsed]
  google_compute_instance.vm: Destruction complete after 2m36s

  Destroy complete! Resources: 1 destroyed.
 ```

*Hope this blog gives you familiarity with `google_compute_instance` and Terraform template rendering.*

Complete source code can be found [here](https://github.com/pradeepbhadani/terraform-webserver).

If you have feedback or questions, please reach out to me on [LinkedIn](https://linkedin.com/in/pradeepbhadani) or [Twitter](https://twitter.com/bhadanipradeep)