Showing posts with label terraform. Show all posts
Showing posts with label terraform. Show all posts

Tuesday, July 7, 2020

Conclusion - CI/CD Series

In this series, we have covered how to implement CI/CD (continuous integration, continuous deployment) using DevOps (development and operations). We have shown how to go from a simple change in the code, to adding tests, building an assembly, planning the deployment, and eventually apply the changes to our live environment:
  1. Introduction
  2. Base Application
  3. Unit Tests
  4. Assembly
  5. PATs
  6. Plan
  7. Apply
Future enhancements to this pipeline could include:
  • Separate DEV and PROD environments
    • DEV deploy on pushes to master
    • PROD deploy on specific tag/release cycles
  • Performance tests for the REST service
  • Ability to destroy the resources using the Terraform destroy command
  • Run PATs against service deployed in the DEV environment
    • Currently, PATs are only run against the service running in-memory

Monday, July 6, 2020

Apply - CI/CD Series

In the final piece of our CI/CD Series puzzle, we will perform the Terraform apply action. This action will perform the actual changes (as shown on the Terraform plan action) to our Heroku app and ensure everything starts up correctly. Once this command finishes, our application will be live in our account and accessible for utilization.

The Terraform apply CI stage is very similar to the plan stage we developed previously. However, there are some differences to note:
  • We will run this command on pushes to the master branch
    • The plan action was ran on pull requests to master
  • We will create a GitHub Release with a specific version of our application
    • The plan action used a hard-coded URL
  • We will point Heroku to the GitHub Release to ensure the correct version is deployed
Create Release

To create a release, we can utilize the GitHub Actions create release template. Our release name will be our application version plus the build number to ensure each release has a unique id. Once we create the release, we will save the version to a file so it can be referenced from other jobs within our CI/CD pipeline.

create_release:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2

- name: Create Release
id: release
uses: actions/create-release@v1
env:
# This token is provided by Actions, you do not need to create your own token
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: 0.1.0-${{ github.run_number }}
release_name: Release 0.1.0-${{ github.run_number }}
draft: false
prerelease: false

# Heroku needs the .tar.gz URL so modify tag URL to expected format
- name: Create Version File
run: |
export RELEASE_URL=${{ steps.release.outputs.html_url }}
RELEASE_URL+=".tar.gz"
echo "Release URL:"
echo ${RELEASE_URL}
export ARCHIVE_URL=$(echo "$RELEASE_URL" | sed 's~releases/tag~archive~')
echo "Archive URL:"
echo ${ARCHIVE_URL}
echo ${ARCHIVE_URL} >> archive.txt

# Upload version file as build artifact
- name: Upload Version File
uses: actions/upload-artifact@v2
with:
name: archive.txt
path: archive.txt

Pass Release Version

On the stage to perform the apply, we first need to read the version file uploaded when creating the release. Then, we can tell Terraform about this variable so it gets injected at runtime (since it changes on every build). In our example, we expose the variable "build_url" from our Terraform file. 

variable "build_url" {
type = string
}

# Build code & release to the app
resource "heroku_build" "guestbook_build" {
app = heroku_app.guestbook_app.name
buildpacks = ["https://github.com/heroku/heroku-buildpack-scala"]

source = {
url = var.build_url
}

To change this at runtime, we make use of Terraform's variables. This variable gets initialized by:
  1. Reading the version URL from the artifact created via the release.
  2. Exporting the version URL to an environment variable.
export TF_VAR_build_url=$(cat archive.txt)
echo "Archive URL:"
echo ${TF_VAR_build_url}

Apply Action

Now that we have everything setup, we just need to perform the actual apply command via Terraform. In this example, we still running the commands validate and plan just to ensure things are correct, but these can be skipped as we ran them on the pull request itself.

deploy:
runs-on: ubuntu-latest
needs: create_release

steps:
- name: Checkout Repo
uses: actions/checkout@v2

# Download artifact
- name: Download Version File
uses: actions/download-artifact@v2
with:
name: archive.txt

- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

- name: Terraform Init
id: init
run: terraform init

- name: Terraform Validate
id: validate
run: terraform validate -no-color

# The build_url is blank for planning since we will create a new URL upon commits
- name: Terraform Plan
id: plan
run: |
export TF_VAR_build_url=$(cat archive.txt)
echo "Archive URL:"
echo ${TF_VAR_build_url}
export HEROKU_API_KEY=${{ secrets.HEROKU_API_KEY }}
export HEROKU_EMAIL=${{ secrets.HEROKU_EMAIL }}
terraform plan -no-color

- name: Terraform Apply
id: apply
run: |
export TF_VAR_build_url=$(cat archive.txt)
echo "Archive URL:"
echo ${TF_VAR_build_url}
export HEROKU_API_KEY=${{ secrets.HEROKU_API_KEY }}
export HEROKU_EMAIL=${{ secrets.HEROKU_EMAIL }}
terraform apply -auto-approve -no-color

Deployed

Once the CI/CD pipeline succeeds on the master branch, the service will be live and available to be used. Also, since we used the Terraform Cloud for our remote state storage, you can browse to see how the state file has changed. This will keep track of all the changes to your application over the history of every deploy.

The live service can be accessed via its health check:
This URL is also output from the apply action:

Conclusion

In conclusion, we were able to fully automate our deploys on pushes to the master branch of our repo. This will ensure that each time a code change happens, the latest version gets automatically pushed to our live service.

The full changeset can be found on this pull request.

Sunday, June 28, 2020

Plan - CI/CD Series

The next step in our CI/CD workflow is to plan out the changes to be made to our production instance. This planning will allow us to:
  • Perform a dry-run of upgrading our environment
  • Run the dry-run on all pull requests to the master branch
  • Ensure our deployment process is repeatable
  • Ensure our deployment process is automated
Heroku

As mentioned in the introduction post, we will be hosting our site on Heroku. Heroku is a Platform as a Service (PaaS) which allows us to just tell the system where our code is, how to build it, and how to run. The rest of the cloud orchestration is taken care for us:
  • Security
  • Compute nodes
  • Logging
  • Access management
  • Optional add-ons
While we could as easily deploy this application to AWS, GCP, Azure, etc, Heroku takes away all the heavy lifting of ensuring our service is managed in a safe and secure way.

Terraform

To help with this planning phase of our application, we will be using Terraform. Terraform is an Infrastructure as Code tool which allows us to specify the resources we need in code definition files rather than scripts, plugins, manual edits, etc. This can really be a benefit if we have many resources across multiple domains because the definitions and ways to apply those definitions are still the same no matter what cloud/resources we need to build. Also, Terraform supports many cloud providers and Heroku is one of them.

In our example, we only have 1 resource on Heroku so it would be possible to use scripts or plugins to perform this same deployment, however I find it easier to be consistent and use Terraform for as much as possible. This way should we add something else (e.g. S3 bucket) to our deployment, we do not need to change the plan/deploy actions, we would only need to add extra definitions to our files to specify we now need something else.

Terraform State

When Terraform runs an action (plan, apply, destroy), it manages the state of the system in a state file. By default, this file is placed in the current directory of the actions being ran. Though, Terraform allows this state file to be saved elsewhere via remote state. Since our CI/CD build runs on top of GitHub Actions using Docker images, the current directory gets wiped each time we do a new build. Thus, for our application, we will be using the free Terraform Cloud which allows us to save our state file to be reused across all of our builds.

Heroku Buildpack

To launch our application on Heroku, we need to specify a build pack to use. This will let Heroku know what kind of application we have, how to build it, and how to run it. For our use-case, we will be using the Heroku Buildpack for Scala. To get this buildpack to work correctly, we need to provide a few things:
  1. A new SBT command of "stage" which can build the application from source.
  2. A URL to the source to be built.
  3. Procfile which specifies how to run our built application.
For the "stage" command, this is as simple as adding an alias to our "build.sbt" file:

addCommandAlias("stage", "clean;compile;assembly")

For the URL, right now we can leave this blank. Since we are only doing the initial planning of resources, we will not actually be deploying anything. Once we add the code to do the final deploy, we will have to modify this URL based upon the git tag we want to use.

Our Procfile for this application is very simple. We just use the same Java commands we have used for our PATs previously:

web: java -jar target/scala-2.13/cicd-series-assembly-*.jar

Heroku Terraform File

Next, we want to start to build our Terraform file which will indicate how we build our resources on Herkou. Our file will consist of the following items:
  1. Specifying we want to use remote state management and what organization/workspace to use.
  2. Specifying that this file uses Heroku resources.
  3. Allowing a variable to be injected for the source URL of the build.
  4. What Heroku application we want to manage.
  5. How our application gets built with Heroku build.
  6. What type of compute resources we want to use specified by Heroku formation.
  7. The output URL of our application when running.
The full Terraform file for this is:

# Example copied from - https://www.terraform.io/docs/github-actions/setup-terraform.html

terraform {
backend "remote" {
organization = "cicd-series"

workspaces {
name = "heroku-prod"
}
}
}

provider "heroku" {
version = "~> 2.0"
}

variable "build_url" {
type = string
}

resource "heroku_app" "guestbook_app" {
name = "cicd-series-guestbook"
region = "us"
}

# Build code & release to the app
resource "heroku_build" "guestbook_build" {
app = heroku_app.guestbook_app.name
buildpacks = ["https://github.com/heroku/heroku-buildpack-scala"]

source = {
url = var.build_url
}
}

# Launch the app's web process by scaling-up
resource "heroku_formation" "guestbook_formation" {
depends_on = [heroku_build.guestbook_build]

app = heroku_app.guestbook_app.name
type = "web"
quantity = 1
size = "free"
}

output "guestbook_url" {
value = "https://${heroku_app.guestbook_app.name}.herokuapp.com"
}

GitHub Action

Now that we have all of the individual pieces setup, we need to integrate this plan into our GitHub Actions. For our use-case, we will run Terraform's plan command on every pull request to master. Terraform has template that can be used to directly integrate with GitHub Actions:
Our setup is very similar to the example provided in that repo, however we need to specify our Terraform variable. The full syntax of our plan is:

# Terraform setup copied from
# https://github.com/hashicorp/setup-terraform
plan:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2

- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

- name: Terraform Init
id: init
run: terraform init

- name: Terraform Validate
id: validate
run: terraform validate -no-color

# The build_url is blank for planning since we will create a new URL upon commits
- name: Terraform Plan
id: plan
run: |
export TF_VAR_build_url=""
terraform plan -no-color

- name: Terraform Report
id: report
uses: actions/github-script@0.9.0
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖${{ steps.validate.outputs.stdout }}
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

<details><summary>Show Plan</summary>

\`\`\`${process.env.PLAN}\`\`\`

</details>

*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;

github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})

GitHub Secrets

Since we are using live accounts on Terraform Cloud and Heroku, we need to specify a few API keys to ensure our builds use our accounts. The following GitHub Secrets are needed to be added to this repo to work correctly:
  • HEROKU_EMAIL
    • Used to specify which email account to be used for Heroku access
  • HEROKU_API_KEY
    • Used to specify which API key to be used for Heroku access
  • TF_API_TOKEN
    • Used to specify which API key to be used for Terraform Cloud access
Conclusion

In this post, we went from having no cloud resources to now having a plan of what cloud resources will be provisioned when we apply our configuration. In the final piece of the puzzle, we will add this apply stage upon pushes to the master branch.

The full code changeset can be found on this pull request.

Wednesday, May 27, 2020

Introduction - CI/CD Series

Over the last several years, the terms CI/CD (continuous integration, continuous deployment) and DevOps (development and operations) have grown to be known throughout most of the software industry. The implication of these practices is that no longer must software wait for a "release ritual" to push a new version to production. Nor must software be developed by one team and then "handed over" to a production support team. 

Instead, now a development team manages the software from end-to-end:
  • Requirements
  • Code changes
  • Testing
  • Build and package artifacts
  • Deploy to production
  • Support
Also, software has seen a much quicker release cycles. It is even possible to have pipelines that perform releases and deployments upon every committed code change.

Throughout the next few posts, we will build a fully automated CI/CD pipeline for a simple REST service. The series will be broken down into individual posts:
  1. Introduction (this post)
  2. Base application
  3. CI for testing
  4. Dockerizing the application
  5. Product acceptance testing (PATs)
  6. CD for deployment
The tools we will be working with throughout this project will be: