/gradle-heroku-plugin

Gradle plugin for managing Heroku deployments.

Primary LanguageGroovyApache License 2.0Apache-2.0

Gradle Heroku plugin CircleCI

Overview

There are tons of built-in deployment mechanisms provided by Heroku. However if you need or want to have control over your deployment pipeline, and rely on Heroku as a platform implementation detail, this plugin will help you to automate interactions with Heroku API.

Bear in mind that we talk about deploy in a broader, or e2e, sense. From the creation of the Heroku App until checking that the dynos are serving the expected content. We provide an opinionated view of a deployment and the building blocks so you can craft your own deployment task. The only previous requirements for the plugin to work are:

  • Your apps packed on binary form.

  • A Heroku account and its related API key.

You may need the binary for different purposes such as integration testing, QA environment or to support different deployments like enterprise on-premises solutions. The pattern of build the binary only once enables the following goodies:

  • Efficiency through build avoidance.

  • Audit. You may enforce traceability policies to ensure that the binary that you tested against QA is the very same than the one deployed on production.

This plugin embraces the agnostic nature of Heroku, as far as you provide a supported buildpack with a compatible binary.

Usage

You can apply the plugin using the plugins DSL

plugins {
  id "com.felipefzdz.gradle.heroku" version "1.0.6"
}

Or using the buildscript block

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.felipefzdz.gradle.heroku:gradle-heroku-plugin:1.0.6"
  }
}

apply plugin: "com.felipefzdz.gradle.heroku"

N.B. Gradle 5.2 is required as minimum version.

This full-fledged plugin provides an opinionated view of some Heroku related tasks, plus the building blocks defined on the base plugin.

You’ll need an account in Heroku with its related API_KEY. Creating a team is optional, as you can use personal apps. That key can be injected into Gradle through an env key named GRADLE_HEROKU_PLUGIN_API_KEY or a Gradle property named herokuPluginApiKey.

  • GRADLE_HEROKU_PLUGIN_API_KEY=123 ./gradlew yourTask

  • ./gradlew yourTask -PherokuPluginApiKey=123

DSL Example

import com.felipefzdz.gradle.heroku.tasks.model.HerokuWebApp
import com.felipefzdz.gradle.heroku.tasks.model.HerokuDatabaseApp

heroku {
    bundles {
        dev {
            'database'(HerokuDatabaseApp) {
                bundlePosition = 1
                teamName = 'test'
                stack = 'heroku-16'
                personalApp = true
                addons {
                    database {
                        plan = 'heroku-postgresql:hobby-dev'
                        config = ['version': '10.7']
                        waitUntilStarted = true
                    }
                }
                migrateCommand = 'bash'
            }
            'excludedDatabase'(HerokuDatabaseApp) {
                excludeFromDeployBundle = true
                teamName = 'test'
                stack = 'heroku-16'
                personalApp = true
                addons {
                    database {
                        plan = 'heroku-postgresql:hobby-dev'
                        waitUntilStarted = true
                    }
                }
                migrateCommand = 'bash'
            }
            'web'(HerokuWebApp) {
                bundlePosition = 2
                teamName = 'test'
                stack = 'heroku-16'
                personalApp = true
                addons {
                    'rabbitmq-bigwig' {
                        plan = 'rabbitmq-bigwig:pipkin'
                        waitUntilStarted = true
                    }
                }
                addonAttachments {
                    database {
                        owningApp = '$DATABASE_APP_NAME'
                    }
                }
                logDrains = ['logDrainUrl', 'anotherLogDrainUrl']
                buildSource {
                    buildpackUrl = 'https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/jvm-common.tgz'
                    buildUrl = { 'https://my-domain/example.tgz' }
                    buildVersion = '666'
                }
                config {
                    configToBeExpected = ['MODE': 'dev', 'API_KEY': 'secret']
                }
                features = ['http-session-affinity']
                process {
                    type = 'web'
                    size = 'standard-1x'
                    quantity = 2
                }
                readinessProbe {
                    url = 'https://web.herokuapp.com/version'
                    command = { app, json ->
                        assert json.buildNumber == app.buildSource.buildVersion
                    }
                }
                disableAcm = true
                domains = ['my-first.domain.com', 'my-second.domain.com']
            }
        }
    }
}

Opinionated tasks

The plugin defines the following tasks, being Dev and Database dynamic placeholders that will vary depending on your bundle and app, respectively.

Task name Type

herokuDeployDevBundle

DeployBundleTask

herokuDeployDevDatabase

DeployAppTask

herokuVerifyConfigForDevBundle

VerifyConfigBundleTask

herokuVerifyConfigForDevDatabase

VerifyConfigTask

Understanding Deploy task

The deploy bundle task will iterate over existing apps contained in the selected bundle and execute them in order (respecting exclusions, if any).

We can see here a list of steps that conforms the deployment of a web app:

maybeCreateApplication
installAddons
configureLogDrains
createBuild
addConfig
enableFeatures
addAddonAttachments
waitForAppFormation
updateProcessFormation
updateDomains
probeReadiness
maybeDisableAcm

Slightly different for a database app:

maybeCreateApplication
installAddons
configureLogDrains
createBuild
addConfig
waitForAppFormation
updateProcessFormation
migrateDatabase
maybeDisableAcm

Understanding Verify Config task

This task might be executed as a pre step for a deployment, or as a infrastructure check to verify that the config contained in your gradle build (that represents your whole platform in a Disaster Recovery event) is consistent with the already deployed config.

To achieve that we provide four user generated collections plus the already deployed config. The task will fail if any of these conditions hold: missingConfig || unexpectedConfig || incorrectConfig

As you shouldn’t store sensible config on your VCS (you should inject it in your env through your preferred mechanism), the convention here is to fill those sensible values with secret. Those values will be therefore excluded from the incorrectConfig check.

This task also enforces the concept of cattle not pets, discouraging config changes made directly on the actual instances (unless you accompany that with a change in your Gradle build).

Opinionated task execution examples

Based on the example DSL provided above:

./gradlew herokuDeployDevBundle will deploy database and web apps on that order.

./gradlew herokuDeployDevDatabase will just deploy database.

./gradlew herokuDeployDevExcludedDatabase will deploy excludedDatabase as the exclusion only applies to the bundle task.

./gradlew herokuVerifyConfigForDevBundle will verify that the config contained in the DSL for the dev bundle is coherent with the already deployed config, if any.

./gradlew herokuVerifyConfigForDevDatabase will verify that the config contained in the DSL for the database app is coherent with the already deployed config, if any.

Extension properties

The heroku closure expects the following property:

Property name Type Description

bundles

NamedDomainObjectContainer<HerokuAppContainer>

A collection of bundles that serves as a wrapper for the apps to be deployed

Within bundles you should provide 1 to N named blocks that will represent your bundles, e.g. dev and prod.

Those HerokuAppContainer will expect 1 to N HerokuApp subtype blocks such as 'database'(HerokuDatabaseApp) or 'web'(HerokuWebApp).

HerokuApp simple extension properties

Let’s see the simple extension properties for HerokuApp:

Property name Type Default value Description

bundlePosition

Integer

-1

Instructs deployment order. This property is optional and in case of position collision first app defined will be first deployed.

teamName

String

''

Teams allow you to manage access to a shared group of applications and other resources.

stack

String

'heroku-16'

Stacks are the different application execution environments available in the Heroku platform.

personalApp

Boolean

false

Force creation of the app in the user account even if a default team is set.

recreate

Boolean

false

Whether to destroy the app beforehand deploying it.

logDrains

List<String>

[]

Log drains provide a way to forward your Heroku logs to an external syslog server for long-term archiving.

disableAcm

Boolean

false

Disable Automated Certificate Management (ACM) flag for an app.

HerokuApp nested extension properties

Those are the nested extension properties for HerokuApp.

BuildSource extension properties

BuildSource → A build represents the process of transforming a code tarball into a slug.

Property name Type Default value Description

buildpackUrl

String

''

Location of the buildpack for the app. Either a url (unofficial buildpacks) or an internal urn (heroku official buildpacks).

buildUrl

Supplier<String>

{ → '' }

URL where gzipped tar archive of source code for build was downloaded. Supplier wrappers serves as a lazy mechanism to avoid costly calculations on configuration time, e.g. presigned AWS urls.

buildVersion

String

''

Version of the gzipped tarball.

HerokuConfig extension properties

HerokuConfig → Allow you to manage the configuration information provided to an app on Heroku.

Property name Type Default value Description

configToBeExpected

Map<String, String>

[:]

User defined config already deployed on an existing app

configToBeRemoved

List<String>

[]

User defined config to be removed on the next release

configToBeAdded

List<String>

[]

User defined config to be added on the next release

configAddedByHeroku

List<String>

[]

Heroku defined config such as DATABASE_URL

HerokuAddon extension properties

HerokuAddon → Add-ons are cloud services that extend Heroku apps with useful features and services.

Property name Type Default value Description

name

String

This is named container, so the value is mandatory when defining the DSL

Globally unique name of the add-on pattern: ^[a-zA-Z][A-Za-z0-9_-]+$

plan

String

''

Unique name of this plan, e.g rabbitmq-bigwig:pipkin

config

Man<String, String>

[:]

custom add-on provisioning options, e.g ['version': '10.7']

waitUntilStarted

Boolean

false

If true, the task will wait up to 10 minutes (this hardcoded value will be made configurable in further plugin releases) with a dumb retry policy until the add-on has been added.

HerokuProcess extension properties

HerokuProcess → The formation of processes that should be maintained for an app.

Property name Type Default value Description

type

String

''

Type of process to maintain. Pattern: ^[-\w]{1,128}$, e.g web.

quantity

Integer

0

Number of processes to maintain.

size

String

'standard-1X'

Dyno size.

HerokuDatabaseApp extension properties

HerokuDatabaseApp subtype offers the following extension properties:

Property name Type Default value Description

migrateCommand

String

''

Command to be executed as one-off dyno. This is meant to be used for db migration purposes, but you could hijack it for whatever you have in mind. Example: migrator -migrationVersion 12, assuming that migrator is a valid command defined on your Procfile.

HerokuWebApp extension properties

HerokuWebApp subtype offers the following simple extension properties:

Property name Type Default value Description

features

List<String>

[]

An app feature represents a Heroku labs capability that can be enabled or disabled for an app on Heroku, e.g. http-session-affinity.

domains

List<String>

[]

Domains define what web routes should be routed to an app on Heroku.

HerokuWebApp subtype offers the following nested extension properties.

HerokuAddonAttachment extension properties

HerokuAddonAttachment → An add-on attachment represents a connection between an app and an add-on that it has been given access to.

Property name Type Default value Description

name

String

This is named container, so the value is mandatory when defining the DSL

Unique name for this add-on attachment to this app.

owningApp

String

''

Unique name of owning app.

ReadinessProbe extension properties

ReadinessProbe → Mechanism to verify that the app is up and running.

Property name Type Default value Description

url

String

''

Url for the health endpoint that will serve us as a readiness probe.

command

BiAction<HerokuApp, Map<String, ?>>

{ app, jsonResponse → }

This function should assert onto the value returned by the health endpoint. Example: ` assert jsonResponse.buildId == app.buildSource.buildVersion`

Base plugin

If you want to create your own opinionated view of a Heroku deployment, the base plugin will give you the required building blocks. You may also be interested on the building blocks in isolation to enhance with automation your existing deployment pipeline.

To use the base plugin, use these alternative snippets:

plugins {
  id "com.felipefzdz.gradle.heroku.base" version "1.0.6"
}

Or using the buildscript block

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.felipefzdz.gradle.heroku:gradle-heroku-plugin:1.0.6"
  }
}

apply plugin: "com.felipefzdz.gradle.heroku.base"

Custom task types

The base plugin provides the following custom task types:

Type Description

AddAddonAttachmentsTask

An add-on attachment represents a connection between an app and an add-on that it has been given access to.

AddEnvironmentConfigTask

Update config-vars for app. You can update existing config-vars by setting them again, and remove by setting it to null.

ConfigureLogDrainsTask

Log drains provide a way to forward your Heroku logs to an external syslog server for long-term archiving.

CreateAppTask

An app represents the program that you would like to deploy and run on Heroku.

CreateBuildTask

A build represents the process of transforming a code tarball into a slug

CreateBundleTask

A bundle is a collections of apps.

DestroyAppTask

Destroys an app.

DestroyBundleTask

Destroys a bundle.

EnableFeaturesTask

An app feature represents a Heroku labs capability that can be enabled or disabled for an app on Heroku.

InstallAddonsTask

Add-ons are cloud services that extend Heroku apps with useful features and services.