Skip to content

Global App Deployment

This example demonstrates how to use ConfigHub to manage a typical micro-service application deployed in different variants for testing, staging, and production across multiple regions.

First go through the setup steps to get things ready. Then move on to the scenario tasks:

Scenario

The application has 3 micro-services components:

  • React frontend
  • Go backend
  • Postgres database

In this example, the app is deployed in the following environments:

  • QA
  • Staging
  • Prod

where staging and prod each is deployed in 3 regions: US, EU and Asia. This adds up to a total of 7 live environments with 7 associated configs plus some base config.

ConfigHub Layout

The example is laid out as an app part and an infrastructure part. The purpose is to illustrate how the app config meshes with infrastructure config for each specific environment. A kubernetes namespace is used to simulate this infrastructure config. In reality this would be a whole different kube cluster in each environment along with other infrastructure resources and data.

The app hierarchy uses spaces for each layer because this allows different regions to be governed individually. E.g. each region can have its own line-of-business IT team own their own app. This is of course not required. It could also be laid out in a single space if desired.

The infra config on the other hand is laid out in a single infra space. The idea is that this is all governed by a single platform or infrastructure team.

The following diagram shows the app space hierarchy:

graph LR
base --> QA
QA --> us-staging
QA --> eu-staging
QA --> asia-staging
us-staging --> us-prod
eu-staging --> eu-prod
asia-staging --> asia-prod

This diagram shows the infra config:

%%{config: {'flowchart': {'curve': 'linear'}}}%%
graph LR
infra --> nginx-base
nginx-base --> nginx
infra --> ns-base
ns-base --> ns-qa
ns-base --> ns-us-staging
ns-base --> ns-us-prod
ns-base --> ns-eu-staging
ns-base --> ns-eu-prod
ns-base --> ns-asia-staging
ns-base --> ns-asia-prod

The infrastructure doesn't have the same need as the app to flow from staging to prod. So all environments are downstream from the base config. If the platform team prefers to flow from staging to prod instead, that is a simple design change.

The purpose of base units

Take the nginx ingress controller as an example, there is both an nginx-base and an nginx unit. Why is that? This layout takes advantage of ConfigHub's clone upgrade feature to combine external updates to ingress-nginx with local changes.

For example, a new version of ingress-nginx can be installed by updating the base unit with the new yaml. The diff from the old version can be inspected for important changes. Then the downstream nginx unit can be upgraded with the new changes using clone upgrade. This merges the new changes to the downstream without clobbering any local changes that were made in the downstream.

Setup

Prerequisites

Make sure you have completed the prerequisites, then get the scripts and files ready with:

git clone https://github.com/confighub/examples.git
cd examples/global-app

Configure ConfigHub

First, set up units, spaces and other stuff in ConfigHub with:

bin/install-base

This will:

  • Create a unique project name which will be used as space prefix for this example to prevent name collisions in your org
  • Create spaces for base units, filters and for infra units
  • Create app base units and load config into the base space from ./baseconfig
  • Create infra base units and load config into the infra space from ./baseconfig

Next, set up all the individual environments:

bin/install-envs

This defines the environment hierarchy and sets everything up accordingly in ConfigHub. It's a good idea to read the source of this script and the scripts being called from there to understand what is going on.

To check out what is in ConfigHub now, you can run:

cub unit tree --node=space --filter $(bin/pre)/app --space '*'

which should print something like:

NODE                                     UNIT        STATUS    UPGRADE-NEEDED    UNAPPLIED-CHANGES    APPLY-GATES
└── chubby-paws-base                     backend     NoLive                                           None
    └── chubby-paws-qa                   backend     NoLive    No                                     None
        ├── chubby-paws-us-staging       backend     NoLive    No                                     None
        │   └── chubby-paws-us-prod      backend     NoLive    No                                     None
        ├── chubby-paws-eu-staging       backend     NoLive    No                                     None
        │   └── chubby-paws-eu-prod      backend     NoLive    No                                     None
        └── chubby-paws-asia-staging     backend     NoLive    No                                     None
            └── chubby-paws-asia-prod    backend     NoLive    No                                     None
└── chubby-paws-base                     frontend    NoLive                                           None
    └── chubby-paws-qa                   frontend    NoLive    No                                     None
        ├── chubby-paws-us-staging       frontend    NoLive    No                                     None
        │   └── chubby-paws-us-prod      frontend    NoLive    No                                     None
        ├── chubby-paws-eu-staging       frontend    NoLive    No                                     None
        │   └── chubby-paws-eu-prod      frontend    NoLive    No                                     None
        └── chubby-paws-asia-staging     frontend    NoLive    No                                     None
            └── chubby-paws-asia-prod    frontend    NoLive    No                                     None
└── chubby-paws-base                     postgres    NoLive                                           None
    └── chubby-paws-qa                   postgres    NoLive    No                                     None
        ├── chubby-paws-us-staging       postgres    NoLive    No                                     None
        │   └── chubby-paws-us-prod      postgres    NoLive    No                                     None
        ├── chubby-paws-eu-staging       postgres    NoLive    No                                     None
        │   └── chubby-paws-eu-prod      postgres    NoLive    No                                     None
        └── chubby-paws-asia-staging     postgres    NoLive    No                                     None
            └── chubby-paws-asia-prod    postgres    NoLive    No                                     None

You can see the infra config with:

cub unit tree --filter $(bin/pre)/infra --space "*"

which will print something like:

NODE                       SPACE                STATUS    UPGRADE-NEEDED    UNAPPLIED-CHANGES    APPLY-GATES
└── ns-base                chubby-paws-infra    NoLive                                           None
    ├── ns-asia-prod       chubby-paws-infra    NoLive    No                                     None
    ├── ns-asia-staging    chubby-paws-infra    NoLive    No                                     None
    ├── ns-us-prod         chubby-paws-infra    NoLive    No                                     None
    ├── ns-qa              chubby-paws-infra    NoLive    No                                     None
    ├── ns-us-staging      chubby-paws-infra    NoLive    No                                     None
    ├── ns-eu-staging      chubby-paws-infra    NoLive    No                                     None
    └── ns-eu-prod         chubby-paws-infra    NoLive    No                                     None
└── nginx-base             chubby-paws-infra    NoLive                                           None
    └── nginx              chubby-paws-infra    NoLive    No                                     None

Note that since all the infra is in a single space, you can also do:

cub unit tree --space $(bin/pre)-infra

Start a cluster

Start and configure a Kind cluster with:

bin/create-cluster

After it completes, you will notice that all units except the base units now have a cluster target:

cub unit list --space '*' --filter $(bin/pre)/all --columns Name,Space.Slug,Target.Slug
NAME               SPACE                       TARGET
backend            chubby-paws-base
frontend           chubby-paws-base
nginx              chubby-paws-base
frontend           chubby-paws-qa              cluster-target
postgres           chubby-paws-base
ns-base            chubby-paws-base
nginx              chubby-paws-infra           cluster-target
backend            chubby-paws-qa              cluster-target
backend            chubby-paws-eu-staging      cluster-target
frontend           chubby-paws-us-staging      cluster-target
...(truncated)

Apply Infrastructure

Apply the infrastructure units first:

cub unit apply --space $(bin/pre)-infra --where "Labels.targetable = 'true'"

Apply apps

Start with the QA environment just to check that things are working:

cub unit apply --space $(bin/pre)-qa

Once all units are ready, see if you can access the app at http://qa.local.cubby.bz:8080. This is a DNS record pointing to 127.0.0.1, so your browser will connect to localhost on a port used by Kind which will forward to nginx ingress which will forward to the app.

The apply command may report a failed apply. Check the final status with:

cub unit list --space $(bin/pre)-qa

Once you have confirmed that it works, you can apply other environments in a similar way or you can apply all apps with:

cub unit apply --where "Space.Labels.prefix = '$(bin/pre)' AND Labels.targetable = 'true'" --space '*' --wait=false

Scenario Tasks

Now that we have everything set up, we can explore various common tasks for this scenario. Don't worry if you did not apply all the units before. You can always do it prior to or during a task.

Roll out a new version

This is probably the most common operational task. Every time a new software build has been produced and passed tests, it is rolled out by updating the container image tag reference in the various Kubernetes manifests.

Bump QA

Let's start by bumping the version of frontend and backend apps in the QA environment:

cub run set-image-reference --container-name frontend --image-reference :1.1.8 --space $(bin/pre)-qa
cub run set-image-reference --container-name backend  --image-reference :1.1.8 --space $(bin/pre)-qa

then apply:

cub unit apply --space $(bin/pre)-qa

Hint: You can perform these 3 commands using the bin/set-version script.

Once the apply has completed, go check out http://qa.local.cubby.bz:8080. You should see some visual difference that were introduced in the latest version.

Promote to next environment

Next, let's check the upgrade status of other environments:

cub unit tree --node=space --filter $(bin/pre)/app --space "*" --columns Space.Slug,UpgradeNeeded,UnappliedChanges

You should see something like this:

NODE                                     UNIT        SPACE                       UPGRADE-NEEDED    UNAPPLIED-CHANGES
└── chubby-paws-base                     backend     chubby-paws-base
    └── chubby-paws-qa                   backend     chubby-paws-qa              No
        ├── chubby-paws-asia-staging     backend     chubby-paws-asia-staging    Yes
        │   └── chubby-paws-asia-prod    backend     chubby-paws-asia-prod       No
        ├── chubby-paws-eu-staging       backend     chubby-paws-eu-staging      Yes
        │   └── chubby-paws-eu-prod      backend     chubby-paws-eu-prod         No
        └── chubby-paws-us-staging       backend     chubby-paws-us-staging      Yes
            └── chubby-paws-us-prod      backend     chubby-paws-us-prod         No
└── chubby-paws-base                     frontend    chubby-paws-base
    └── chubby-paws-qa                   frontend    chubby-paws-qa              No
        ├── chubby-paws-asia-staging     frontend    chubby-paws-asia-staging    Yes
        │   └── chubby-paws-asia-prod    frontend    chubby-paws-asia-prod       No
        ├── chubby-paws-eu-staging       frontend    chubby-paws-eu-staging      Yes
        │   └── chubby-paws-eu-prod      frontend    chubby-paws-eu-prod         No
        └── chubby-paws-us-staging       frontend    chubby-paws-us-staging      Yes
            └── chubby-paws-us-prod      frontend    chubby-paws-us-prod         No
└── chubby-paws-base                     postgres    chubby-paws-base
    └── chubby-paws-qa                   postgres    chubby-paws-qa              No
        ├── chubby-paws-asia-staging     postgres    chubby-paws-asia-staging    No
        │   └── chubby-paws-asia-prod    postgres    chubby-paws-asia-prod       No
        ├── chubby-paws-eu-staging       postgres    chubby-paws-eu-staging      No
        │   └── chubby-paws-eu-prod      postgres    chubby-paws-eu-prod         No
        └── chubby-paws-asia-staging     postgres    chubby-paws-asia-staging    No
            └── chubby-paws-asia-prod    postgres    chubby-paws-asia-prod       No

Units directly downstream of the QA units we just changed are now marked for upgrade. Let's dry-run a promotion to US staging:

cub unit update --dry-run --patch --upgrade --space $(bin/pre)-us-staging

This will dry-run the promotion so you can review the changes before actually performing it in ConfigHub. Assuming you are satisfied, you run the same command without dry-run:

cub unit update --patch --upgrade --space $(bin/pre)-us-staging

Now if you run the cub unit tree command again from above, you will see that US staging no longer needs an upgrade. You will also see that it has unapplied changes because only the config has changed so far. Finally, you will notice that US prod now needs and upgrade because it is directly downstream of US staging which just changed. Let's apply everything in US staging:

cub unit apply --space $(bin/pre)-us-staging

Once it completes, go check out http://us-staging.local.cubby.bz:8080 and you will see the same nice enhancements rolled out to this environment.

Promote to everywhere

Once you have confidence in a new version, you can promote it everywhere in a few steps. First upgrade everything:

cub unit update --patch --upgrade --filter $(bin/pre)/app --space "*"

You may need to run this twice because there are two layers in the clone tree. Note that you would almost never do it this way. Instead you would continue to use the commands above to upgrade one environment at a time and then apply. But this demonstrates how ConfigHub makes it easy for you to perform operations in bulk when needed.

Once all units have been upgraded, apply all changes with:

cub unit apply --where "Labels.targetable = 'true' AND Labels.type = 'app'" --space "*" --wait=false

Set up a new environment

A common task is to set up a new environment. It can be a new dev environment or a test environment dedicated to a specific kind of testing, or it can be a new production environment, e.g. in a new region.

To set up a new environment, you have to set up both the infrastructure and the app. The infrastructure is mimiced simplistically in with just a namespace in this example. Let's set up the infrastructure for a performance test:

bin/new-infra perftest

Now let's set up the app:

bin/new-app-env perftest qa perftest performance-testing dev

This clones the qa environment into a new perftest environment and associates it with the perftest infrastructure. The last two arguments are used to set the role and region parameters. They serve as an example of environment specific configuration that varies between environments.

Next, apply the configuration:

cub unit apply --space $(bin/pre)-perftest

After successful apply, you can reach it in the browser or check the config with curl:

curl http://perftest.local.cubby.bz:8080/api/config

Lateral promotion

Previously, we bumped the version number in the QA environment and then promoted the change via clone upgrade to its downstreams. Another common approach is to make a change to one environment and then promote this change "laterally" to another environment and gradually roll out to all environments using lateral promotion. Finally when the change is fully rolled out, the upstream is updated.

This approach is more conservative and offers even better control over when a change is promoted to a specific environment because there is no risk of someone accidentally upgrading an environment from an upstream without knowing that the upstream has been updated by someone else.

Let's update the CPU request on the backend in US staging and after reviewing the change, we promote it directly to EU staging:

cub run set-container-resources \
        --container-name backend \
        --cpu 100m \
        --memory "" \
        --operation floor \
        --limit-factor 0 \
        --space $(bin/pre)-us-staging

Then apply:

cub unit apply backend --space $(bin/pre)-us-staging

Once applied, you can verify the pod is running with the new resource request.

You can verify what changed in the config by getting a revision list:

cub revision list backend --space $(bin/pre)-us-staging

and diff from the revision prior to head, e.g. if that revision is 5:

cub unit diff -u backend --space $(bin/pre)-us-staging --from=5

Now that you have confirmed that the change works and looks good, you can promote it to EU staging (remember to update merge base and end revision numbers to match what you have):

cub unit update backend \
        --space $(bin/pre)-eu-staging \
        --merge-unit $(bin/pre)-us-staging/backend \
        --merge-base=5 \
        --merge-end=6

Check the revision list for EU staging backend:

cub revision list backend --space $(bin/pre)-eu-staging

and check the diff of the two last revisions, e.g. revision 4 to 5:

cub unit diff -u backend --space $(bin/pre)-eu-staging --from=4

You should see that the CPU request has been updated and now that everything looks good, you can apply EU staging:

cub unit apply --space $(bin/pre)-eu-staging

and once it completes, you can verify the change is live by checking the pod resources in the eu-staging namespace

Change multiple environments at once

While bulk changes to multiple environments are more risky than phased rollouts, there are valid reasons for making bulk changes. It can for example be a trivial and well-tested change that is deemed safe to roll out all at once and save the hassle of doing it one environment at a time. Or it can be an urgent security patch or an urgent rollback where time is of the essence and the risk can be tolerated.

Before continuing, make sure that you have already applied all staging environments by running:

cub unit apply \
        --space "*" \
        --filter $(bin/pre)/all \
        --where "Labels.role = 'staging'"

In this task, we'll change the app's title in all staging environments. The title is set as an environment variable in the backend unit. First we list the units, to verify that the filter is correct and we have the right set of units:

cub unit list \
        --filter $(bin/pre)/all \
        --where "Slug = 'backend' AND Labels.role = 'staging'" \
        --space "*" \
        --columns Name,Space.Slug,HeadRevisionNum

which prints something like this:

NAME       SPACE                      HEAD-REVISION-NUM
backend    chubby-paws-asia-staging   9
backend    chubby-paws-eu-staging     9
backend    chubby-paws-us-staging     10

These are the 3 correct units. Now we can perform the config change with a function:

cub run set-env-var \
        --space "*" \
        --container-name backend \
        --env-var CHAT_TITLE \
        --env-value "Updated AI Chat" \
        --where "Slug = 'backend' AND Labels.role = 'staging'"

Next, you can run the list command from above again and verify that the head revision number was incremented for each unit. You can also run a cub unit diff to inspect the changes before moving forward. Once you are satisfied, you can apply the changes with:

cub unit apply \
        --space "*" \
        --where "Slug = 'backend' AND Labels.role = 'staging'"

And that's it. It is not until this last apply command that the live systems are touched. This gives you plenty of time and opportunity to ensure that the update is correct. While it may look similar, this approach is different from the clone upgrade we performed previously. In this case, we can select an arbitrary set of units to perform an operation on as opposed to just downstream units.

Use changesets

Sometimes you need to make several changes across many units and you don't want someone else to introduce changes in the middle of this process. Changesets help you do this.

Let's say you want to bump the memory and set an environment variable at the same time in all staging environments. You want to ensure other changes are not introduced in between these two changes.

Start by creating a changeset:

cub changeset create --space $(bin/pre) memory-change --description "Bump memory"

Next, associate the changeset with the units we will be changing which is all the backend units in staging environments:

cub unit update --patch --space "*" \
        --changeset $(bin/pre)/memory-change \
        --filter $(bin/pre)/all \
        --where "Slug = 'backend' AND Labels.role='staging'"

When a unit is associated with a changeset, all changes must belong to that changeset. This prevents other users or automations from introducing new revisions. Now perform the changes:

cub run set-env-var \
        --space "*" \
        --changeset $(bin/pre)/memory-change \
        --container-name backend \
        --env-var LARGE_MEMORY \
        --env-value "true" \
        --where "Slug = 'backend' AND Labels.role = 'staging'"

and:

cub run set-container-resources \
        --space "*" \
        --changeset $(bin/pre)/memory-change \
        --container-name backend \
        --memory 1Gi \
        --cpu "" \
        --operation floor \
        --limit-factor 0 \
        --where "Slug = 'backend' AND Labels.role = 'staging'"

To confirm that changesets prevent unintended changes, try to make a change without providing a changeset:

cub run set-env-var \
        --space "*" \
        --container-name backend \
        --env-var testvar \
        --env-value "testvalue" \
        --where "Slug = 'backend' AND Labels.role = 'staging'"

This should fail because the change is not part of the active changeset.

Now that you have made your changes, you can remove the changeset from the units:

cub unit update \
        --patch \
        --changeset - \
        --filter $(bin/pre)/all \
        --space "*" \
        --where "Slug = 'backend' AND Labels.role='staging'"

This "unlocks" the units so they can be changed again. Now you can apply the changeset by referencing it in the revision flag on the apply command:

cub unit apply \
        --filter $(bin/pre)/all \
        --where "Slug = 'backend' AND Labels.role = 'staging'" \
        --revision "ChangeSet:$(bin/pre)/memory-change"
        --space "*"

Note that you can safely unlock the units before performing the apply because when you perform the apply, you reference the specific revision tag produced by the changeset. If a new revision is created just before this command is executed it will create a new head revision but it will not affect what is being applied.