Component Inheritance
Component Inheritance is one of the principles of Component-Oriented Programming (COP) supported by Atmos.
Component Inheritance is the ability to combine multiple configurations through ordered deep-merging of configurations. The concept is borrowed from Object-Oriented Programming to logically organize complex configurations in a way that makes conceptual sense. The side effect of this are extremely DRY and reusable configurations.
In Object-Oriented Programming (OOP), Inheritance is the mechanism of basing an object or class upon another object (prototype-based inheritance) or class (class-based inheritance), retaining similar implementation.
Similarly, in Atmos, Component Inheritance is the mechanism of deriving a component from one or more base components, inheriting all the properties of the base component(s) and overriding only some fields specific to the derived component. The derived component acquires all the properties of the "parent" component(s), allowing creating very DRY configurations that are built upon existing components.
Component Inheritance is implemented and used in Atmos by combining two features: import
and metadata component's configuration section.
- Base Component is an Atmos component from which other Atmos components inherit all the configuration properties
- Derived Component is an Atmos component which derives the configuration properties from other Atmos components
Single Inheritance
Single Inheritance is used when an Atmos component inherits from another base Atmos component.
Let's say we want to provision two VPCs with different settings into the same AWS account.
In the stacks/catalog/vpc.yaml file, add the following config for the VPC component:
components:
terraform:
vpc-defaults:
metadata:
# Setting `metadata.type: abstract` makes the component `abstract`,
# explicitly prohibiting the component from being deployed.
# `atmos terraform apply` will fail with an error.
# If `metadata.type` attribute is not specified, it defaults to `real`.
# `real` components can be provisioned by `atmos` and CI/CD like Spacelift and Atlantis.
type: abstract
# Default variables, which will be inherited and can be overriden in the derived components
vars:
public_subnets_enabled: false
nat_gateway_enabled: false
nat_instance_enabled: false
max_subnet_count: 3
vpc_flow_logs_enabled: true
In the configuration above, the following Component-Oriented Programming concepts are implemented:
- Abstract Components:
atmoscomponentvpc-defaultsis marked as abstract inmetadata.type. This makes the component non-deployable, and it can be used only as a base for other components that inherit from it - Dynamic Polymorphism: All the variables in the
varssection become the default values for the derived components. This provides the ability to override and use the base component properties in the derived components to provision the same Terraform configuration many times but with different settings
In the stacks/ue2-dev.yaml stack config file, add the following config for the derived VPC components in the ue2-dev stack:
# Import the base component configuration from the `catalog`.
# `import` supports POSIX-style Globs for file names/paths (double-star `**` is supported).
# File extensions are optional (if not specified, `.yaml` is used by default).
import:
- catalog/vpc
components:
terraform:
vpc-1:
metadata:
component: infra/vpc # Point to the Terraform component in `components/terraform` folder
inherits:
- vpc-defaults # Inherit all settings and variables from the `vpc-defaults` base component
vars:
# Define variables that are specific for this component
# and are not set in the base component
name: vpc-1
# Override the default variables from the base component
public_subnets_enabled: true
nat_gateway_enabled: true
vpc_flow_logs_enabled: false
vpc-2:
metadata:
component: infra/vpc # Point to the same Terraform component in `components/terraform` folder
inherits:
- vpc-defaults # Inherit all settings and variables from the `vpc-defaults` base component
vars:
# Define variables that are specific for this component
# and are not set in the base component
name: vpc-2
# Override the default variables from the base component
max_subnet_count: 2
vpc_flow_logs_enabled: false
In the configuration above, the following Component-Oriented Programming concepts are implemented:
- Component Inheritance: In the
ue2-devstack (stacks/ue2-dev.yamlstack config file), the Atmos componentsvpc-1andvpc-2inherit from the base componentvpc-defaults. This makesvpc-1andvpc-2derived components - Principle of Abstraction: In the
ue2-devstack, only the relevant information about the derived components in the stack is shown. All the base component settings are "hidden" (in the importedcatalog), which reduces the configuration size and complexity - Dynamic Polymorphism: The derived
vpc-1andvpc-2components override and use the base component properties to be able to provision the same Terraform configuration many times but with different settings
Having the components in the stack configured as shown above, we can now provision the vpc-1 and vpc-2 components into the ue2-dev stack by
executing the following atmos commands:
atmos terraform apply vpc-1 -s ue2-dev
atmos terraform apply vpc-2 -s ue2-dev
As we can see, using the principles of Component-Oriented Programming (COP), we are able to define two (or more) components with different settings, and provision them into the same (or different) environment (account/region) using the same Terraform code (which is environment-agnostic). And the configurations are extremely DRY and reusable.
Multiple Inheritance
Multiple Inheritance is used when an Atmos component inherits from more than one Atmos component.
Multiple Inheritance allows a component to inherit from many base components or mixins, each base component having its own inheritance chain, effectively making it an inheritance matrix. It uses a method similar to Method Resolution Order (MRO) using the C3 linearization algorithm, which is how Python supports multiple inheritance.
In Object-Oriented Programming (OOP), a mixin is a class that contains methods for use by other classes without having to be the parent class of those other classes.
In Component-Oriented Programming (COP) implemented in Atmos, a mixin is an abstract base component that is never meant to be provisioned and does not have any physical implementation - it just contains default settings/variables/properties for use by other Atmos components.
Multiple Inheritance, similarly to Single Inheritance, is defined by the metadata.inherits section in the component
configuration. metadata.inherits is a list of component or mixins names from which the current component inherits.
In the case of multiple base components, it is processed in the order by which it was declared.
For example, in the following configuration:
metadata:
inherits:
- componentA
- componentB
Atmos will recursively deep-merge all the base components of componentA (each component overriding its base),
then all the base components of componentB (each component overriding its base), then the two results are deep-merged together with componentB
inheritance chain overriding the values from componentA inheritance chain.
All the base components/mixins referenced by metadata.inherits must be already defined in the Stack configuration, either by using an import
statement or by explicitly defining them in the Stack configuration. The metadata.inhertis statement does not imply that we are importing anything.
Here is a concrete example:
# Import all the base components and mixins we want to inherit from.
# `import` supports POSIX-style Globs for file names/paths (double-star `**` is supported).
import:
- catalog/terraform/test/test-component-override
- catalog/terraform/test/test-component-override-2
- catalog/terraform/mixins/test-*.*
components:
terraform:
test/test-component-override-3:
vars: {}
metadata:
# `real` is implicit, you don't need to specify it.
# `abstract` makes the component protected from being deployed.
type: real
# Terraform component. Must exist in `components/terraform` folder.
# If not specified, it's assumed that this component `test/test-component-override-3`
# is also a Terraform component in
# `components/terraform/test/test-component-override-3` folder.
component: "test/test-component"
# Multiple inheritance.
# It's a down-top/left-right matrix similar to Method Resolution Order (MRO) in Python.
inherits:
- "test/test-component-override"
- "test/test-component-override-2"
- "mixin/test-1"
- "mixin/test-2"
In the configuration above, all the base components and mixins are processed and deep-merged in the order they are specified in the inherits list:
test/test-component-override-2overridestest/test-component-overrideand its base components (all the way up its inheritance chain)mixin/test-1overridestest/test-component-override-2and its base components (all the way up its inheritance chain)mixin/test-2overridesmixin/test-1and its base components (all the way up its inheritance chain)The current component
test/test-component-override-3overridesmixin/test-2and its base components (all the way up its inheritance chain)
When we run the following command to provision the test/test-component-override-3 Atmos component into the stack tenant1-ue2-dev:
atmos terraform apply test/test-component-override-3 -s tenant1-ue2-dev
atmos will process all configurations for the current component and all the base components/mixins and will show the following console output:
Command info:
Atmos component: test/test-component-override-3
Terraform component: test/test-component
Terraform command: apply
Stack: tenant1-ue2-dev
Inheritance: test/test-component-override-3 -> mixin/test-2 -> mixin/test-1 ->
test/test-component-override-2 -> test/test-component-override -> test/test-component
The Inheritance output shows the multiple inheritance steps that Atmos performed and deep-merged into the final configuration, including
the variables which are sent to the Terraform component test/test-component that is being provisioned.