Showing posts with label heroku. Show all posts
Showing posts with label heroku. 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:

Thursday, May 28, 2015

Tv Show Tracker - On Heroku

I recently decided that I should start testing out my tv show tracker website out in "the real web" instead of just local development. So to help me along I chose to use Heroku, which is a PaaS - platform as a service. The advantage of using a PaaS is that they manage your actual box, upgrades, security, etc. for you. All you have to do is build and deploy your site according to their guidelines.

After going through their documentation of how to deploy a Scala application, I have successfully deployed tv-show-tracker on Heroku. However, this was not as straight-forward as the documentation made it out to be - especially when I'm using spray instead of just a quick HTTP server like their example uses. So I thought I'd go through some of the things I had to do to get my site up and running on their platform.

First, lucky for me, I already had my site fully utilizing sbt so I had most of the config and dependencies working as they want you to. However, I did have to add an sbt plugin:
 addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.8.0")  
After that was added, I had to add the following to my build.sbt file:
 import NativePackagerKeys._  
 packageArchetype.java_application  
Heroku utilizes this native packager and foreman to run Scala applications which is not something I was doing when I would run it locally.

Secondly, I had issues connecting to my PostgreSQL database. They went through some of the steps, however I felt some things were lacking. In the end, my database connection looks like:
 val dbUri = new URI(System.getenv("DATABASE_URL"))  
 val username = dbUri.getUserInfo.split(":")(0)  
 val password = dbUri.getUserInfo.split(":")(1)  
 val db = Database.forURL(s"jdbc:postgresql://${dbUri.getHost}:${dbUri.getPort}${dbUri.getPath}", driver = "org.postgresql.Driver", user = username, password = password)  
This connection setup uses slick, where their example just had a regular database connection.

Next, I had issues seeing the data in my database. I was lucky enough to come across this stack overflow question which helped me solve my problem - look at my data on my local install of pgadmin. Now, the accepted answer is what I used to get it working, however not all steps are properly documented in that answer.

To get all of the information needed to fill out the pgadmin server, you first need to run:
 heroku config  
If you have the database add-on installed, it will spit out a database url that we need. We just need to know how to decipher this long url and break it into the individual parts:
 DATABASE_URL: postgres://user:password@server:port/database  
Once we know how the url is broken down (example above), you just plug each part into pgadmin (like the SO answer says) and you can see your data on your local install of pgadmin.

Lastly, I had issues with my site properly binding to the host and port and being able to hit the url from my computer.

For the port, I tried putting in 8080 and 5000 (from the example) but neither seemed to work. I eventually found this documentation which explains that I need to get the port from a system variable - PORT.

I also had issues with the hostname I was supposed to connect to. I tried "localhost" and my site's url but neither of those worked. Then I came across this SO answer which showed I needed to use "0.0.0.0". This allows the app to bind to the localhost and accept incoming traffic from my sites Heroku url.
So, in the end my connection looks like:
 val myPort = Properties.envOrElse("PORT", "8080").toInt // for Heroku compatibility  
 IO(Http) ? Http.Bind(service, interface = "0.0.0.0", port = myPort)  
This allows my app to get the proper port from Heroku or use a default of 8080 if I'm running locally.

All in all, this turned out pretty well seeing that I was able to go from my local setup to running on Heroku in just a few hours. There were a few hiccups along the way, but it seems like other people have ran into the issues I experienced before and I was able to use the answers given to them.