In this post, I want to share some more details on the design of meshStack’s exciting new building block feature. To provide some context, please take a look at the previous posts in this series:
When implementing anything beyond a simple Landing Zone, the challenge of composing multiple building blocks arises. Here are some common examples of such compositions:
- A VNet/VPC building block that provides on-premise connectivity and a firewall rule building block that allows application teams to specify specific traffic flows.
- A Bastion Host that provides secure access to a VNet or VPC created by another building block.
- A GitHub repository connected to an optional CI/CD Platform forming a managed DevOps Toolchain.
To create an intuitive yet capable composition mechanism, we extensively studied other examples of composition mechanisms that platform engineers are already familiar with, such as Terraform modules or package managers like npm or pip.
Building Block Composition in meshStack
Building block composition differs in several important ways from the generic „infrastructure as code“ composition performed by application teams using tools like Terraform or Pulumi. Landing zone level building blocks typically represent a hard responsibility barrier. The platform engineers of the cloud foundation team are responsible for defining and providing the implementation of the building blocks, while application teams consume them through a well-defined interface.
This means that platform engineers control the deployment of building blocks, providing an avenue to easily integrate manual workflows, like approvals, as well as performing privileged operations in a safe and controlled manner, such as adding networking peerings to an on-premise connectivity hub. This is in contrast to the typical CI/CD pipeline of application teams, which deploys infrastructure that the application team fully controls.
Another aspect is that generic infrastructure as code tools focus on composition at the level of individual cloud resources. Tools like Terraform solve the composition between an infinite set of possible cloud resources (provided by providers) with an equally infinite set of possible attributes. This requires a strong type system supporting primitive types, lists, and objects, as well as a capable configuration language to express dependencies between resources. This goes as far as providing a rich library of transformation functions between resource input and output values.
On the other hand, building blocks encapsulate resources into a reasonably small set of higher-level abstractions meant to be composed together in explicitly constrained ways, forming a „golden path“ for application teams. This means that application teams should be able to ignore technical details of the composition as much as possible, while platform engineers can typically exercise explicit control on both ends of a building block dependency.
Building Block Dependencies
A dependency is the key composition primitive between two building blocks. When building block B
depends on A
, it means:
- Application teams cannot provision
B
withoutA
. For example, you cannot provision a firewall rule without first provisioning a VNet. - Platform engineers can instruct
B
to consume inputs sourced from outputs ofA
. For example, passing a VNet ID between blocks.
When
building landing zones, platform engineers usually control both sides of a building block dependency. One important early design decision we made is that input/output allows no transformations when passing values. This makes dependencies very easy to reason about when platform engineers define building blocks (definition time) and when application teams consume them (consumption time). In rare cases where a dependency use case requires transformations of input and output values between blocks, platform engineers can easily model them with more capable tools like Terraform in building block implementations.
Cardinality
One interesting design detail we considered is whether dependencies should support other cardinalities than 1:1 dependencies. After studying the problem space, we have decided to support named dependencies for now to offer 1:x (where x is known) dependencies. An example use case is a Transit Peering building block that implements a Tenant to Tenant transit network, which depends on two VNet building blocks. Explicitly naming dependencies allows addressing outputs individually (e.g., src.id
and dest.id
) and also gives platform engineers the opportunity to choose meaningful names and descriptions for dependencies.
We also considered optional dependencies, which can provide flexibility to application teams at consumption time. For example, an application team can add a GitHub repository building block that can optionally connect to their cloud tenant to enable deployment via GitHub actions. We plan to consider this scenario in the future with a more general concept of optional inputs.
Supporting true 1:m dependencies (e.g., connecting a GitHub repository to an arbitrary number of cloud tenants) might be useful in some situations but adds a lot of complexity to the design. Therefore, we have decided to leave this out of scope for now.
Building Block Versioning
We paid a lot of attention to ensuring that dependencies work well with versioning. We anticipate that platform engineers will need to update their building blocks from time to time. This can be as simple as updating an internal aspect of the building block’s implementation or as complex as adding or removing inputs/outputs. Dependency management with versioning is a challenging problem. Our primary assumption is that platform engineers want to establish a single „golden path“ for application teams, meaning that they explicitly define and validate dependencies at definition time. In other words, dependencies between „latest“ versions of building blocks should always result in a sound composition. This has a couple of interesting consequences for our design.
First and foremost, meshStack can validate dependencies between building blocks at definition time. This will be very valuable to cloud foundation teams that want to safely make changes to individual blocks without breaking compositions.
Second, we can simplify the behavior at consumption time by modeling dependencies as an „API compatibility“ problem, rather than requiring platform engineers to explicitly define versioning constraints as package managers do. In simple terms, we define building block dependencies at the level of a building block definition (e.g., FirewallRule
depends on VNet
) as a mapping between inputs and outputs (e.g., VNet.id → FirewallRule.srcNetId
). As long as that „API“ between the two block definitions is not broken, the resulting composition is sound at definition time. At consumption time, meshStack already handles missing building block inputs, which can occur when the building block definition calls for manual operator input or in failure modes like the VNet
deployment failing or the VNet
deployment not producing the required VNet.id
output because the VNet
block is deployed according to an outdated version that did not yet have this output.
Composing Building Blocks with meshObjects
meshStack also enables the composition of building blocks with existing meshObjects, such as a meshTenant. For example, platform engineers can easily add an Azure Subscription ID as an input to a VNet
building block. We will be adding more capabilities to this feature soon, allowing you to easily consume custom tags.
Our long-term vision is to expose building blocks via the meshObject API to a Terraform provider, creating a sort of inception. This will solve several interesting use cases, such as consuming building block output values in the IaC code that an application team uses to deploy their apps. However, that’s a topic for another time. Until then, have fun building (blocks)!