Building Invocation Images

When you build a Cloud Native Application Bundle (CNAB) with Porter, a bundle.json and an invocation image are created for you. How does Porter turn your porter.yaml into an invocation image? This walkthrough will explain how Porter constructs the invocation image, including how mixins and other bundles allow you to compose functionality.

Starting From Scratch

When you create a new bundle with Porter, your project is bootstrapped with a sample porter.yaml and a new cnab directory. This scaffolding provides almost everything you need to generate your CNAB, including the invocation image. Let’s use this to explain how the invocation image is built.

To create a new CNAB with Porter, you first run porter create. The generated porter.yaml will look like this:


name: porter-hello
version: 0.1.0
description: "An example Porter configuration"
registry: getporter

mixins:
  - exec

install:
  - exec:
      description: "Install Hello World"
      command: ./helpers.sh
      arguments:
        - install

upgrade:
  - exec:
      description: "World 2.0"
      command: ./helpers.sh
      arguments:
        - upgrade

uninstall:
  - exec:
      description: "Uninstall Hello World"
      command: ./helpers.sh
      arguments:
        - uninstall

After the scaffolding is created, you may edit the porter.yaml and modify the registry: getporter element representing the Docker registry that you can push to. Note that the bundle is not pushed during the porter build workflow.

Once you have modified the porter.yaml, you can run porter build to generate your first invocation image. Here we add the --debug flag to see all of the output:

$  porter build --debug
Resolved porter binary from /usr/local/bin/porter to /Users/sigje/.porter/porter
Running linters for each mixin used in the manifest...
Copying porter runtime ===>
Copying mixins ===>
Copying mixin exec ===>

Generating Dockerfile =======>
DEBUG name:    exec
DEBUG pkgDir: /Users/sigje/.porter/mixins/exec
DEBUG file:
DEBUG stdin:
actions:
  install:
  - exec:
      arguments:
      - install
      command: ./helpers.sh
      description: Install Hello World2
  uninstall:
  - exec:
      arguments:
      - uninstall
      command: ./helpers.sh
      description: Uninstall Hello World
  upgrade:
  - exec:
      arguments:
      - upgrade
      command: ./helpers.sh
      description: World 2.0

/Users/sigje/.porter/mixins/exec/exec build --debug
FROM debian:stretch

ARG BUNDLE_DIR

RUN apt-get update && apt-get install -y ca-certificates

# exec mixin has no buildtime dependencies


COPY . $BUNDLE_DIR
RUN rm -fr $BUNDLE_DIR/.cnab
COPY .cnab /cnab
COPY porter.yaml $BUNDLE_DIR/porter.yaml
WORKDIR $BUNDLE_DIR
CMD ["/cnab/app/run"]

Writing Dockerfile =======>
FROM debian:stretch

ARG BUNDLE_DIR

RUN apt-get update && apt-get install -y ca-certificates

# exec mixin has no buildtime dependencies


COPY . $BUNDLE_DIR
RUN rm -fr $BUNDLE_DIR/.cnab
COPY .cnab /cnab
COPY porter.yaml $BUNDLE_DIR/porter.yaml
WORKDIR $BUNDLE_DIR
CMD ["/cnab/app/run"]

Starting Invocation Image Build =======>
Step 1/9 : FROM debian:stretch
 ---> 5738956efb6b
Step 2/9 : ARG BUNDLE_DIR
 ---> Using cache
 ---> c9d91881dd7c
Step 3/9 : RUN apt-get update && apt-get install -y ca-certificates
 ---> Using cache
 ---> afa85b98ed97
Step 4/9 : COPY . $BUNDLE_DIR
 ---> Using cache
 ---> e4057b41978c
Step 5/9 : RUN rm -fr $BUNDLE_DIR/.cnab
 ---> Using cache
 ---> ee114d95bc2d
Step 6/9 : COPY .cnab /cnab
 ---> Using cache
 ---> 1bb73c63ef65
Step 7/9 : COPY porter.yaml $BUNDLE_DIR/porter.yaml
 ---> Using cache
 ---> 483c6b05a0b7
Step 8/9 : WORKDIR $BUNDLE_DIR
 ---> Using cache
 ---> 9d2497296f3b
Step 9/9 : CMD ["/cnab/app/run"]
 ---> Using cache
 ---> 23c208fd5dc7
Successfully built 23c208fd5dc7
Successfully tagged getporter/porter-hello-installer:0.1.0
DEBUG name:    arm
DEBUG pkgDir: /Users/sigje/.porter/mixins/arm
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/arm/arm version --output json --debug
DEBUG name:    aws
DEBUG pkgDir: /Users/sigje/.porter/mixins/aws
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/aws/aws version --output json --debug
DEBUG name:    az
DEBUG pkgDir: /Users/sigje/.porter/mixins/az
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/az/az version --output json --debug
DEBUG name:    exec
DEBUG pkgDir: /Users/sigje/.porter/mixins/exec
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/exec/exec version --output json --debug
DEBUG name:    gcloud
DEBUG pkgDir: /Users/sigje/.porter/mixins/gcloud
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/gcloud/gcloud version --output json --debug
DEBUG name:    helm
DEBUG pkgDir: /Users/sigje/.porter/mixins/helm
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/helm/helm version --output json --debug
DEBUG name:    kubernetes
DEBUG pkgDir: /Users/sigje/.porter/mixins/kubernetes
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/kubernetes/kubernetes version --output json --debug
DEBUG name:    terraform
DEBUG pkgDir: /Users/sigje/.porter/mixins/terraform
DEBUG file:
DEBUG stdin:

/Users/sigje/.porter/mixins/terraform/terraform version --output json --debug

A lot just happened by running that command! Let’s walk through the output and discuss what happened.

Copying porter runtime ===>
Copying mixins ===>
Copying mixin exec ===>

The first thing that happens after running porter build, Porter will copy its runtime plus any mixins into the .cnab/app directory of your bundle.

Porter locates available mixins in the $PORTER_HOME/mixins directory. By default, the Porter home directory is located in ~/.porter. In this example, we are using the exec mixin, so the $PORTER_HOME/mixins/exec directory will be copied into the invocation image. When a mixin is installed for use with Porter, it contains binaries for multiple operating systems. The correct binary will be copied into the current .cnab directory for use in the invocation image.

After copying any mixins to the .cnab directory of the bundle, a Dockerfile is generated:

Generating Dockerfile =======>
FROM debian:stretch

ARG BUNDLE_DIR

RUN apt-get update && apt-get install -y ca-certificates

# exec mixin has no buildtime dependencies


COPY . $BUNDLE_DIR
RUN rm -fr $BUNDLE_DIR/.cnab
COPY .cnab /cnab
COPY porter.yaml $BUNDLE_DIR/porter.yaml
WORKDIR $BUNDLE_DIR
CMD ["/cnab/app/run"]

Porter starts the Dockerfile by using a base image. You can customize the base image by specifying a Dockerfile template in the porter.yaml. Next, a set of CA certificates is added. Next, contents of the current directory are copied into /cnab/app/ in the invocation image. This will include any contributions from the mixin executables. Finally, an entry point that conforms to the CNAB specification is added to the image.

Once this is completed, the image is built:

Starting Invocation Image Build =======>
Step 1/9 : FROM debian:stretch
 ---> 5c43e435cc11
Step 2/9 : ARG BUNDLE_DIR
 ---> Using cache
 ---> 7b7947fb2576
Step 3/9 : RUN apt-get update && apt-get install -y ca-certificates
 ---> Using cache
 ---> d60d94e3f701
Step 4/9 : COPY . $BUNDLE_DIR
 ---> 79290bcf128f
Step 5/9 : RUN rm -fr $BUNDLE_DIR/.cnab
 ---> Running in 7f12cd3f447d
 ---> 01b633a31bf8
Step 6/9 : COPY .cnab /cnab
 ---> 25c0b1e5f70a
Step 7/9 : COPY porter.yaml $BUNDLE_DIR/porter.yaml
 ---> dbb26cacf8d8
Step 8/9 : WORKDIR $BUNDLE_DIR
 ---> Running in b051cb2b6ddb
 ---> e10d6ab60595
Step 9/9 : CMD ["/cnab/app/run"]
 ---> Running in 50f1aa7c5b53
 ---> c8e0fc788a0d
Successfully built c8e0fc788a0d
Successfully tagged jeremyrickard/porter-hello-installer:0.1.0

Mixins Help The Build

In the simple example above, the resulting Dockerfile was built entirely by the default porter build functionality. The porter build output reported that the exec mixin did not have any build time dependencies:

# exec mixin has no buildtime dependencies

In many cases, however, mixins will have build time requirements. Next let’s see what happens when we use the Helm mixin. Here is another example porter.yaml:

mixins:
- helm:
    repositories:
      bitnami:
        url: "https://charts.bitnami.com/bitnami"

name: mysql
version: "0.1.0"
registry: jeremyrickard

credentials:
- name: kubeconfig
  path: /root/.kube/config

install:
- helm:
    description: "Install MySQL"
    name: porter-ci-mysql
    chart: bitnami/mysql
    version: "6.14.2"
uninstall:
- helm:
    description: "Uninstall MySQL"
    releases:
    - porter-ci-mysql
    purge: true

When we run porter build on this, the output is different:

$ porter build --verbose
Copying porter runtime ===>
Copying mixins ===>
Copying mixin helm ===>

Generating Dockerfile =======>
FROM debian:stretch

ARG BUNDLE_DIR

RUN apt-get update && apt-get install -y ca-certificates

RUN apt-get update && \
 apt-get install -y curl && \
 curl -o helm.tgz https://get.helm.sh/helm-v2.14.3-linux-amd64.tar.gz && \
 tar -xzf helm.tgz && \
 mv linux-amd64/helm /usr/local/bin && \
 rm helm.tgz
RUN helm init --client-only
RUN apt-get update && \
 apt-get install -y apt-transport-https curl && \
 curl -o kubectl https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl && \
 mv kubectl /usr/local/bin && \
 chmod a+x /usr/local/bin/kubectl

COPY . $BUNDLE_DIR
RUN rm -fr $BUNDLE_DIR/.cnab
COPY .cnab /cnab
COPY porter.yaml $BUNDLE_DIR/porter.yaml
WORKDIR $BUNDLE_DIR
CMD ["/cnab/app/run"]

First, the helm mixin is copied instead of exec mixin. The Dockerfile looks similar in the beginning, but we can then see our next difference. The following lines of our generated Dockerfile were contributed by the helm mixin:

RUN apt-get update && \
 apt-get install -y curl && \
 curl -o helm.tgz https://get.helm.sh/helm-v2.14.3-linux-amd64.tar.gz && \
 tar -xzf helm.tgz && \
 mv linux-amd64/helm /usr/local/bin && \
 rm helm.tgz
RUN helm init --client-only
RUN apt-get update && \
 apt-get install -y apt-transport-https curl && \
 curl -o kubectl https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl && \
 mv kubectl /usr/local/bin && \
 chmod a+x /usr/local/bin/kubectl

How did that happen? To find out, let’s first look at the helm mixin:

~/.porter/mixins/helm/helm
A helm mixin for porter 👩🏽‍✈️

Usage:
  helm [command]

Available Commands:
  build       Generate Dockerfile lines for the bundle invocation image
  help        Help about any command
  install     Execute the install functionality of this mixin
  invoke      Execute the invoke functionality of this mixin
  schema      Print the json schema for the mixin
  uninstall   Execute the uninstall functionality of this mixin
  upgrade     Execute the upgrade functionality of this mixin
  version     Print the mixin version

Flags:
      --debug   Enable debug logging
  -h, --help    help for helm

Use "helm [command] --help" for more information about a command.

The Porter Mixin Contract specifies that mixins must provide a build sub command that generates Dockerfile lines to support the runtime execution of the mixin. In the case of the helm mixin, this includes installing Helm and running a helm init --client-only to prepare the image. At build time, Porter uses the porter.yaml to determine what mixins are required for the bundle. Porter then invokes the build sub-command for each specified mixin and appends that output to the base Dockerfile.

In the end, the result is a single invocation image with all of the necessary pieces: the porter-runtime, selected mixins and any relevant configuration files, scripts, charts or manifests. That invocation image can then be executed by any tool that supports the CNAB spec, while still taking advantage of the Porter capabilities.