Video: Using Terraform for blue-green deployments on Triton

April 09, 2018 - by Alexandra White

In this post, you'll learn how to implement the blue-green deployment model using Terraform as a way to deploy with confidence by reducing risk when updating applications and services. If you haven't already, read how to create custom infrastructure images with Packer and how to get started with Terraform.

Deployment models let us deploy application infrastructure with confidence. There are a number of common models including (but not limited to) canary deployment, rolling deployment, and the blue-green deployment model. In the blue-green model, two versions of an application are maintained. One version is live while the other serves as a standby deployment, upon which testing can be done. This allows developers to easily rollback changes if there are problems in a new release. Remove the risk and organizational paralysis by executing changes quickly.

Blue-green deployments ensure you can predictably release updates without disruption to your customer experience. By using Terraform to implement an automated blue-green deployment, you can quickly update your applications as a controlled release.

Watch our screencast to see this process in action. You can also watch the webinar to get a more in-depth, step-by-step overview of this concept. Not interested in the full walkthrough? Download the Terraform files to get started on your own.

Watch to learn

Watch our video to see how quickly you can update applications with the blue-green deployment model using Terraform on Triton.

Tell me more about blue-green deployments

Blue-green deployments are most often associated with different environments for an application, with the colors as indicators for the environments. One color represents the staging environment, while the other represents production.

Why blue-green and not red-orange or purple-yellow? It's a historical term for the deployment model, but the colors are irrelevant. What matters is there are different deployments existing in parallel, and you can seamlessly migrate from one to another.

For this demo, we're going to make updates to a web application. If you weren't a big fan of all of the cats in the last post, this application will probably make you happier. We'll be deploying a happiness randomizer, designed to give random GIFs and inspirational motivation to whomever visits.

Prerequisites

Picard applauds you

Each version of our application is delivered as an immutable, reproducibly created image (in this case, using Packer) that will boot the VM or container we'll run it in. Before we can jump into how to use Terraform to do blue-green deployments, we first have to create our images. The Packer configuration file is available on GitHub and if unmodified, it will create an image named happy_randomizer version 1.0.0.

Later in the demo, we'll update the application image to version 1.1.0, for which there are instructions in the repository.

NOTE: While I think it's fun to use a custom application where you can see the difference (as well as take use of a prior blog exercise), you could just as easily run through this post with two stock Ubuntu images, ubuntu-14.04 and ubuntu-16.04. Check out all of the stock images by executing triton images.

Once happy_randomizer 1.0.0 is available on Triton, create a brand new directory for your Terraform files. The files refer to the images on Triton and do not require local application files. Here's how to create the directory via the terminal:

<code class="language-bash">mkdir randomizer-deployment</code>

NOTE: That directory can be named whatever you see fit.

Initial Terraform configuration files

In this demo, you'll be working with two Terraform configuration files. variables.tf will store, you guessed it, the variables, and main.tf which is what is responsible for the actual deployment.

Variables

There are a number of values that should be included in the variables file including image names and versions, production setting, CNS service names, networks, and instance counts.

The original Happy Randomizer, version 1.0, is configured to be our starting point and designated as our blue deployment.

Create your variables file:

<code class="language-bash">$ touch variables.tf</code>

Using the text-editor of your choice, add the following variables to variables.tf:

<code class="language-hcl">#
# Details about all deployments of this application
#
variable "service_production" {
    type = "string"
    description = "Which deployment is considered 'production'? The other is 'staging'. Value can be one of 'blue' or 'green'."
    default = "blue"
}
variable "service_name" {
    type = "string"
    description = "The name of the service in CNS."
    default = "happiness"
}
variable "service_networks" {
    type = "list"
    description = "The name or ID of one or more networks the service will operate on."
    default = ["Joyent-SDC-Public"]
}
#
# Details about the "blue" deployment
#
variable "blue_image_name" {
    type = "string"
    description = "The name of the image for the 'blue' deployment."
    default = "happy_randomizer"
}
variable "blue_image_version" {
    type = "string"
    description = "The version of the image for the 'blue' deployment."
    default = "1.0.0"
}
variable "blue_count" {
    type = "string"
    description = "The number of 'blue' instances to create."
    default = "3"
}
variable "blue_package_name" {
    type = "string"
    description = "The package to use when making a blue deployment."
    default = "g4-highcpu-128M" 
}</code>

Any default you don't fill out will be asked by the Terraform CLI. For example, if the default value for blue_count is left empty, Terraform will ask you for the number of instances you wish to create upon executing terraform apply.

Deployment file

Here's the real meat of our Terraform deployment. Our main configuration file will set up our provider, declare our data sources, and create our blue and green instances.

Terraform can use your environment variables to fill out the provider. If you haven't set those up, create variables in variables.tf for your account name, SSH fingerprint, and the data center name.

In the same directory as our variables file, create main.tf:

<code class="language-bash">$ touch main.tf</code>

Using the text-editor of your choice, add the following variables to main.tf:

<code class="language-hcl">#
# You must have installed Terraform v 0.10.0 or above.
#
terraform {
  required_version = ">= 0.10.0"
}
#
# The provider will take the SDC_URL, SDC_ACCOUNT, and SDC_KEY_ID environment vars as defaults. Uncomment data within the triton provider if no environment vars are set up.
#
provider "triton" {
    # url = "https://${var.dc_name}.api.joyent.com"
    # account = "${var.triton_account}"
    # key_id = "${var.triton_key_id}"
}
#
# Details about all deployments of this application
#
data "triton_network" "service_networks" {
  count = "${length(var.service_networks)}"
  name = "${element(var.service_networks, count.index)}"
}
#
# Details about the "blue" deployment
#
data "triton_image" "blue_image" {
    name = "${var.blue_image_name}"
    version = "${var.blue_image_version}"
    type = "lx-dataset"
    most_recent = true
}
resource "triton_machine" "blue_machine" {
    count = "${var.blue_count}"
    name = "blue_happy_${count.index + 1}"
    package = "${var.blue_package_name}"
    image = "${data.triton_image.blue_image.id}"
    networks = ["${data.triton_network.service_networks.*.id}"]
    cns {
        services = ["${var.service_production == "blue" ? var.service_name : "staging-${var.service_name}" }", "blue-${var.service_name}"]
    }
}
#
# Outputs from the "blue" deployment
#
output "blue_domains" {
  value = ["${triton_machine.blue_machine.*.domain_names}"]
}</code>

The end result of this configuration file will be creating three blue machines on my default data center, us-sw-1. When you run terraform apply, you'll be given the Triton CNS domains as a result of the blue_domains output.

To better understand the nitty gritty details, read through the configuration file setup for a simple Terraform application.

Step 1: get into the Triton environment

Our Triton provider uses environment variables, so it's important to first get into the Triton environment:

<code class="language-sh">$ eval "$(triton env)"</code>

If your default environment is not the same data center as referred to in your Terraform files, be sure to declare the correct profile.

Step 2: initializing Terraform

Now that your files have been created, you can get ready to execute your infrastructure. First, you must download the providers. This step is critical.

Execute terraform init to download the Triton provider in the background into the local application directory. You should be using Terraform version 0.10.x to execute the following commands.

Step 3: creating the blue infrastructure

Once you've initiated Terraform, you can start the process of deploying happy_randomizer version 1.0.0. That will be your blue infrastructure.

Planning the blue infrastructure

Run terraform plan to review the deployment. The result should look similar to the following:

<code class="language-bash">$ terraform plan -out blue-green.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.blue_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.blue_machine[0]
      id:                   <computed>
      cns.#:                "1"
      cns.0.services.#:     "2"
      cns.0.services.0:     "happiness"
      cns.0.services.1:     "blue-happiness"
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "b3045d03-75a4-4e3d-97e7-76b1a8de757a"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "blue_happy_1"
      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.blue_machine[1]
      id:                   <computed>
      cns.#:                "1"
      cns.0.services.#:     "2"
      cns.0.services.0:     "happiness"
      cns.0.services.1:     "blue-happiness"
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "b3045d03-75a4-4e3d-97e7-76b1a8de757a"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "blue_happy_2"
      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.blue_machine[2]
      id:                   <computed>
      cns.#:                "1"
      cns.0.services.#:     "2"
      cns.0.services.0:     "happiness"
      cns.0.services.1:     "blue-happiness"
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "b3045d03-75a4-4e3d-97e7-76b1a8de757a"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "blue_happy_3"
      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: 3 to add, 0 to change, 0 to destroy.
This plan was saved to: blue-green.plan
To perform exactly these actions, run the following command to apply:
    terraform apply "blue-green.plan"</code>

This is a preview of exactly what should happen when you execute terraform apply "blueplan".

NOTE: it is recommended and considered best practice to run terraform apply with an output plan, i.e. terraform apply -out=<plan-name>. However, it is possible to leave it off, i.e. terraform apply. One benefit of using a specific plan name is that you can save multiple plans to go back and forth if needed. Additionally, you can see if the environment drifted between the plan and apply phases.

Applying the blue infrastructure

Run terraform apply "blue-green.plan" to set your infrastructure plan into motion, deploying our containers to us-sw-1.

<code class="language-bash">$ terraform apply "blue-green.plan"
triton_machine.blue_machine[2]: Creating...
  cns.#:                "" => "1"
  cns.0.services.#:     "" => "2"
  cns.0.services.0:     "" => "happiness"
  cns.0.services.1:     "" => "blue-happiness"
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "b3045d03-75a4-4e3d-97e7-76b1a8de757a"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "blue_happy_3"
  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.blue_machine[0]: Creating...
  cns.#:                "" => "1"
  cns.0.services.#:     "" => "2"
  cns.0.services.0:     "" => "happiness"
  cns.0.services.1:     "" => "blue-happiness"
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "b3045d03-75a4-4e3d-97e7-76b1a8de757a"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "blue_happy_1"
  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.blue_machine[1]: Creating...
  cns.#:                "" => "1"
  cns.0.services.#:     "" => "2"
  cns.0.services.0:     "" => "happiness"
  cns.0.services.1:     "" => "blue-happiness"
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "b3045d03-75a4-4e3d-97e7-76b1a8de757a"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "blue_happy_2"
  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.blue_machine[1]: Creation complete after 46s (ID: 3f7e2689-07fb-45f0-bd71-953e3f90877d)
triton_machine.blue_machine[2]: Creation complete after 46s (ID: 5bf6052f-0823-e558-de5b-c5b31b404245)
triton_machine.blue_machine[0]: Creation complete after 57s (ID: 63590c9a-e9e5-60ed-d959-8cf3992c5d29)
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
blue_domains = [
   [
      "63590c9a-e9e5-60ed-d959-8cf3992c5d29.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happy-1.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "3f7e2689-07fb-45f0-bd71-953e3f90877d.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happy-2.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "5bf6052f-0823-e558-de5b-c5b31b404245.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happy-3.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ]
]</code>

Thanks to the ouput in our main.tf file, we immediately have access to all of the CNS-powered domain names for our blue instances.

The domain which starts with happiness, indicates the instances are in production. blue-happiness domains indicate the color of the deployment.

Step 4: building the green image

Now, we're ready to create version 1.1.0 of our application. Follow the instructions in the Happy Randomizer repo to create the image for happy_randomizer 1.1.0. This will be our green image.

Be sure to deploy it to your default data center (which you set in step 1), where version 1.0.0 lives.

Again, if your default environment is not the same data center as referred to in your Terraform files, be sure to declare the same profile in your new Packer image configuration.

Step 5: creating green application and infrastructure

To add green infrastructure to our Terraform deployment, first we must edit variables.tf. Using the text editor of your choice, add the following content to the file:

<code class="language-hcl">#
# Details about the "green" deployment
#
variable "green_image_name" {
    type = "string"
    description = "The name of the image for the 'green' deployment."
    default = "happy_randomizer"
}
variable "green_image_version" {
    type = "string"
    description = "The version of the image for the 'green' deployment."
    default = "1.1.0"
}
variable "green_count" {
    type = "string"
    description = "The number of 'green' instances to create."
    default = "3"
}
variable "green_package_name" {
    type = "string"
    description = "The package to use when making a green deployment."
    default = "g4-highcpu-128M" 
}</code>

This declares the green image name, version, number of instances to create, and package.

NOTE: If your application requires a certain number of instances to always be up and running, be aware that Terraform often deletes before it creates. For example, if you need a minimum of three instances, you should deploy four instances. This way, when one instance is deleted, there's still three running. Once you're satisfied the application has been updated successfully and the changes are live, you can remove the extra instance.

You also must edit main.tf to create the instances. Add the following content to the end of that file:

<code class="language-hcl">#
# Details about the "green" deployment
#
data "triton_image" "green_image" {
    name = "${var.green_image_name}"
    version = "${var.green_image_version}"
    type = "lx-dataset"
    most_recent = true
}
resource "triton_machine" "green_machine" {
    count = "${var.green_count}"
    name = "green_happy_${count.index + 1}"
    package = "${var.green_package_name}"
    image = "${data.triton_image.green_image.id}"
    networks = ["${data.triton_network.service_networks.*.id}"]
    cns {
        services = ["${var.service_production == "green" ? var.service_name : "staging-${var.service_name}" }", "green-${var.service_name}"]
    }
}
#
# Outputs from the "green" deployment
#
output "green_domains" {
  value = ["${triton_machine.green_machine.*.domain_names}"]
}</code>

This will take our new variables to create green instances.

Planning green infrastructure

Now that your files have been updated to include the green instances, run terraform plan to review the deployment. The result should look similar to the following:

<code class="language-bash">$ terraform plan -out blue-green.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.green_image: Refreshing state...
data.triton_image.blue_image: Refreshing state...
data.triton_network.service_networks: Refreshing state...
triton_machine.blue_machine[2]: Refreshing state... (ID: 5bf6052f-0823-e558-de5b-c5b31b404245)
triton_machine.blue_machine[1]: Refreshing state... (ID: 3f7e2689-07fb-45f0-bd71-953e3f90877d)
triton_machine.blue_machine[0]: Refreshing state... (ID: 63590c9a-e9e5-60ed-d959-8cf3992c5d29)
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.green_machine[0]
      id:                   <computed>
      cns.#:                "1"
      cns.0.services.#:     "2"
      cns.0.services.0:     "staging-happiness"
      cns.0.services.1:     "green-happiness"
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "bb767cc6-124b-40b1-baf7-1e23564a2e5a"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "green_happy_1"
      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.green_machine[1]
      id:                   <computed>
      cns.#:                "1"
      cns.0.services.#:     "2"
      cns.0.services.0:     "staging-happiness"
      cns.0.services.1:     "green-happiness"
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "bb767cc6-124b-40b1-baf7-1e23564a2e5a"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "green_happy_2"
      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.green_machine[2]
      id:                   <computed>
      cns.#:                "1"
      cns.0.services.#:     "2"
      cns.0.services.0:     "staging-happiness"
      cns.0.services.1:     "green-happiness"
      created:              <computed>
      dataset:              <computed>
      disk:                 <computed>
      domain_names.#:       <computed>
      firewall_enabled:     "false"
      image:                "bb767cc6-124b-40b1-baf7-1e23564a2e5a"
      ips.#:                <computed>
      memory:               <computed>
      name:                 "green_happy_3"
      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: 3 to add, 0 to change, 0 to destroy.
This plan was saved to: blue-green.plan 
To perform exactly these actions, run the following command to apply:
    terraform apply "blue-green.plan "</code>

The new plan is creating the three new green instances. The blue instances will remain untouched.

Applying to add green infrastructure

Create the green instances with terraform apply with our plan name, blue-green.plan.

<code class="language-bash">$ terraform apply "blue-green.plan"
triton_machine.green_machine[1]: Creating...
  cns.#:                "" => "1"
  cns.0.services.#:     "" => "2"
  cns.0.services.0:     "" => "staging-happiness"
  cns.0.services.1:     "" => "green-happiness"
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "bb767cc6-124b-40b1-baf7-1e23564a2e5a"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "green_happy_2"
  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.green_machine[2]: Creating...
  cns.#:                "" => "1"
  cns.0.services.#:     "" => "2"
  cns.0.services.0:     "" => "staging-happiness"
  cns.0.services.1:     "" => "green-happiness"
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "bb767cc6-124b-40b1-baf7-1e23564a2e5a"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "green_happy_3"
  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.green_machine[0]: Creating...
  cns.#:                "" => "1"
  cns.0.services.#:     "" => "2"
  cns.0.services.0:     "" => "staging-happiness"
  cns.0.services.1:     "" => "green-happiness"
  created:              "" => "<computed>"
  dataset:              "" => "<computed>"
  disk:                 "" => "<computed>"
  domain_names.#:       "" => "<computed>"
  firewall_enabled:     "" => "false"
  image:                "" => "bb767cc6-124b-40b1-baf7-1e23564a2e5a"
  ips.#:                "" => "<computed>"
  memory:               "" => "<computed>"
  name:                 "" => "green_happy_1"
  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.green_machine[1]: Creation complete after 46s (ID: 7db3429a-e20c-ca47-dd2e-cc0e84a24e5e)
triton_machine.green_machine[2]: Creation complete after 46s (ID: 6c623e78-d879-e17c-b582-f8b5bf130a11)
triton_machine.green_machine[0]: Creation complete after 58s (ID: 48abac15-a182-c904-fd42-8c85b5492c01)
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
blue_domains = [
   [
      "63590c9a-e9e5-60ed-d959-8cf3992c5d29.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happy-1.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "3f7e2689-07fb-45f0-bd71-953e3f90877d.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happy-2.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "5bf6052f-0823-e558-de5b-c5b31b404245.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happy-3.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "blue-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ]
]
green_domains = [
   [
      "48abac15-a182-c904-fd42-8c85b5492c01.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happy-1.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "7db3429a-e20c-ca47-dd2e-cc0e84a24e5e.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happy-2.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "6c623e78-d879-e17c-b582-f8b5bf130a11.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happy-3.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ]
]</code>

At this point in time, you should have three blue instances and three green instances. All of your instance domains will be listed.

Step 6: checking for errors

It's important to make sure that our newly updated Happy Randomizer is running as expected. I visited green-happy-1.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone, and I'm satisfied that the new thumbs up GIFs are in place.

To see the other information associated with your deployment, execute terraform show. This uses the .tfstate file to show all available values.

Step 7: point production DNS name to green instances

At this point, we can begin to consider updating a load balancer or vanity domain name via a CNAME update. My application was pointed to alexandra.space.

In variables.tf, the production variable has a default value set to blue. Now that we're certain the application is working as expected, update that value to be set as green:

<code class="language-hcl">variable "service_production" {
    type = "string"
    description = "Which deployment is considered 'production'? The other is 'staging'. Value can be one of 'blue' or 'green'."
    default = "green"
}</code>

After our next Terraform run, this will update CNS to remove staging- from the green instances' DNS names and add it to the blue instances' DNS names. Once applied, the DNS names for your instances will be as follows:

  • Blue instances: staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone
  • Green instances: happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone

Read more about Triton CNS and vanity URLs.

Planning the updated CNS names

Run terraform plan to check that the appropriate changes will be made.

<code class="language-sh">$ terraform plan -out blue-green.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.blue_image: Refreshing state...
data.triton_network.service_networks: Refreshing state...
data.triton_image.green_image: Refreshing state...
triton_machine.green_machine[1]: Refreshing state... (ID: 7db3429a-e20c-ca47-dd2e-cc0e84a24e5e)
triton_machine.blue_machine[1]: Refreshing state... (ID: 3f7e2689-07fb-45f0-bd71-953e3f90877d)
triton_machine.green_machine[0]: Refreshing state... (ID: 48abac15-a182-c904-fd42-8c85b5492c01)
triton_machine.green_machine[2]: Refreshing state... (ID: 6c623e78-d879-e17c-b582-f8b5bf130a11)
triton_machine.blue_machine[2]: Refreshing state... (ID: 5bf6052f-0823-e558-de5b-c5b31b404245)
triton_machine.blue_machine[0]: Refreshing state... (ID: 63590c9a-e9e5-60ed-d959-8cf3992c5d29)
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place
Terraform will perform the following actions:
  ~ triton_machine.blue_machine[0]
      cns.0.services.0: "happiness" => "staging-happiness"
  ~ triton_machine.blue_machine[1]
      cns.0.services.0: "happiness" => "staging-happiness"
  ~ triton_machine.blue_machine[2]
      cns.0.services.0: "happiness" => "staging-happiness"
  ~ triton_machine.green_machine[0]
      cns.0.services.0: "staging-happiness" => "happiness"
  ~ triton_machine.green_machine[1]
      cns.0.services.0: "staging-happiness" => "happiness"
  ~ triton_machine.green_machine[2]
      cns.0.services.0: "staging-happiness" => "happiness"
Plan: 0 to add, 6 to change, 0 to destroy.
This plan was saved to: blue-green.plan
To perform exactly these actions, run the following command to apply:
   terraform apply "blue-green.plan"</code>

Applying the updated CNS names

Once you're satisfied the information is correct, run terraform apply "blue-green.plan".

Remember that new DNS names often do not propagate immediately. You can use dig to confirm that the correct IP addresses are pointed at your domain:

<code class="language-sh">$ dig +noall +answer alexandra.space
happy.alexandra.space.  1764    IN  CNAME   happiness.svc.d9a01feb-be7d-6a32-b58d-ec4a2bf4ba7d.us-sw-1.triton.zone.
happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone. 30 IN A165.225.159.240
happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone. 30 IN A165.225.158.220
happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone. 30 IN A165.225.159.196</code>

Step 8: (optional) decommission the blue instances

It's now safe to decommission the blue instances. I'll go into variables.tf to change blue_count to 0:

<code class="language-hcl">variable "blue_count" {
    type = "string"
    description = "The number of 'blue' instances to create."
    default = "0"
}</code>

This is optional because the blue instances now have a staging DNS name. If you don't want anyone to have access to the older version, or you're concerned about the additional cost of running both simultaneously, proceed to plan this update. Otherwise, skip to the wrap up.

Planning the updated infrastructure

Let's make sure that the infrastructure will be properly updated with terraform plan:

<code class="language-bash">$ terraform plan -out blue-green.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.green_image: Refreshing state...
data.triton_image.blue_image: Refreshing state...
data.triton_network.service_networks: Refreshing state...
triton_machine.green_machine[0]: Refreshing state... (ID: 48abac15-a182-c904-fd42-8c85b5492c01)
triton_machine.green_machine[2]: Refreshing state... (ID: 6c623e78-d879-e17c-b582-f8b5bf130a11)
triton_machine.green_machine[1]: Refreshing state... (ID: 7db3429a-e20c-ca47-dd2e-cc0e84a24e5e)
triton_machine.blue_machine[1]: Refreshing state... (ID: 3f7e2689-07fb-45f0-bd71-953e3f90877d)
triton_machine.blue_machine: Refreshing state... (ID: 63590c9a-e9e5-60ed-d959-8cf3992c5d29)
triton_machine.blue_machine[2]: Refreshing state... (ID: 5bf6052f-0823-e558-de5b-c5b31b404245)
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:
  - triton_machine.blue_machine
  - triton_machine.blue_machine[1]
  - triton_machine.blue_machine[2]
Plan: 0 to add, 0 to change, 3 to destroy.
This plan was saved to: blue-green.plan
To perform exactly these actions, run the following command to apply:
    terraform apply "blue-green.plan"</code>

Now that we have confirmed just the blue instances will be removed, we can set it into motion.

Applying the updated infrastructure

Let's officially remove the blue instances with the blue-green.plan.

<code class="language-bash">$ terraform apply "blue-green.plan"
triton_machine.blue_machine[2]: Destruction complete after 21s
triton_machine.blue_machine: Destruction complete after 21s
triton_machine.blue_machine[1]: Destruction complete after 21s
Apply complete! Resources: 0 added, 0 changed, 3 destroyed.
Outputs:
green_domains = [
   [
      "48abac15-a182-c904-fd42-8c85b5492c01.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happy-1.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "7db3429a-e20c-ca47-dd2e-cc0e84a24e5e.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happy-2.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ],
   [
      "6c623e78-d879-e17c-b582-f8b5bf130a11.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happy-3.inst.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "staging-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone",
     "green-happiness.svc.d9a01feb-bo7d-6a32-b38d-ec4a2bf4ca7d.us-sw-1.triton.zone"
   ]
]</code>

The Happy Randomizer has officially been updated to version 1.1.0.

Wrapping up

Step-by-step review of blue-green deployment

We just went through eight steps for a full blue-green deployment. Not every step was directly related to instance creation/deletion. Some steps, like step 1, were essential preambles in order to successfully implement the deployment.

Download the Terraform files.

There are a number of use cases for blue-green deployments, this was just one example. Go forth with your new knowledge of how to create a Terraform configuration file geared towards automating updates to your applications.