Skip to main content

Infrastructure as Code

Infrastructure as code (IaC) means infrastructure is defined in version-controlled configuration rather than created by hand. In CNP that configuration is managed the same way as application code: it lives in GitHub, changes go through pull requests, and it is built and deployed by a standard pipeline.

We prefer Software as a Service (SaaS) over Platform as a Service (PaaS) over Infrastructure as a Service (IaaS) — use the highest-level managed offering that meets the need before provisioning raw infrastructure. In practice, “infrastructure as code” in CNP covers configuration for SaaS offerings, and provisioning and configuration of PaaS and IaaS resources.

All our infrastructure is public cloud-based (Azure). We use Terraform to manage it as code.

Sensitive values such as passwords and key pairs must not be managed by Terraform or stored in GitHub — they belong in Key Vault. See secrets management.

Infrastructure levels

Every application in CNP is made up of infrastructure at three levels: platform, product and component (see the glossary for what these terms mean generally). Levels are additive and provisioned in order — platform, then product, then component — and each environment has exactly one set of platform-level infrastructure and one set of each relevant product and component configuration.

Platform-level infrastructure

The base-level infrastructure present in every environment, managed by Platform Operations. Service and common component teams can assume it already exists and consume it — they don’t provision it themselves.

Where platform-level configuration needs a team-specific value, that’s requested as a pull request to the relevant platform repository, reviewed and approved by Platform Operations.

Product-level infrastructure

Infrastructure provisioned by a product team and shared between all components in that product — also referred to as “shared infrastructure”, which is why it follows this repository convention:

  • Repository name: {product}-shared-infrastructure
  • Terraform .tf files go in the root of the repository

Component-level infrastructure

Infrastructure specific to a single component within a product.

  • Terraform .tf files go in the /infrastructure directory of the component’s GitHub repository
  • The draft-store component is a good example

There’s no hard rule for what belongs at product vs component level — if it’s shared between components in a product, it usually belongs at product level.

Pipeline and repository setup

Both product- and component-level infrastructure are built with the common CNP pipeline, which enforces the standards below. To onboard a new infrastructure repository:

  1. Add a .terraform-version file to the repository root so Jenkins uses an explicit Terraform version. Example.
  2. Add a Jenkinsfile_CNP to the repository root to define the build/deploy pipeline. Example.

Terraform modules

Use shared HMCTS Terraform modules instead of building resources from scratch where one exists — they come with sensible defaults, mandated security settings, and less boilerplate to maintain.

Some frequently used modules:

Find the full set in GitHub:

Shared vs platform-internal modules

Not every module is meant for product/component teams to consume directly. Some (typically under cnp-module-* and terraform-module-*) are shared modules intended for teams to use in their own infrastructure, such as the ones listed above. Others exist to build the underlying platform itself and are maintained and consumed by Platform Operations rather than product teams.

Before adopting a module, check its README for its intended audience and required inputs. If it’s not clear whether a module is meant for team consumption, ask in #cloud-native or #sds-cloud-native before building on it.

Approved resources and modules only

Only a pre-defined set of Azure resources and Terraform modules can be used in CNP environments. This whitelist is managed in cnp-jenkins-config, and the common CNP pipeline enforces it — a build fails if it uses a non-approved resource or module. Additional resources can be whitelisted for a specific product or component with appropriate approval.

Tagging

All infrastructure should apply the HMCTS common tag set consistently. These tags are used for cost attribution, support ownership, automation (for example auto-shutdown/expiry in non-prod), and auditability.

This is how we support:

  • cost attribution — knowing which team and product a resource’s spend belongs to
  • operational ownership — knowing who to page and which Slack channel to contact when something goes wrong
  • automated lifecycle management — non-production resources are shut down and sandbox resources are expired based on their tags
  • compliance and audit — resources can be traced back to the change and pipeline run that created them

How tags are supplied depends on your delivery pipeline:

  • Jenkins (common CNP pipeline): tags are injected automatically as a common_tags variable, built by the spinInfra step in cnp-jenkins-library, into every terraform plan/apply. It includes things like the environment, the owning team, the business area, and the build/change that created the resource, plus environment-specific tags such as autoShutdown outside production and expiresAfter in sandbox. You don’t need to (and shouldn’t) set the values yourself — the pipeline supplies them at plan/apply time.
  • Azure DevOps or other Terraform-based delivery: use the terraform-module-common-tags module directly and pass its common_tags output into your resources/modules — same variable name and shape as the Jenkins-injected one:

    module "tags" {
      source      = "git::https://github.com/hmcts/terraform-module-common-tags.git?ref=master"
      environment = var.env
      product     = var.product
      builtFrom   = var.builtFrom
    }
    

    Your product needs an entry in the module’s team-config.yml (or the CFT/SDS equivalents, cnp-jenkins-config’s or sds-jenkins-config’s) for application/businessArea to resolve.

Jenkins is the outlier here — tags come from the pipeline library rather than a module call — so in non-Jenkins pipelines you must explicitly compose and pass the tags yourself using the module above.

However you get them, declare a common_tags variable (a map(string)) in your Terraform and pass it into the common_tags/tags input of every module and resource that accepts one — see any shared module’s README (for example, cnp-module-key-vault) for the exact variable name it expects.

Environment variables and tfvars

The common CNP pipeline supports environment-specific Terraform values via {environment}.tfvars files (for example, prod.tfvars, sandbox.tfvars) placed alongside your .tf files — the matching file is passed to Terraform automatically for each environment.

Jenkins also injects some variables directly at plan/apply time. You don’t need to (and shouldn’t) set their values, but you do need to declare empty Terraform variables so they can be received:

  • env
  • product
  • subscription

If you’re using the Key Vault module, also declare:

  • tenant_id
  • jenkins_AAD_objectId


Next step in Creating a New Component: Set up secrets management

This page was last reviewed on 10 September 2026. It needs to be reviewed again on 10 March 2027 by the page owner platops-build-notices .