Developing First-party Modules

First-party modules are those infrastructure modules that are written and maintained by your organization and located in your organization’s infrastructure stack repository. They are written using the OpenTofu (Terraform) syntax.

Setting up the Infrastructure Directory

It is assumed that your infrastructure modules will live in your stack repository under a single directory.

Set the iac_dir repo variable to that directory per the reference docs.

For example, if iac_dir is set to infrastructure, it is expected your filesystem layout will look as follows:

infrastructure/
- [module_1]/
- main.tf
- vars.tf
- outputs.tf
- [module_2]/
- [module_3]/
***

All the normal conventions for developing OpenTofu (Terraform) modules apply.

Deploying Modules

All the rules in the main module deployment guide apply. However, there are some additional considerations for first-party modules:

Sourcing

We provide convenience functionality for sourcing first-party modules in your Terragrunt configuration. This takes care of common issues around versioning and caching:

In your terragrunt.hcl, you should source your module as follows:

terraform {
source = include.panfactum.locals.source
}

By default, the source will be the infrastructure module with the same directory name as the directory containing the terragrunt.hcl.

For example, consider the following repository layout:

environments/
- development/
- us-east-2/
- module1/
- terragrunt.hcl
- module.yaml
infrastructure/
- module1/
- module2/

The environments/development/us-east-2/module1/terragrunt.hcl configuration will automatically source the infrastructure/module1 module since the terragrunt.hcl is in a directory called module1.

Overriding Default Source

You can override this behavior by setting the module key in the module.yaml.

Consider the above example again. This time, the module.yaml has the following contents:

module: module2

Now the sourced module will be infrastructure/module2.

Using a Private Git Repository

If your first-party IaC is stored in a private git repository, you will need to do some additional setup:

  • Set the repo_url repository variable. This will likely have already been set up when your system was first deployed.

  • Set GIT_USERNAME and GIT_PASSWORD in your personal .env file. The values will be combined with repo_url to clone your IaC over HTTPS as follows:

    git clone https://<GIT_USERNAME>:<GIT_PASSWORD>@<repo_url> 1

    While this is supported by all major git hosting providers, the exact values may differ for GIT_USERNAME and GIT_PASSWORD. Below we outline some common scenarios:

Versioning

By default, the local code for the module will be sourced. This can be ideal for local testing and/or an integration environment where you always want your local changes to be deployed. However, this is not ideal for higher environments where you want a more controlled, stable release process.

You can pin a module (or set of modules) to a particular version by setting the version key to a git ref in one of the environment.yaml, region.yaml, or module.yaml files. A git ref can be a tag, a branch name, or even a specific commit hash of your stack repository (the repository specified by repo_url).

For example, consider a stack repo that has the git tags v1.0.0, v1.1.0, and v2.0.0 representing versioned releases of your IaC code.

This stack repo has the following layout:

environments/
- development/
- environment.yaml
- us-east-2/
- module1/
- module2/
- staging/
- environment.yaml
- us-east-2/
- module1/
- module2/
- production/
- environment.yaml
- us-east-2/
- module1/
- module2/
- us-west-2/
- region.yaml
- module1/
- module2/
infrastructure/
- module1/
- module2/

The environment.yaml files have the following version keys:

  • development/environment.yaml: main
  • staging/environment.yaml: v2.0.0
  • production/environment.yaml: v1.1.0

Additionally, the production/us-west-2/region.yaml file has the version key set to v1.0.0.

In this scenario, even though all three environments will use both the module1 and module2 modules, different versions of these modules will be deployed:

  • All modules in development will use the latest infrastructure/* code on the main branch (useful for ensuring the latest code is always deployed). This is because development/environment.yaml has version set to main.
  • Similarly, all modules in staging will use the infrastructure/* code at the v2.0.0 git tag.
  • The modules in production will default to using the infrastructure/* code at the v1.1.0 tag except for the modules in us-west-2 which will use v1.0.0.
Terragrunt module versioning diagram

Writing Modules

Using Panfactum Submodules

In addition to the infrastructure modules that you deploy directly, we provide many submodules that you many import directly for use in your custom modules.

You can accomplish this via module blocks in your infrastructure code (docs).

For example:

module "pod" {
source = "github.com/Panfactum/stack.git//packages/infrastructure/kube_pod?ref=main"
}

However, the above syntax has a major downside: your module sources will not automatically align to the version of the Panfactum Stack to which you are deploying the module. As a particular submodule version is always meant to be running on the same version of the Panfactum Stack, this is a problem. 2

Fortunately, we provide a convenient way to automatically align your submodule versions:

  1. Add the following variables to your module:

    variable "pf_module_source" {
    description = "The source for Panfactum submodules"
    type = string
    }
    variable "pf_module_ref" {
    description = "The git ref for Panfactum submodules"
    type = string
    }
  2. You can now source Panfactum submodules as follows:

    module "pod" {
    source = "${var.pf_module_source}kube_pod${var.pf_module_ref}"
    }

Our Terragrunt configuration automatically takes care of passing in the appropriate values for pf_module_source and pf_module_ref. Whenever you change the pf_stack_version Terragrunt variable, these variables will automatically update.

Finally, if you are using Panfactum submodules, you should not set version constraints for IaC providers used by the submodule (check the module’s documentation for what providers are used). Instead, your first-party IaC will automatically inherit the constraints from the submodule’s configuration. This ensures that no first-party IaC updates are required when changing Panfactum versions.

For example, you should remove static version constraints:

terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.34.0"
}
}
}

Using First-Party Submodules

You may also want to create submodules that you can reference in other modules. There are no special rules to follow, but we recommend sourcing the modules via relative paths rather than via a remote address:

module "utility" {
source = "../utility_module"
}

This ensures that you do not need to keep track of version dependencies across modules and reduces your deployment runtimes by avoiding unnecessary network calls.

Local Development

When developing modules, you may want to test changes before committing them and waiting on the CI/CD pipeline for deployment. Additionally, you may want to use your own settings for the OpenTofu (Terraform) providers (for example, using a specific AWS profile for authentication).

You can override all values in the committed version of global.yaml, environment.yaml, region.yaml, and module.yaml with *.user.yaml counterparts (e.g., environment.user.yaml). These files are never committed and are specific to you. For all available settings, see the reference documentation.

For example, you can set version to local in order to deploy your local code when you run terragrunt apply in a module directory.

It is common that you might want to set the version in environments/development/<some-region>/region.user.yaml if you are frequently testing local changes in a particular development region:

version: local

Best Practices

Monorepo

One of the reasons we strongly recommend a monorepo setup is so that you can version your infrastructure code in tandem with your application code. Often times application code and infrastructure depend on one another, and by using tandem versioning you can ensure that version X of your application code will run properly as long as version X of the infrastructure has been deployed.

In our experience, mistakes in managing this dependency graph causes a significant number (>25%) of all major bugs and outages in most software organizations. It benefits you to keep this as simple as possible.

Using the IaC Version for Application Code

By default, version only refers to which version of the module to source. However, you may also want to use this to control which version of your application code to deploy as well (for example which container image tag to use in your Kubernetes deployment). That way changing version from X to Y will update both the infrastructure and the application code to version Y.

In your terragrunt.hcl, you can retrieve the version via include.panfactum.locals.version. For example:

inputs = {
kube_image_version = include.panfactum.locals.version
}

This works especially well in a monorepo setup.

If you need the commit hash for a version, use include.panfactum.locals.version_hash instead.

Rolling Deployments in Integration Environments

In your integration environment (often called the development environment), it is often helpful to have the latest code on the primary integration branch deployed immediately. Simply set the version to the branch name in order to accomplish this.

This eliminates a manual process and will allow you to catch bugs early and often.

Pinned Versions in Higher Environments

You should always pin your versions in higher environments like production. This allows your organization to release new changes only when you are ready for them.

Additionally, having to change a file in the repo to trigger a deployment ensures that your organization will implicitly implement change control practices (e.g., pull request approvals, immutable audit log, etc.) that meets most compliance frameworks such as SOC 2.

Showing the Commit Hash

Often you want to know exactly what code is deployed in an environment at any given time. Git refs like branch names or even tags are not helpful as they are mutable and can change what code they point to.

The Panfactum Terragrunt system will automatically provide all deployed modules with an input variable called version_hash which represents the actual commit hash being deployed. You should use this to label and tag your infrastructure in your first-party modules; we do this automatically in the Panfactum modules.

Note that if version is set to local, the version_hash will also be calculated as local.

Footnotes

  1. This is just a conceptual example. The exact interpolation will depend on the repo_url you provide. For example, if you provide git::https://example.com/vpc.git as the repo_url, then the framework will automatically insert the GIT_USERNAME and GIT_PASSWORD in the appropriate location.

  2. For example, you should not run a submodule version edge.24-10-09 when the cluster is running edge.24-11-24.