Get started with Terraform and a simple application

November 08, 2017 - by Alexandra White

The three-word roundup that everyone (including Hashicorp) uses to describe Terraform is: infrastructure as code. If that's too succinct without being informative, let's add a word and modify it: managing infrastructure with code. More on what that means in a moment.

I’ll be using Terraform to deploy an application that I'll dub the Cat Randomizer, an HTML+JS application by Bryce Osterhaus. If you like cat GIFs and random meows, you're in luck. Otherwise, you may want to keep your sound off once you've launched your application.

I'll be skipping the instructions for how to create an image for this application, as I've already covered that in a post on how to create custom infrastructure images with Packer. Instead, this post will focus exclusively on adding infrastructure.

Watch our on-demand webinar to see this process in action.

What is Terraform and why do you care?

Terraform

Hashicorp's Terraform is a tool designed for creating, managing, updating, and versioning reproducible application infrastructure. Application infrastructure is composed of all physical and virtual resources (including compute resources and upstack services) which support the flow, storage, processing, and analysis of data.

We've recently updated our Triton Terraform Provider, integrating the ability to use Triton CNS, query Triton images, and add network APIs. We already have a long history of Terraform and Triton integration, but it works well with other providers, too.

With simple configuration files, Terraform is told what images are needed to create what types of instances to run an application on a specific Triton datacenter. There's even a method of planning your use of Terraform before applying it, so you know what's going to happen before it happens. As you update those configuration files, Terraform sees what changes are made and incrementally executes those changes as requested.

There's a full glossary for Terraform, but here's some of the basics you should know before we get started:

Provider: the underlying platforms which support Terraform. Providers are responsible for managing the lifecycle of a resource: create, read, update, delete. Triton is a Terraform provider. Other providers include AWS, Microsoft Azure, Heroku, and Terraform Enterprise.

Resources: resource blocks define components of your infrastructure. This could be a VM or container on Triton, or it could be an email provider, DNS record, or database provider.

Data sources: data sources allow data to be fetched or computed for use within Terraform configuration, allowing Terraform to build infrastructure based on information from outside of Terraform (or from a separate Terraform configuration file). Providers are responsible for defining and implementing data sources, which present read-only views of pre-existing data or compute new values on the fly.

Plan: the plan is the first of two steps required for Terraform to make changes to infrastructure. Using terraform plan determines what changes need to be made and outputs what will be done before it's done.

Apply: the second of two steps required to make changes to the infrastructure. With terraform apply, Terraform communicates with external APIs (i.e. the providers) to make changes.

State: the Terraform state is the state of your infrastructure stored from the last time Terraform was run or applied. By default, this is stored in a local file named terraform.tfstate.

With Terraform, we're going to plan and apply an infrastructure plan to launch our web application.

Step 1: install Terraform

To install Terraform, download the appropriate package for your operating system. All systems have available packages on the Terraform downloads page.

Note: Be sure to download version 0.10+. I used Terraform 0.10.5 in this example.

Once you've downloaded Terraform, unzip the package. Terraform will run as a binary named terraform. The final step is to ensure the binary is available on the PATH.

Set PATH on macOS or Linux

Open your terminal and run the following command:

export PATH=$PATH:/path/to/dir

You can also symlink to terraform:

cd /usr/bin
sudo ln -s </path/to/dir> terraform

Set PATH on Windows

Go to: Control Panel --> System --> Advanced System settings* --> Environment Variables.

Scroll down in system variables until you find PATH. Click edit and change accordingly. You will need to launch a new console for the settings to take effect.

Verify your installation

After Terraform has been installed and PATH has been set, verify the installation by opening a new terminal session. Execute terraform and you should see a help output similar to this:

$ terraform
Usage: terraform [--version] [--help] <command> [args]

The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.

Common commands:
    apply              Builds or changes infrastructure
    console            Interactive console for Terraform interpolations
# ...

If you receive an error that terraform could not be found, PATH was not properly set up. Please go back and ensure your PATH variable has the correct directory where Terraform was installed.

Step 2: add the configuration files

If you haven't already, sign up for a Triton account.

Terraform configuration is the set of files which describes how to build and manage the various components of an application and the resources required for it to run (i.e. infrastructure as code). Our configuration files will launch a single container on Triton, requiring the Triton provider. We'll be creating one configuration file, though it is easy to split into two files for environment variables and provider setup.

Within the terminal, cd into the directory of your Cat Randomizer application.

Create your new configuration file by executing touch provider.tf.

Define the providers

First, we'll add the Triton provider to our infrastructure. We'll be creating our infrastructure within Triton's us-sw-1 datacenter.

provider "triton" {
  account = "<username>"
  key_id = "<ssh fingerprint>"
  url = "https://us-sw-1.api.joyent.com"
}

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.

Define data sources

Our Triton provider is responsible for implementing data sources, presenting read-only views of data. In particular, we need to define three pieces of data that already exist in our Triton data center: the infrastructure image for our container, the public network, and private network. On Triton, we call these networks fabrics, and every customer automatically has access to a public network named "Joyent-SDC-Public" and a private network, "Joyent-SDC-Private."

While there are a number of possible network configuration, we'll focus on what has already been allocated.

data "triton_image" "cats" {
    name = "cats_randomizer"
    version = "1.0.0"
}

data "triton_network" "private" {
    name = "Joyent-SDC-Private"
}

data "triton_network" "public" {
    name = "Joyent-SDC-Public"
}

Define the resource

Next, we'll need to create a resource for our container. Reminder, a resource is a component of your infrastructure. In this case, we're defining a container to be provisioned.

resource "triton_machine" "cats" {
    name = "cats-terraform-infra"
    package = "g4-highcpu-128M"
    image   = "${data.triton_image.cats.id}"
    networks = ["${data.triton_network.public.id}", "${data.triton_network.private.id}"]
    tags {
    cns = "cats-terraform"
  }
}

Let's break down this block further:

  • The name of our container will be cats-terraform-infra
  • The package assigned to our container is g4-highcpu-128M
  • The image uses the ID of the previously defined data source, cats
  • The networks include the IDs for both data sources for private and public
  • We've added tags for Triton CNS, so our DNS names will start with cats-terraform

The full configuration file

All together, your provider.tf file should look like this:

provider "triton" {
  account = "${var.triton_account}"
  key_id = "${var.triton_key_id}"
  url = "${var.triton_url}"
}

data "triton_image" "cats" {
    name = "cats_randomizer"
    version = "1.0.0"
}

data "triton_network" "private" {
    name = "Joyent-SDC-Private"
}

data "triton_network" "public" {
    name = "Joyent-SDC-Public"
}

resource "triton_machine" "cats" {
    name = "cats-terraform-infra"
    package = "g4-highcpu-128M"
    image   = "${data.triton_image.cats.id}"
    networks = ["${data.triton_network.public.id}", "${data.triton_network.private.id}"]
    tags {
    cns = "cats-terraform"
  }
}

Initialize

Once your configuration file has been saved, you must download the providers. This step is critical to determining how Terraform will be handled going forward.

Execute terraform init to download the Triton provider in the background into the local application directory.

$ terraform init
Initializing provider plugins...
- downloading plugin for provider "triton"...

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.triton: version = "~> 0.3"

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

The output informs us that version 0.3 of Triton has been installed. If you require a different version of a provider, you can specify it within the configuration file.

Once again, it's essential that you're using Terraform version 0.10.x to execute the following commands.

Step 3: planning the infrastructure

You're ready to plan your infrastructure. Get into the Triton environment.

eval "$(triton env)"

Run terraform plan to review what Terraform will be building based on your configuration file. The result should look similar the following:

$ terraform 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.cats: Refreshing state...
data.triton_network.private: Refreshing state...
data.triton_network.public: Refreshing state...
triton_machine.cats: Refreshing state... (ID: 5472d295-e6e3-e403-cda9-a3b06b69b1eb)

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.cats
      id:                   <computed>
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "fc54ddf8-d0f4-478d-a2af-5a03776453eb"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "cats-terraform-infra"
      networks.#:           "1"
      networks.0:           "31428241-4878-47d6-9fba-9a8436b596a4"
      nic.#:                <computed>
      package:              "g4-highcpu-128M"
      primaryip:            <computed>
      root_authorized_keys: <computed>
      tags.%:               "1"
      tags.cns:             "cats-terraform"
      type:                 <computed>
      updated:              <computed>

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

If there have been any errors, you may have to go back and modify the configuration file before proceeding.

Step 4: applying the infrasturcture

Once you know what Terraform will do, you can use terraform apply to make it happen. This will build our new infrastructure container on us-sw-1. Enough talk, let's make it happen.

terraform apply
data.triton_image.cats: Refreshing state...
data.triton_network.public: Refreshing state...
data.triton_network.private: Refreshing state...
triton_machine.cats: Refreshing state... (ID: 5472d295-e6e3-e403-cda9-a3b06b69b1eb)
triton_machine.cats: Creating...
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "fc54ddf8-d0f4-478d-a2af-5a03776453eb"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "cats-terraform-infra"
  networks.#:           "" => "1"
  networks.0:           "" => "31428241-4878-47d6-9fba-9a8436b596a4"
  nic.#:                "" => "<computed>"
  package:              "" => "g4-highcpu-128M"
  primaryip:            "" => "<computed>"
  root_authorized_keys: "" => "<computed>"
  tags.%:               "" => "1"
  tags.cns:             "" => "cats-terraform"
  type:                 "" => "<computed>"
  updated:              "" => "<computed>"
triton_machine.cats: Still creating... (10s elapsed)
triton_machine.cats: Still creating... (20s elapsed)
triton_machine.cats: Still creating... (30s elapsed)
triton_machine.cats: Creation complete after 1m9s (ID: 8ba50cde-7c65-48c7-a1a8-f8799eb8f36e)

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

The results will reiterate the same information from the plan. The container may take a while to actually be created (the first time I ran it, it took upwards of three minutes), so be patient.

Step 5: visiting the application on Triton

Congrats! You have a running container of the Cat Randomizer web application.

Cat fist bump

You can see the instance in your list by running triton instances:

$ triton instances
SHORTID   NAME                       IMG                    STATE    FLAGS  AGE
8ba50cde  cats-terraform-infra       cats_randomizer@1.0.0  running  -      4m

Let's view our application on the web. Get the DNS name with triton inst get cats-terraform-01:

$ triton inst get cats-terraform-infra
{
    "id": "8ba50cde-7c65-48c7-a1a8-f8799eb8f36e",
    "name": "cats-terraform-infra",
    "type": "smartmachine",
    "brand": "lx",
    "state": "running",
    "image": "fc54ddf8-d0f4-478d-a2af-5a03776453eb",
    "ips": [
        "165.225.173.147",
        "10.121.74.52"
    ],
    [...]
    "package": "g4-highcpu-128M",
    "dns_names": [
        "8ba50cde-7c65-48c7-a1a8-f8799eb8f36e.inst.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-sw-1.triton.zone",
        "cats-terraform-infra.inst.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-sw-1.triton.zone",
        "8ba50cde-7c65-48c7-a1a8-f8799eb8f36e.inst.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-sw-1.cns.joyent.com",
        "cats-terraform-infra.inst.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-sw-1.cns.joyent.com"
    ]
}

Open your browser and visit your application with the Triton CNS-powered DNS name. Because I've enabled Triton CNS, I was able to connect a vanity domain. You can visit my live application at cats.alexandra.space.

Conclusion

Terraform is an incredibly powerful tool to manage your infrastructure. With one short configuration file and two even shorter commands, we were able to spin up a container on Triton to see randomized cat GIFs.

Now that you know the basics of how to spin up an application with Terraform, add more resources and more containers to spin up something more complex.

If you want a visual overview of this process, watch our on-demand webinar to learn how to build application infrastructure with Terraform.