Principles and Patterns with AWS CDK Pipelines
In the paper Design Principles and Design Patterns, Robert C Martin discusses the symptoms of a ‘rotting design’. He highlights that one of the key symptoms of a poor design is rigidity; ‘the tendency for software to be difficult to change’.
In this blog, we investigate how principles and programming patterns can be utilised to create a flexible and maintainable AWS CDK Pipeline application.
Tightly Coupled Implementations
Let's start with the snippet below, taken from a pipeline application that consumes a context argument envType.
- When envType is passed with a value of ‘dev’ a pipeline template is created that will deploy to a single AWS account and region.
- When envType is passed with a value of ‘release’, two pipeline stages are used to deploy to multiple AWS accounts and regions.
This may appear simple enough, however, consider the implications of developing this code further. For example:
- Adding further pipeline variations alongside dev and release.
- Releasing different versions of application stacks per CicdStage.
- Using different synth steps for each pipeline
As the dev and release stage implementations are tightly coupled to the pipeline, the application is already rigid; further development becomes an exercise in bolting on more if/else logic to cater for all combinations of pipeline, stage, and application stacks.
The consequences of implementing these changes are that gradually the code becomes messy and fragile, and development tasks take longer and become unnecessarily complex.
“over time as the rotting continues, the ugly festering sores and boils accumulate until they dominate the design of the application. The program becomes a festering mass of code that the developers find increasingly hard to maintain” Robert C. Martin
Dependency inversion
The dependency inversion principle (SOLID) states that an application should be built on abstractions so that implementations can be swapped out, making the application more flexible.
Let’s revisit the sample pipeline application code and abstract our pipeline implementations, starting with a base class.
Next, create subclasses of the base class for both dev and release pipeline types. For example:
As we are extending the base class, we must implement the abstract method addStages()
The template pattern
When a CodePipeline is created in the CDK, there are essentially two key steps, synth and the adding of stages.
Note that in the base class we declare these steps in the generatePipeline()
method and this is known as the template pattern; specifying a set of steps for subclasses to follow, yet allowing behaviour to be altered by overriding methods.
generatePipeline()
can be called from both the dev or test implementation of the base class, they will share the same synth method but execute their respective overriddenaddStages
methods.
Passing interfaces
Next, let's introduce an interface.
And change PipelineDev and PipelineTest to implement PipelineInterface.
This subtle change enables us to refer to the interface and not an implementation; therefore, anything that implements the interface can be used in its place.
The factory pattern
A factory pattern is used to create object instances, based on a provided value. We can extend the work above by introducing a factory pattern to obtain the pipeline type.
Completing the abstraction
To create an instance of pipeline via the PipelineFactory, we now use the following:
Any references to pipeline implementations have been abstracted away. We can use any PipelineInterface implementation to swap in and use, without changing any of the underlying pipeline code.
Creating a new pipeline variation
Now that all our pipeline implementations are abstracted away from the pipeline itself, we are able to swap pipeline implementations. Let’s revisit the code changes required at the beginning of this blog.
How do we add another pipeline variation alongside dev and release?
Simply extend the MyPipelineBase class, and implement addStages() as required. Modify the PipelineFactory to create the instance as required.
How do we implement different versions of application stacks per CicdStage?
Apply the same patterns discussed in this blog to your pipeline stages, using a factory pattern to determine which implementation to use.
How do we use a different synth implementation?
Simply override the base class implementation of synth with your own.
Conclusion
In this blog, we looked at how we can apply principles and patterns to make a fragile and rigid CDK Pipeline application flexible and more easily maintainable.
It is never too early to think about application architecture and maintenance, regardless of how simple the project may be. Rotting designs surface as the result of a gradual process and become harder to fix the longer they are left untreated.
About the Author:
Paul Harwood is a Senior AWS DevOps Engineer here at Version 1.