Deploy Web Server on Google Compute Engine (GCE) with Terraform

Updated on August 10, 2022

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.

The source code used in this blog is available here.



Photo

Photo by Markus Spiske on Unsplash

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 and Getting started with Google Cloud SDK.

Create a Compute VM Instance

  1. Create a unix directory for the Terraform project.

    mkdir ~/terraform-webserver
    cd ~/terraform-webserver
  2. Define Terraform Google Provider.

    vi provider.tf

    This file has following content

    # Specify the GCP Provider
    provider "google" {
      project = var.project_id
      region  = var.region
    }
  3. Write below terraform code to create a Google Compute VM Instance.

    vi vm.tf

    To use the latest debian disk, we can use the data source

    data "google_compute_image" "ubuntu" {
      family  = "ubuntu-2204-lts"
      project = "ubuntu-os-cloud"
    }
    # Creates a GCP VM Instance.  Metadata Startup script install the Nginx webserver.
    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.ubuntu.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"].

  4. Now, lets define a template file which has script to install Nginx server and create a simple webpage index.html

    mkdir template
    vi template/install_nginx.tpl
    #!/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.

  5. Let’s, render the above template.

    vi vm.tf

    Append the following code.

    data "template_file" "nginx" {
      template = "${file("${path.module}/template/install_nginx.tpl")}"
    
      vars = {
        ufw_allow_nginx = "Nginx HTTP"
      }
    }
  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.

    vi outputs.tf
    output "webserver_ip" {
    value = google_compute_instance.vm.network_interface.0.access_config.0.nat_ip
    }
  7. Now, define all the variables in a file.

    vi variables.tf
    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-nginx-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
    }
  8. Define require variables value in tfvars file.

    vi terraform.tfvars
      project_id = "gcp-project-id"
      labels     = {
        "environment" = "dev"
        "team"        = "devops"
        "application" = "webserver"
      }
  9. We now have all the required terraform configuration. So, let’s initialize the terraform project.

    terraform init

    Output

    Initializing the backend...
    
    Initializing provider plugins...
    - Finding latest version of hashicorp/google...
    - Finding latest version of hashicorp/template...
    - Installing hashicorp/google v4.31.0...
    - Installed hashicorp/google v4.31.0 (signed by HashiCorp)
    - Installing hashicorp/template v2.2.0...
    - Installed hashicorp/template v2.2.0 (signed by HashiCorp)
    
    Terraform has created a lock file .terraform.lock.hcl to record the provider
    selections it made above. Include this file in your version control repository
    so that Terraform can guarantee to make the same selections by default when
    you run "terraform init" in the future.
    
    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.
  10. After successful initialization, run plan and save plan in a file.

    terraform plan --out 1.plan

    Output

    Terraform used the selected providers to generate the following execution plan. 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)
        + current_status          = (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" = "dev"
            + "team"        = "devops"
          }
        + machine_type            = "f1-micro"
        + metadata_fingerprint    = (known after apply)
        + metadata_startup_script = <<-EOT
              #!/bin/bash
              set -e
              echo "*****    Installing Nginx    *****"
              apt update
              apt install -y nginx
              ufw allow 'Nginx HTTP'
              systemctl enable nginx
              systemctl restart nginx
                  
              echo "*****   Installation Complteted!!   *****"
                  
              echo "Welcome to Google Compute VM Instance deployed using Terraform!!!" > /var/www/html/index.html
                  
              echo "*****   Startup script completes!!    *****"
          EOT
        + min_cpu_platform        = (known after apply)
        + name                    = "my-nginx-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/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20220712a"
                + labels = (known after apply)
                + size   = (known after apply)
                + type   = (known after apply)
              }
          }
    
        + confidential_instance_config {
            + enable_confidential_compute = (known after apply)
          }
    
        + network_interface {
            + ipv6_access_type   = (known after apply)
            + name               = (known after apply)
            + network            = "default"
            + network_ip         = (known after apply)
            + stack_type         = (known after apply)
            + subnetwork         = (known after apply)
            + subnetwork_project = (known after apply)
    
            + access_config {
                + nat_ip       = (known after apply)
                + network_tier = (known after apply)
              }
          }
    
        + reservation_affinity {
            + type = (known after apply)
    
            + specific_reservation {
                + key    = (known after apply)
                + values = (known after apply)
              }
          }
    
        + scheduling {
            + automatic_restart           = (known after apply)
            + instance_termination_action = (known after apply)
            + min_node_cpus               = (known after apply)
            + on_host_maintenance         = (known after apply)
            + preemptible                 = (known after apply)
            + provisioning_model          = (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.
    
    Changes to Outputs:
    + webserver_ip = (known after apply)
    
    ───────────────────────────────────────────────────────────────────────────────────────────────────────
    
    Saved the plan to: 1.plan
    
    To perform exactly these actions, run the following command to apply:
      terraform apply "1.plan"
  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.

    terraform apply 1.plan

    Output

    google_compute_instance.vm: Creating...
    google_compute_instance.vm: Still creating... [10s elapsed]
    google_compute_instance.vm: Creation complete after 12s [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    webserver_ip = "34.79.25.220"
  12. Now if you navigate to Google Console 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://34.79.25.220 Photo

  13. For cleanup, run terraform destroy.

    terraform destroy

    Output

    google_compute_instance.vm: Refreshing state... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver]
    
    
    Terraform used the selected providers to generate the following execution plan. 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
        - current_status          = "RUNNING" -> null
        - deletion_protection     = false -> null
        - enable_display          = false -> null
        - guest_accelerator       = [] -> null
        - id                      = "projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver" -> null
        - instance_id             = "6795915107669774507" -> null
        - label_fingerprint       = "ko4NbUxfxog=" -> null
        - labels                  = {
            - "application" = "webserver"
            - "environment" = "dev"
            - "team"        = "devops"
          } -> null
        - machine_type            = "f1-micro" -> null
        - metadata                = {} -> null
        - metadata_fingerprint    = "mE2Cwt2znPk=" -> null
        - metadata_startup_script = <<-EOT
              #!/bin/bash
              set -e
              echo "*****    Installing Nginx    *****"
              apt update
              apt install -y nginx
              ufw allow 'Nginx HTTP'
              systemctl enable nginx
              systemctl restart nginx
                  
              echo "*****   Installation Complteted!!   *****"
                  
              echo "Welcome to Google Compute VM Instance deployed using Terraform!!!" > /var/www/html/index.html
                  
              echo "*****   Startup script completes!!    *****"
          EOT -> null
        - name                    = "my-nginx-webserver" -> null
        - project                 = "workshop-demo-34293" -> null
        - resource_policies       = [] -> null
        - self_link               = "https://www.googleapis.com/compute/v1/projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-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-nginx-webserver" -> null
    
            - initialize_params {
                - image  = "https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20220712a" -> 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.33" -> null
            - queue_count        = 0 -> null
            - stack_type         = "IPV4_ONLY" -> 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       = "34.79.25.220" -> null
                - network_tier = "PREMIUM" -> null
              }
          }
    
        - scheduling {
            - automatic_restart   = true -> null
            - min_node_cpus       = 0 -> null
            - on_host_maintenance = "MIGRATE" -> null
            - preemptible         = false -> null
            - provisioning_model  = "STANDARD" -> 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.
    
    Changes to Outputs:
    - webserver_ip = "34.79.25.220" -> null
    
    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-nginx-webserver]
    google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver, 10s elapsed]
    google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver, 20s elapsed]
    google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver, 30s elapsed]
    google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver, 40s elapsed]
    google_compute_instance.vm: Still destroying... [id=projects/workshop-demo-34293/zones/europe-west1-b/instances/my-nginx-webserver, 50s elapsed]
    google_compute_instance.vm: Destruction complete after 51s
    
    Destroy complete! Resources: 1 destroyed.



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

If you have feedback or questions, please reach out to me on LinkedIn or Twitter