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.

No comments:

Post a Comment