Modules
The power behind terranix is the module system which is fundamentally different from the Terraform module system. HCL is deliberately simple, and its limitations come from modules being essentially static resource descriptions and not truly programmatic.
HCL modules by comparison #
In an HCL project, all resources live in .tf / .tf.json files in one big, flat directory that
concatenate resources into one namespace. There are many reasons why one may grow out of this. Too
many resources, even when split into multiple files: multiple sub-projects, or multiple regions of
responsibility, e.g. DNS, GitHub organization, deployment environments.
HCL offers modules which let you split resources into sub-directories:
.
├── main.tf # root module
├── variables.tf # root inputs
├── outputs.tf # root outputs
└── modules/
└── dns/
├── main.tf # child module resources
├── variables.tf # child inputs
└── outputs.tf # child outputsThey come at a serious cost, though:
Each module is only another big, flat directory, no nested scopes. Passing values between modules,
one must declare inputs as variable blocks, outputs as output blocks, and nothing crosses
implicitly. If, for example, you provision a VPS and want to point a subdomain to its IP address,
and the DNS record and the VPS live in separate modules, you need to hoist out a variable that
connects them. Those variables and outputs need to be defined at the parent and in both modules.
HCK lacks means of abstraction, such as functions, so modules easily become a means of abstraction:
module "dns" {
source = "./modules/dns"
domain_name = var.domain_name
records = {
www = {
type = "A"
ttl = 300
records = ["192.0.2.10"]
}
api = {
type = "CNAME"
ttl = 300
records = ["www.${var.domain_name}"]
}
mail = {
type = "MX"
ttl = 3600
records = ["10 mail.${var.domain_name}"]
}
}
}info
For a deeper dive on how modules compose, see the NixOS Wiki: NixOS Modules page — the same module system powers terranix.
Module Structure #
A module is usually looks always like this:
{ config, lib, pkgs, ... }:
{
imports = [
# list of path to other modules.
];
options = {
# attribute set of option declarations.
};
config = {
# attribute set of option definitions.
};
}Example Module #
Here is an example Module to enable bastion host setups.
{ config, lib, pkgs, ... }:
{
options.security.bastion = {
enable = mkEnableOption "bastion host infrastructure";
vpcID = mkOption {
default = "\${ aws_default_vpc.default.id }";
type = lib.types.str;
description = "vpc id to which the bastion host should proxy";
};
};
config = mkIf (config.security.bastion.enable) {
resource.aws_instance."bastion" = {
ami = "ami-969ab1f6"
instance_type = "t2.micro"
associate_public_ip_address = true
};
resource.aws_security_group."bastion-sg" = {
name = "bastion-security-group";
vpc_id = config.security.bastion.vpcId;
ingress.protocol = "tcp";
ingress.from_port = 22;
ingress.to_port = 22;
ingress.cidr_blocks = ["0.0.0.0/0"];
};
output."bastion_public_ip".value = "\${ aws_instance.bastion.public_ip }";
};
}Now you can set the following, to enable the bastion host setup.
{
security.bastion.enable = true;
}mkAssert #
To make an assertion in your module use the mkAssert command.
Here is an example
config = mkAssert (cfg.parameter != "fail") "parameter is set to fail!" {
resource.aws_what_ever."${cfg.parameter}" = {
I = "love nixos";
};
};Provide terranix modules using nix flakes #
Have a look at Writing terranix modules in the flakes documentation.