6 tips for maintainable pipelines

Or, good design for minimizing technical debt

By applying these 6 techniques to Continuous Integration (CI) and Continuous Deployment (CD) pipeline code you can improve the ability of the code to adapt to the emerging needs of your organization.

πŸ€” Maintain all pipeline configuration and code in the same repo as the source code it is working with.πŸ‘ˆ

This might seem "obvious" to someone who has only used Gitlab-CI or Github Actions where that is your only choice, but not so much if you have experience with Jenkins, Bamboo or Azure DevOps Pipelines. A pipeline, source code union is critical if you want the potential for re-running a pipeline at any point in commit history.

πŸ˜– Keep pipeline code simple and small - a "shim". πŸ‘ˆ

Pipeline code should just be managing the flow of data and artifacts through the pipeline and thus the pipeline build environment. Hand-off the "heavy lifting" of the pipeline to a more general purpose language - Bash/Powershell at a pinch if what you are doing is really simple, preferably Python, Golang or something specific to your target application.

🫣 Align your release management workflow and release identity patterns with your branching/merging strategies. πŸ‘ˆ

The important point here is to ensure that a pipeline will always generate a uniquely identified artifact no matter what release status the build has. The only place for a potentially ambiguous artifact should be a custom developer build in their own environment (nominally, for their own consumption).

😳 Use containers as the basis for your pipeline runtime environment.πŸ‘ˆ

This will give you dependency control in relation to the target application of your pipeline and portability across the infrastructure you are running on.

πŸ‘‰ Have a single execution path in pipeline code across release states. πŸ‘ˆ

In practice it will almost always be necessary to have some release state dependent conditional logic, so you absolutely want to minimize the differences embodied by these code paths.

Otherwise you'll run into "oops, we ran the release pipeline and it failed because we had made a change pre-release and forgot to account for how it affected the release pipeline". Sound familiar? πŸ€”

Typically this means deferring any conditional logic to a full language where you can implement unit tests to always verify pre and post release logic in the code. If you were paying attention to the β€œshim” point above, this should also sound a little familiar... 😳

πŸ‘‰ Make pipeline tasks idempotent. πŸ‘ˆ

From a CI perspective this relates to the very first tip - you want to be able to run a pipeline multiple times and achieve the same outcome and particularly not to fail. For Docker containers and applications this includes things such as pinning dependency versions - preferably including transitive dependencies (dependencies of dependencies) if such a thing exists for your language of choice.

For example the poetry package manager in Python has the excellent poetry.lock file where it manages and pins transitive dependencies somewhat automatically.

One exception to this rule is for uploading or publishing packages to an artifact service. The artifact service needs packages to be write once - meaning that once a package of a particular release state has been published we do not want that package to be silently replaced with a new package.

For a CD pipeline the importance of idempotence is that it achieves a weak form of rollback. Since a pipeline run β€œdeclares” the infrastructure to be in a particular state, reverting to an older commit simply declares the infrastructure to be in that older state, noting of course, the potential subtleties involving databases and other types of storage resources that may need special attention for such a rollback.

So there it is. 6 ways to establish maintainable pipeline code:

  • Maintain all pipeline configuration and code in the same repo as the source code it is working with.

  • Keep pipeline code simple and small - a "shim".

  • Align your release management workflow and release identity patterns with your branching/merging strategies.

  • Use containers as the basis for your pipeline runtime environment.

  • Have a single execution path in pipeline code across release states.

  • Make pipeline tasks idempotent.

Reply

or to participate.