Chapter 15 explained how well-designed components can make an infrastructure system easier and safer to change. This message supports this book’s theme of using speed of change to continuously improve the quality of a system, and using high quality to enable faster change.
This chapter focuses on modularizing infrastructure stacks; that is, breaking stacks into smaller pieces of code. There are several reasons to consider modularizing a stack:
Put knowledge of how to implement a particular construct into a component so you can reuse it across different stacks.
Create the ability to swap different implementations of a concept, so you have flexibility in building your stacks.
Improve the speed and focus of testing by breaking a stack into pieces that can be tested separately before integrating them. If a component is composable, you can replace them with test doubles (“Using Test Fixtures to Handle Dependencies”) to further improve the isolation and speed of testing.
Share composable, reusable, and well-tested components between teams, so people can build better systems more quickly.
As mentioned in “Stack Components Versus Stacks as Components”, breaking a stack into modules and libraries simplifies the code, but it doesn’t make stack instances any smaller or simpler. Stack components have the potential to make things worse by obscuring the number and complexity of infrastructure resources they add to your stack instance.
So you should be sure you understand what lies beneath the abstractions, libraries, and platforms that you use. These things are a convenience that can let you focus on higher-level tasks. But they shouldn’t be a substitute for fully understanding how your system is implemented.
Chapter 4 describes different types of infrastructure code languages. The two main styles of language for defining stacks are declarative (see “Declarative Infrastructure Languages”) and imperative (see “Programmable, Imperative Infrastructure Languages”). That chapter mentions that these different types of languages are suitable for different types of code (see “Declarative Versus Imperative Languages for Infrastructure”).
These differences often collide with stack components, when people write them using the wrong type of language. Using the wrong type of language usually leads to a mixture of both declarative and imperative languages, which, as explained previously, is a Bad Thing (see “Separate Declarative and Imperative Code”).
The decision of which language to use tends to be driven by which infrastructure stack management tool you’re using, and the languages it supports.1
The patterns defined later in this chapter should encourage you to think about what you’re trying to achieve with a particular stack and its components. To use this to consider the type of language, and potentially the type of stack tool, that you should use, consider two classes of stack component based on language type.
Most stack management tools with declarative languages let you write shared components using the same language. CloudFormation has nested stacks and Terraform has modules. You can pass parameters to these modules, and the languages have at least some programmability (such as the HCL expressions sublanguage for Terraform). But the languages are fundamentally declarative, so nearly all complex logic written in them is barbaric.
So declarative code modules work best for defining infrastructure components that don’t vary very much. A declarative module works well for a facade module (see “Pattern: Facade Module”), which wraps and simplifies a resource provided by the infrastructure platform. These modules get nasty when you use them for more complex cases, creating spaghetti modules (see “Antipattern: Spaghetti Module”).
As mentioned in “Challenge: Tests for Declarative Code Often Have Low Value”, testing a declarative module should be fairly simple. The results of applying a declarative module don’t vary very much, so you don’t need comprehensive test coverage. This doesn’t mean you shouldn’t write tests for these modules. When a module combines multiple declarations to create a more complex entity, you should test that it fulfills its requirement.
Some stack management tools, like Pulumi and the AWS CDK, use general-purpose, imperative languages. You can use these languages to write reusable libraries that you can call from your stack project code. A library can include more complex logic that dynamically provisions infrastructure resources according to how it’s used.
For example, the ShopSpinner team’s infrastructure includes different application server infrastructure stacks. Each of these stacks provisions an application server and networking structures for that application. Some of the applications are public facing, and others are internally facing.
In either case, the infrastructure stack needs to assign an IP address and DNS name to the server and create a networking route from the relevant gateway. The IP address and DNS name will be different for a public-facing application versus an internally facing one. And a public-facing application needs a firewall rule to allow the connection.
The checkout_service stack hosts a public-facing application:
application_networking = new ApplicationServerNetwork(PUBLIC_FACING, "checkout")virtual_machine:name:appserver-checkoutvlan:$(application_networking.address_block)ip_address:$(application_networking.private_ip_address)
The stack code creates an ApplicationServerNetwork object from the
application_networking library, which provisions or references the necessary infrastructure elements:
classApplicationServerNetwork{defvlan;defpublic_ip_address;defprivate_ip_address;defgateway;defdns_hostname;publicApplicationServerNetwork(application_access_type,hostname){if(application_access_type==PUBLIC_FACING){vlan=get_public_vlan()public_ip_address=allocate_public_ip()dns_hostname=PublicDNS.set_host_record("${hostname}.shopspinners.xyz",this.public_ip_address)}else{//SimilarstuffbutforaprivateVLAN}private_ip_address=allocate_ip_from(this.vlan)gateway=get_gateway(this.vlan)create_route(gateway,this.private_ip_address)if(application_access_type==PUBLIC_FACING){create_firewall_rule(ALLOW,'0.0.0.0',this.private_ip_address,443)}}}
This pseudocode assigns the server to a public VLAN that already exists and sets its private IP address from the VLAN’s address range. It also sets a public DNS entry for the server, which in our example will be checkout.shopspinners.xyz. The library finds the gateway based on the VLAN that was used, so this would be different for an internally facing application.
The following set of patterns and antipatterns give ideas for designing stack components and evaluating existing components. It’s not a complete list of ways you should or shouldn’t build modules and libraries; rather, it’s a starting point for thinking about the subject.
Also known as: wrapper module.
A facade module creates a simplified interface to a resource from the stack tool language or the infrastructure platform. The module exposes a few parameters to the calling code (see Example 16-1).
use module:shopspinner-servername:checkout-appservermemory:8GB
The module uses these parameters to call the resource it wraps, and hardcodes values for other parameters needed by the resource (Example 16-2).
declare module:shopspinner-servervirtual_machine:name:${name}source_image:hardened-linux-basememory:${memory}provision:tool:servermakermaker_server:maker.shopspinner.xyzrole:application_servernetwork:vlan:application_zone_vlan
This example module allows the caller to create a virtual server, specifying the name and the amount of memory for the server. Every server created using the module uses a source image, role, and networking defined by the module.
A facade module simplifies and standardizes a common use case for an infrastructure resource. The stack code that uses a facade module should be simpler and easier to read. Improvements to the quality of the module code are rapidly available to all of the stacks that use it.
Facade modules work best for simple use cases, usually involving a basic infrastructure resource.
A facade module limits how you can use the underlying infrastructure resource. Doing this can be useful, simplifying options and standardizing around better and more secure implementations. But it limits flexibility, so won’t apply to every use case.
A module is an extra layer of code between the stack code and the code that directly specifies the infrastructure resources. This extra layer adds at least some overhead to maintaining, debugging, and improving code. It can also make it harder to understand the stack code.
Implementing a facade module generally involves specifying an infrastructure resource with a number of hardcoded values, and a small number of values that are passed through from the code that uses the module. A declarative infrastructure language is appropriate for a facade module.
An obfuscation module is a facade module that doesn’t hide much, adding complexity without adding much value. A bundle module (“Pattern: Bundle Module”) declares multiple related infrastructure resources, so is like a facade module with more parts.
An obfuscation module wraps the code for an infrastructure element defined by the stack language or infrastructure platform, but does not simplify it or add any particular value. In the worst cases, the module complicates the code. See Example 16-3.
use module:any_serverserver_name:checkout-appserverram:8GBsource_image:base_linux_imageprovisioning_tool:servermakerserver_role:application_servervlan:application_zone_vlan
The module itself passes the parameters directly to the stack management tool’s code, as shown in Example 16-4.
declare module:any_servervirtual_machine:name:${server_name}source_image:${origin_server_image}memory:${ram}provision:tool:${provisioning_tool}role:${server_role}network:vlan:${server_vlan}
An obfuscation module may be a facade module (see “Pattern: Facade Module”) gone wrong. Sometimes people write this kind of module aiming to follow the DRY principle (see “Avoid duplication”). They see that code that defines a common infrastructure element, such as a virtual server, load balancer, or security group, is used in multiple places in the codebase. So they create a module that declares that element type once and use that everywhere. But because the elements are being used differently in different parts of the code, they need to expose a large number of parameters in their module.
Other people create obfuscation modules in a quest to design their own language for referring to infrastructure elements, “improving” the one provided by their stack tool.
Nobody intentionally writes an obfuscation module. You may debate whether a given module obfuscates or is a facade, and that debate is useful. You should consider whether a module adds real value and, if not, then refactor it into code that uses the stack language directly.
Writing, using, and maintaining module code rather than directly using the constructs provided by your stack tool adds overhead. It adds more code to maintain, cognitive overhead to learn, and extra moving parts in your build and delivery process. A component should add enough value to make the overhead worthwhile.
If a module neither simplifies the resources it defines nor adds value over the underlying stack language code, consider replacing usages by directly using the stack language.
An obfuscation module is similar to a facade module (see “Pattern: Facade Module”), but doesn’t noticeably simplify the underlying code.
A bundle module declares a collection of related infrastructure resources with a simplified interface. The stack code uses the module to define what it needs to provision:
use module:application_serverservice_name:checkout_serviceapplication_name:checkout_applicationapplication_version:1.23min_cluster:1max_cluster:3ram_required:4GB
The module code declares multiple infrastructure resources, usually centered on a core resource. In Example 16-5, the resource is a server cluster, but also includes a load balancer and DNS entry.
declare module:application_serverserver_cluster:id:"${service_name}-cluster"min_size:${min_cluster}max_size:${max_cluster}each_server_node:source_image:base_linuxmemory:${ram_required}provision:tool:servermakerrole:appserverparameters:app_package:"${checkout_application}-${application_version}.war"app_repository:"repository.shopspinner.xyz"load_balancer:protocol:httpstarget:type:server_clustertarget_id:"${service_name}-cluster"dns_entry:id:"${service_name}-hostname"record_type:"A"hostname:"${service_name}.shopspinner.xyz"ip_address:{$load_balancer.ip_address}
A bundle module is useful to define a cohesive collection of infrastructure resources. It avoids verbose, redundant code. These modules are useful to capture knowledge about the various elements needed and how to wire them together for a common purpose.
A bundle module is suitable when you’re working with a declarative stack language, and when the resources involved don’t vary in different use cases. If you find that you need the module to create different resources or configure them differently depending on the usage, then you should either create separate modules, or else switch to an imperative language and create an infrastructure domain entity (see “Pattern: Infrastructure Domain Entity”).
A bundle module may provision more resources than you need in some situations. Users of the module should understand what it provisions, and avoid using the module if it’s overkill for their use case.
Define the module declaratively, including infrastructure elements that are closely related to the declared purpose.
A facade module (“Pattern: Facade Module”) wraps a single infrastructure resource, while a bundle module includes multiple resources, although both are declarative in nature. An infrastructure domain entity (“Pattern: Infrastructure Domain Entity”) is similar to a bundle module, but dynamically generates infrastructure resources. A spaghetti module is a bundle module that wishes it was a domain entity but descends into madness thanks to the limitations of its declarative language.
A spaghetti module is configurable to the point where it creates significantly different results depending on the parameters given to it. The implementation of the module is messy and difficult to understand, because it has too many moving parts (see Example 16-6).
declare module:application-server-infrastructurevariable:network_segment = {if ${parameter.network_access} = "public"id:public_subnetelse if ${parameter.network_access} = "customer"id:customer_subnetelseid:internal_subnetend}switch ${parameter.application_type}:"java":virtual_machine:origin_image:base_tomcatnetwork_segment:${variable.network_segment}server_configuration:if ${parameter.database} != "none"database_connection:${database_instance.my_database.connection_string}end..."NET":virtual_machine:origin_image:windows_servernetwork_segment:${variable.network_segment}server_configuration:if ${parameter.database} != "none"database_connection:${database_instance.my_database.connection_string}end..."php":container_group:cluster_id:${parameter.container_cluster}container_image:nginx_php_imagenetwork_segment:${variable.network_segment}server_configuration:if ${parameter.database} != "none"database_connection:${database_instance.my_database.connection_string}end...endswitch ${parameter.database}:"mysql":database_instance:my_databasetype:mysql......
This example code assigns the server it creates to one of three different network segments, and optionally creates a database cluster and passes a connection string to the server configuration. In some cases, it creates a group of container instances rather than a virtual server. This module is a bit of a beast.
As with other antipatterns, people create a spaghetti module by accident, often over time. You may create a facade module (“Pattern: Facade Module”) or a bundle module (“Pattern: Bundle Module”), that grows in complexity to handle divergent use cases that seem similar on the surface.
Spaghetti modules often result from trying to implement an infrastructure domain entity (“Pattern: Infrastructure Domain Entity”) using a declarative language.
A module that does too many things is less maintainable than one with a tighter scope. The more things a module does, and the more variations there are in the infrastructure that it can create, the harder it is to change it without breaking something. These modules are harder to test. As I explain in Chapter 8, better-designed code is easier to test, so if you’re struggling to write automated tests and build pipelines to test the module in isolation, it’s a sign that you have a spaghetti module.
A spaghetti module’s code often contains conditionals that apply different specifications in different situations. For example, a database cluster module might take a parameter to choose which database to provision.
When you realize you have a spaghetti module on your hands, you should refactor it. Often, you can split it into different modules, each with a more focused remit. For example, you might decompose your single application infrastructure module into different modules for different parts of the application’s infrastructure. An example of a stack that uses decomposed modules in this way, rather than using the spaghetti module from Example 16-6, might look like Example 16-7.
use module:java-application-serversname:checkout_appserverapplication:"shopping_app"application_version:"4.20"network_segment:customer_subnetserver_configuration:database_connection:${module.mysql-database.outputs.connection_string}use module:mysql-databasecluster_minimum:1cluster_maximum:3allow_connections_from:customer_subnet
Each of the modules is smaller, simpler, and so easier to maintain and test than the original spaghetti module.
A spaghetti module is often an attempt at building an infrastructure domain entity using declarative code. It could also be a facade module (“Pattern: Facade Module”) or a bundle module (“Pattern: Bundle Module”) that people tried to extend to handle different use cases.
A infrastructure domain entity implements a high-level stack component by combining multiple lower-level infrastructure resources. An example of a higher-level concept is the infrastructure needed to run an application.
This example shows how a library that implements a Java application infrastructure instance might be used from stack project code:
use module:application_serverservice_name:checkout_serviceapplication_name:checkout_applicationapplication_version:1.23traffic_level:medium
The code defines the application and version to deploy, and also a traffic level. The domain entity library code could look similar to the bundle module example (Example 16-5), but includes dynamic code to provision resources according to the traffic_level parameter:
...switch(${traffic_level}){case("high"){$appserver_cluster.min_size=3$appserver_cluster.max_size=9}case("medium"){$appserver_cluster.min_size=2$appserver_cluster.max_size=5}case("low"){$appserver_cluster.min_size=1$appserver_cluster.max_size=2}}...
A domain entity is often part of an abstraction layer (see “Building an Abstraction Layer”) that people can use to define and build infrastructure based on higher-level requirements. An infrastructure platform team builds components that other teams can use to assemble stacks.
Because an infrastructure domain entity dynamically provisions infrastructure resources, it should be written in an imperative language rather than a declarative one. See “Declarative Versus Imperative Languages for Infrastructure” for more on why.
On a concrete level, implementing an infrastructure domain entity is a matter of writing the code. But the best way to create a high-quality codebase that is easy for people to learn and maintain is to take a design-led approach.
I recommend drawing from lessons learned in software architecture and design. The infrastructure domain entity pattern derives from Domain Driven Design (DDD), which creates a conceptual model for the business domain of a software system, and uses that to drive the design of the system itself.3 Infrastructure, especially one designed and built as software, should be seen as a domain in its own right. The domain is building, delivering, and running software.
A particularly powerful approach is for an organization to use DDD to design the architecture for the business software, and then extend the domain to include the systems and services used for building and running that software.
A bundle module (see “Pattern: Bundle Module”) is similar to a domain entity in that it creates a cohesive collection of infrastructure resources. But a bundle module normally creates a fairly static set of resources, without much variation. The mindset of a bundle module is usually bottom-up, starting with the infrastructure resources to create. A domain entity is a top-down approach, starting with what’s required for the use case.
Most spaghetti modules (see “Antipattern: Spaghetti Module”) for infrastructure stacks are a result of pushing declarative code to implement dynamic logic. But sometimes an infrastructure domain entity becomes overly complicated. A domain entity with poor cohesion becomes a spaghetti module.
An abstraction layer provides a simplified interface to lower-level resources. A set of reusable, composable stack components can act as an abstraction layer for infrastructure resources. Components can implement the knowledge of how to assemble low-level resources exposed by the infrastructure platform into entities that are useful for people focused on higher-level tasks.
For example, an application team may need to define an environment that includes an application server, database instance, and access to message queues. The team can use components that abstract the details of assembling rules for routing and infrastructure resource permissions.
Components can be useful even for a team that has the skills and experience to implement the low-level resources. An abstraction helps to separate different concerns, so that people can focus on the problem at a particular level of detail. They should also be able to drill down to understand and potentially improve or extend the underlying components as needed.
You might be able to implement an abstraction layer for some systems using more static components like facade modules (see “Pattern: Facade Module”) or bundle modules (see “Pattern: Bundle Module”). But more often you need the components of the layer to be more flexible, so dynamic components like infrastructure domain entities (see “Pattern: Infrastructure Domain Entity”) are more useful.
An abstraction layer might emerge organically as people build libraries and other components. But it’s useful to have a higher-level design and standards so that the components of the layer work well together and fit into a cohesive view of the system.
The components of an abstraction layer are normally built using a low-level infrastructure language (“Low-Level Infrastructure Languages”). Many teams find it useful to build a higher-level language (“High-Level Infrastructure Languages”) for defining stacks with their abstraction layer. The result is often a higher-level, declarative language that specifies the requirements for part of the application runtime environment, which calls to dynamic components written in a low-level, imperative language.
The Open Application Model is an example of an attempt to define a standard architecture that decouples application, runtime, and infrastructure.
Building stacks from components can be useful when you have multiple people and teams working on and using infrastructure. But be wary of the complexity that comes with abstraction layers and libraries of components, and be sure to tailor your use of these constructs to match the size and complexity of your system.
1 As I write this, in mid-2020, tool vendors are rapidly evolving their strategies around types of languages. I’m hopeful that the next few years will see a maturing of stack componentization.
2 The rule of three was defined in Robert Glass’s book, Facts and Fallacies of Software Engineering (Addison-Wesley). Jeff Atwood also commented on the rule of three in his post on the delusion of reuse.
3 See Domain-Driven Design: Tackling Complexity in the Heart of Software, by Eric Evans (Addison-Wesley).