Webinar: Build a custom application with Packer and Terraform

June 28, 2018 - by Alexandra White

It's quick to deploy your applications on Triton, with the help of Hashicorp's Packer and Terraform. In this demo, we'll be working with 2048, an incredibly popular game for smartphones. Now, you can deploy it to the web to play any time, in any browser.

Sign up to watch our webinar to get an in-depth, step-by-step overview of this process. Or, continue reading to see all of the code used for this project.

Creating the image with Packer

It's simple to automate and create a new machine image with a single Packer configuration file. With the help of scripts, install and configure your software on one of Triton's images. Triton provides numerous hardware virtual machine (HVM) and infrastructure images, just run triton images to see what we offer. Thanks to Packer, you can quickly spin up multiple containers without needing to individually tweak each one.

For this application, we'll be using Nginx as a source image.

To better learn terminology and the prerequisites for setting up Packer, read my previous post: create custom infrastructure images with Packer

Adding the shell script

Clone or download a copy of 2048 and open the directory. Using whatever text-editor you prefer, create and edit a shell script which will modify our Nginx image. I'll call mine directories.sh.

$ cd ~/2048/
$ touch directories.sh

Our shell script is going to create the directories which match our application, for CSS, JavaScript, and other resources.

mkdir -p /usr/share/nginx/html/js
mkdir -p /usr/share/nginx/html/meta
mkdir -p /usr/share/nginx/html/style
mkdir -p /usr/share/nginx/html/style/fonts

That's it. Those four lines create the necessary directories and subdirectories. We'll talk more about why this is important in our later step on provisioning.

Adding a Packer configuration file

With the help of JSON, next we'll write a configuration file to build our application image. I'll call mine twentyfortyeight.json.

$ touch twentyfortyeight.json

Variables

Our file will begin by establishing the necessary credentials to connect to Triton. This requires setting up Triton environment variables for your account. Environment variables ensure all keys are kept secret and stored locally instead of being tied directly to the application.

{
 "variables": {
    "triton_url": "{{env `TRITON_URL`}}",
    "triton_account": "{{env `TRITON_ACCOUNT`}}",
    "triton_key_id": "{{env `TRITON_KEY_ID`}}"
  },

Builder

Our builder uses Triton and CloudAPI to create the new image. Since 2048 is a web application, Nginx will be a great source image to use along with the smallest and most affordable package g4-highcpu-128M.

You'll need to reference the image UUID in the builder, which is easy to find with triton.

$ triton image get nginx-1 | grep "id"
"id": "88cf77e4-1958-11e7-bda8-777cc817ade5"

Our custom image will be called twentyfortyeight.

   "builders": [
      {
         "name": "triton-builder",
         "type": "triton",
         "triton_url": "{{user `triton_url`}}",
         "triton_account": "{{user `triton_account`}}",
         "triton_key_id": "{{user `triton_key_id`}}",

         "ssh_username": "root",

         "source_machine_name": "nginx-1",
         "source_machine_package": "g4-highcpu-128M",
         "source_machine_image": "88cf77e4-1958-11e7-bda8-777cc817ade5",

         "image_name": "twentyfortyeight",
         "image_version": "1.0.0",
         "image_tags": {
            "Project": "Twenty-Forty-Eight"
         }
      }
  ],

NOTE: For your SSH key to be usable, it must be available through the ssh-agent.

Provisioners

Provisioners upload application files and modify the source image. We'll be using two types of provisioners: shell and file.

The shell provisioner calls to the shell script, which ensures that the Nginx image matches the setup of our local application.

The file provisioners upload all of the necessary application materials. This can be either a single file, such as index.html, or an entire directory, such as style/.

   "provisioners": [
     {
       "type": "shell",
       "script": "directories.sh"
     },
     {
       "type": "file",
       "source": "favicon.ico",
       "destination": "/usr/share/nginx/html/"
     },
     {
       "type": "file",
       "source": "index.html",
       "destination": "/usr/share/nginx/html/"
     },
     {
       "type": "file",
       "source": "style/",
       "destination": "/usr/share/nginx/html/style/"
     },
     {
       "type": "file",
       "source": "js/",
       "destination": "/usr/share/nginx/html/js/"
     },
     {
       "type": "file",
       "source": "meta/",
       "destination": "/usr/share/nginx/html/meta/"
     }
  ]
}

Once added, your Packer configuration file is complete.

Build the image

Now that your Packer configuration file is complete, you can build the custom image on Triton.

It's essential to validate the template, ensuring the JSON syntax and configuration values are correct.

Run packer validate with the name of the Packer template:

$ packer validate twentyfortyeight.json
Template validated successfully.

Once successful, it's time to build the 2048 image. Execute packer build with the name of the configuration file.

$ packer build twentyfortyeight.json

==> triton-builder: Waiting for source machine to become available...
==> triton-builder: Waiting for SSH to become available...
==> triton-builder: Connected to SSH!
==> triton-builder: Provisioning with shell script: directories.sh
==> triton-builder: Uploading favicon.ico => /usr/share/nginx/html/
==> triton-builder: Uploading index.html => /usr/share/nginx/html/
==> triton-builder: Uploading style/ => /usr/share/nginx/html/style/
==> triton-builder: Uploading js/ => /usr/share/nginx/html/js/
==> triton-builder: Uploading meta/ => /usr/share/nginx/html/meta/
==> triton-builder: Stopping source machine (fc268ac6-7e30-cbbc-dd57-d10cd340f2dd)...
==> triton-builder: Waiting for source machine to stop (fc268ac6-7e30-cbbc-dd57-d10cd340f2dd)...
==> triton-builder: Creating image from source machine...
==> triton-builder: Waiting for image to become available...
==> triton-builder: Deleting source machine...
==> triton-builder: Waiting for source machine to be deleted...
Build 'triton-builder' finished.

==> Builds finished. The artifacts of successful builds are:
--> triton-builder: Image was created: 64f8fab0-4628-414d-87e8-03ab5c0571bc

The image ID for twentyfortyeight is declared at the end of the build process, and you can now find your image in the list of all Triton images:

$ triton images | grep "twentyfortyeight"
64f8fab0  twentyfortyeight          1.0.0      I     linux    lx-dataset    2018-06-26

The twentyfortyeight image is now available for you to deploy with Terraform.

Deploy your application with Terraform

Like Packer, Terraform uses configuration files to determine what images are needed to create what types of instances to run an application on a specific Triton data center. As applications change, you can update those configuration files. Terraform sees what changes are made and incrementally executes those changes as requested.

To better learn terminology and the prerequisites for setting up Terraform, read my previous post: get started with Terraform and a simple application

Define the variables

First, we'll create a file to store any necessary variables. This separates information that is more likely to change (image version, for example) from the configuration itself which builds the infrastructure.

NOTE: Though you do not need to have the local version of the application anymore, I'll be keeping all of these files together.

$ touch variables.tf

The first three variables deal directly with the application image. The last three help configure the container including package size, service name, and networks.

NOTE: Just because we used a specific package in the Packer build, doesn't mean the same package size will be chosen with Terraform.

variable "image_name" {
  type        = "string"
  description = "The name of the image for the deployment."
  default     = "twentyfortyeight"
}

variable "image_version" {
  type        = "string"
  description = "The version of the image for the deployment."
  default     = "1.0.0"
}

variable "image_type" {
  type        = "string"
  description = "The type of the image for the deployment."
  default     = "lx-dataset"
}

variable "package_name" {
  type        = "string"
  description = "The package to use when making a deployment."
  default     = "g4-highcpu-128M"
}

variable "service_name" {
  type        = "string"
  description = "The name of the service in CNS."
  default     = "2048"
}

variable "service_networks" {
  type        = "list"
  description = "The name or ID of one or more networks the service will operate on."
  default     = ["Joyent-SDC-Public"]
}

Define the infrastructure

Once our variables are complete, we can create main.tf to define the infrastructure we'll be building.

$ touch main.tf

It is essential to use Terraform version 0.10.0 or higher, so that's the first piece of configuration to add to our file.

terraform {
  required_version = ">= 0.10.0"
}

Terraform provider

Add the Triton provider to define where the infrastructure will be built. The provider uses those same Triton environment variables to validate your credentials and establish which data center to use, therefore the provider doesn't need any arguments. I've clarified this using comments.

provider "triton" {
  # The provider takes the following environment variables:
  # TRITON_URL, TRITON_ACCOUNT, and TRITON_KEY_ID
}

The "triton" provider includes the username to login to your Triton account, the SSH fingerprint attached to your account (which you can get from the Triton portal, and a URL to a Triton datacenter.

By setting our provider, we are establishing that triton can be used to manage the lifecycle of our application.

Data sources

Data sources present read-only views of data which already exist within the Triton data center. This includes the image details and fabrics (i.e. networks).

These data sources make use of the variables we've already established for image name, version, and type, as well as service networks.

#
# Details about the deployment
#
data "triton_image" "game_image" {
  name        = "${var.image_name}"
  version     = "${var.image_version}"
  type        = "${var.image_type}"
  most_recent = true
}

data "triton_network" "service_networks" {
  count = "${length(var.service_networks)}"
  name  = "${element(var.service_networks, count.index)}"
}

Resources

Resources are the components being created by Terraform which compose our application infrastructure. In this case, we only need one resource, a container to be provisioned running 2048.

Resources make use of information from data sources as well as from variables.

# The container name cannot be identical to the image name
resource "triton_machine" "game_machine" {
  name     = "twenty_forty_eight"
  package  = "${var.package_name}"
  image    = "${data.triton_image.game_image.id}"
  networks = ["${data.triton_network.service_networks.*.id}"]

  cns {
    services = ["${var.service_name}"]
  }
}

Outputs

Finally, outputs give an opportunity to quickly get the IP address and DNS names associated with the new container. This way, we can quickly access our new application and start playing!

output "primaryIp" {
  value = ["${triton_machine.game_machine.*.primaryip}"]
}

output "dns_names" {
  value = ["${triton_machine.game_machine.*.domain_names}"]
}

Download the providers

In order to use Terraform, you must download the providers to the directory of your application. Execute terraform init to download the Triton provider in the background.

$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "triton" (0.5.1)...

[...]

* provider.triton: version = "~> 0.5"

Terraform has been successfully initialized!

You may now begin working with Terraform. [...]

We're working with the latest version of the Triton provider, 0.5. If you must use a different version of a provider, specify that version within the configuration file.

Plan your application

Like with any major application deployment, it's good to know what will be built before it happens. Running terraform plan will review what Terraform will implement. You should have a similar result:

   $ terraform plan -out game.plan
   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.triton_image.game_image: Refreshing state...
   data.triton_network.service_networks: 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:

     + triton_machine.game_machine
         id:                          <computed>
         cns.#:                       "1"
         cns.0.services.#:            "1"
         cns.0.services.0:            "2048"
         compute_node:                <computed>
         created:                     <computed>
         dataset:                     <computed>
         deletion_protection_enabled: "false"
         disk:                        <computed>
         domain_names.#:              <computed>
         firewall_enabled:            "false"
         image:                       "64f8fab0-4628-414d-87e8-03ab5c0571bc"
         ips.#:                       <computed>
         memory:                      <computed>
         name:                        "twenty_forty_eight"
         networks.#:                  "1"
         networks.0:                  "31428241-4878-47d6-9fba-9a8436b596a4"
         nic.#:                       <computed>
         package:                     "g4-highcpu-128M"
         primaryip:                   <computed>
         root_authorized_keys:        <computed>
         type:                        <computed>
         updated:                     <computed>

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

   This plan was saved to: game.plan

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

My plan informs me that one resource will be added, zero changed or destroyed. If you encounter an error, you will most likely have to modify your configuration files.

Building your application with Terraform

The plan tells you how to build exactly what it declared would happen at the end of the output. With terraform apply, we can get our game up and running.

   $ terraform apply "game.plan"
   triton_machine.game_machine: Creating...
     cns.#:                       "" => "1"
     cns.0.services.#:            "" => "1"
     cns.0.services.0:            "" => "2048"
     compute_node:                "" => "<computed>"
     created:                     "" => "<computed>"
     dataset:                     "" => "<computed>"
     deletion_protection_enabled: "" => "false"
     disk:                        "" => "<computed>"
     domain_names.#:              "" => "<computed>"
     firewall_enabled:            "" => "false"
     image:                       "" => "64f8fab0-4628-414d-87e8-03ab5c0571bc"
     ips.#:                       "" => "<computed>"
     memory:                      "" => "<computed>"
     name:                        "" => "twenty_forty_eight"
     networks.#:                  "" => "1"
     networks.0:                  "" => "31428241-4878-47d6-9fba-9a8436b596a4"
     nic.#:                       "" => "<computed>"
     package:                     "" => "g4-highcpu-128M"
     primaryip:                   "" => "<computed>"
     root_authorized_keys:        "" => "<computed>"
     type:                        "" => "<computed>"
     updated:                     "" => "<computed>"

     triton_machine.game_machine: Creation complete after 52s (ID: f4eab506-052d-e851-a411-e6cd01c9da24)

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

   Outputs:

   dns_names = [
       [
           f4eab506-052d-e851-a411-e6cd01c9da24.inst.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-east-3.triton.zone,
           twenty-forty-eight.inst.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-east-3.triton.zone
       ]
   ]
   primaryIp = [
       165.225.174.172
   ]

I'm now ready to attempt to best my high score on 2048, live on Triton.

Wrapping up

Once again, sign up to watch our webinar to see this application demo in full. It's around ten minutes long, so a small time commitment with a big learning reward.

If you want to see other Packer and Terraform examples, we recently ran a series on Packer and Hashicorp using a different web application. These posts also include short videos and links to webinars: