CI/CD with ENV-less pipelines

Roelant van der Munnik
NN Tech
Published in
5 min readApr 23, 2022

A wise man once said: “There is only one place where code lives and that is in Production”. And indeed, Production is the only environment where code gets battle-tested. Production is the place where you’ll receive unfiltered feedback and experience unforeseen problems.

This is not to say that environments like Development, Test and/or Acceptance (DTA) are less important, but their only goal is to help us get resilient code into Production as fast as we can. One of the better practices for accomplishing that is ensuring code and environment always act the same. Although the outcome of our code may differ per environment, the execution paths of the code should not differ just because it is running in a different environment. Once you start using if/else statements based on environments (think var ENV), you are setting yourself up for a lot of additional work.

That general principle goes for continuous integration and continuous delivery/deployment (CI/CD) too. When pipelines always execute the same code, regardless of environment, you can be sure that moving through DTA closely resembles Production.

Challenge: var ENV

if [ $ENV == "DEV" ]; 
then
# do something
elif [ $ENV == "TST" ];
then
# do something that is (slightly) different then DEV
elif [ $ENV == "ACC" ];
then
# do something that is (slightly) different then DEV or TST
else
# do something that is (slightly) different then DEV, TST or ACC
fi

Does this look familiar? By branching an execution path based on a variable that embodies the environment (‘the context’) we diverge in each environment from what should happen/happens in Production.

Bear in mind that this example above️ is obvious, and recognising such a construct is easy. But pipeline configurations can get very long and complex, and it is not always clear at a glance that the configuration contains duplicate-but-different execution paths. Even a simple pipeline configuration might obfuscate that we have in fact have dragged the context into the code.

In the example below we also created not one, but four flows. One for each environment. During our DTA staging we never experience the same flow as the one that will run in Production. Don’t Repeat Yourself (DRY) suddenly seems like sound advice! ⬇️

Multiple execution paths (one per environment)

Rationale: var ENV

There are a number of pitfalls when taking the “environmental” (a variable designating the environment) approach. You will need to maintain a number of different flows: one for each environment, and a multitude of variables. The differences might be small — like variables named per environment — but they are indicators that you are repeating yourself and writing/maintaining the same code with slight variations per environment.

${DEVDBINSTANCE} ${TSTDBINSTANCE} ${ACCDBINSTANCE} ${PRDDBINSTANCE}

The fact that all flows reside in one configuration file doesn’t mean you have just one flow to maintain and you will likely encounter challenges when promoting to Production, since you have not tested the exact flow that will end up there.

Tip: A code minimap in your favourite editor might give you a first indication of repetitive patterns in your code.

Okay, so naming variables according to environment, or coding logic according to environment might not be a smart thing to do. Then what?

We still have DTAP to consider and we cannot use the same values for each environment. A compute instance with identifier i-97d76d0f in DEV is unlikely to exist in TST (assuming these environments are separated in one way or another).

Solution: Context, context, context

The solution is as simple as it is eloquent: Make sure the pipeline doesn’t care in which environment it is running. Yes, that’s right! Just let it always execute the same code. Always. And independent of the environment. The outcome will be determined by the context the pipeline is running in.

So…how does that work? Well, it depends on how you designed your environments. Let’s go with an example of four environments (DTAP), and let’s say we used the free IAM boundary provided by an AWS account provides. In other words: We have an AWS account per environment.

Since we already designated an account as Development, everything that runs in there can be considered Development. The context here is the AWS account and there is no need to tell anything in there it is running in Development. So if we were to use an self-hosted Runner (GitLab) or Agent (Azure DevOps Services) we would not need to specify that it runs in Development because that is already established by the account.

Everything the pipelines configuration tells the agent/runner to do will automatically be (in) the context Development. The pipeline just runs and neither the pipeline nor the runner knows - or cares - in which environment it runs.

Notice the absence of any variable denoting environment in the configuration ⬇️

ENV-less pipeline
ENV-less pipeline configuration

We can run in this configuration in every environment we want and never have to worry about it doing different things, the code (execution) always acts the same and it gives us great confidence when moving to Production. We never have to juggle with naming variables for environments and such!

But, but…how?

How does the job gets to run int to the right environment you might ask? This is where Agent pools (Azure DevOps Services) shine. Below we created an Agent pool for each environment.️

Agent pools
Agent pool per environment

Then we created pipeline casings per environment for each configuration, and connect the pipeline casing to an Agent pool.️

Connect pipeline to a specific context

Notice the configuration (file) does not change!

Connect pipeline to a specific context

To round things off, we created a folder structure for our pipelines ⬇️

And there you have it!

This can all be accomplished with standard Azure DevOps functionality, so there’s no need to be going down a rabbit hole for this. Easy does it!

Interested in more about ENV-less CI/CD? Just Respond ⬇️

--

--

Roelant van der Munnik
NN Tech
Writer for

54 68 65 72 65 20 69 73 20 6E 6F 20 73 68 6F 72 74 63 75 74 20 66 6F 72 20 65 78 70 65 72 69 65 6E 63 65 21