Skip to main content
As your infrastructure grows, a single forma file stops scaling. The same stack, target, and tagging conventions get copied from file to file, and a networking pattern you got right once gets pasted into the next project. formae files are Pkl, so you have Pkl’s own tools for reuse: put shared values in a module, group related resources into a class, and compose those pieces into the forma you apply. This guide covers the general reuse mechanics: sharing configuration, grouping resources, and composing modules. For a complete worked class that a platform team hands to developers as a self-service offering, see Build self-service infrastructure.

Share configuration with a vars module

Most projects reuse the same stack and target across several formae. Put those in a plain Pkl module and import it wherever you need them.
Import it and reference the shared values from any forma:
Now the stack and target are defined once. Change the region in vars.pkl and every forma that imports it picks up the new value.

Group resources into a class

When several resources always travel together, a VPC with its subnets and route table, wrap them in a Pkl class. The class takes its inputs as typed fields, builds the resources with hidden, and exposes them as a single resources listing.
A few things worth noting:
  • hidden keeps each resource internal to the class. Only the resources listing needs to be consumed from outside.
  • vpc.res.id is a resolvable. Inside the class the subnet references the VPC’s ID, and formae works out that the VPC has to be created first.
  • The class exposes one thing worth reading: resources, a listing formae knows how to apply.
Wire the class to your inputs and spread its resources into the forma. The ... operator expands the listing into individual resources:

Compose classes

Layered infrastructure is where classes pay off. Build the networking layer, then pass its resources into a class that needs them. A field typed as a resource accepts an instance produced by another class, and the receiving class reads properties off it with .res.
Instantiate both layers, hand the networking resources to the database, and spread each layer’s resources into the forma:

Reference parent fields with outer

When one class nests another, use outer inside the inner definition to reach a field on the enclosing class. This lets a top-level class thread a single input, like a region or an account ID, down into the resources it builds without repeating it.

Prefer functions for straightforward creation

If a module just produces resources from inputs without composing layers, a function is lighter than a class. A function takes its inputs as arguments and returns either a single resource or a Listing.
Call the functions in the forma, spreading any that return a listing:
Reach for a class when you need to hide internal resources behind an interface or pass resources between layers. Reach for a function when creation follows a clear flow and there is nothing to encapsulate.

Apply as usual

Modules and classes are an authoring convenience. By the time formae sees your forma, the class fields and function calls have resolved to a flat set of resources, so applying works exactly as it does for a single-file forma.
  formae apply · reconcile                                                                                main.pkl
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  + 15 create

  ▌ Targets
  Operation ▲   Label
  + create      aws-target

  ▌ Stacks
  Operation ▲   Label
  + create      lifeline

  ▌ Resources
  Operation ▲   Label                                                       Type
  + create      lifeline-vpc                                                AWS::EC2::VPC
  + create      lifeline-igw                                                AWS::EC2::InternetGateway
  + create      lifeline-igw-attachment                                     AWS::EC2::VPCGatewayAttachment
  + create      lifeline-public-subnet-1                                    AWS::EC2::Subnet
  + create      lifeline-public-subnet-2                                    AWS::EC2::Subnet
  + create      lifeline-public-rt                                          AWS::EC2::RouteTable
  + create      lifeline-public-route                                       AWS::EC2::Route
  + create      lifeline-public-subnet-1-assoc                              AWS::EC2::SubnetRouteTableAssociation
  + create      lifeline-public-subnet-2-assoc                              AWS::EC2::SubnetRouteTableAssociation
  + create      lifeline-alb-sg                                             AWS::EC2::SecurityGroup
      ↓ show 10 more (3 remaining)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ↑↓: select  space: expand  →←: column  s: sort  y: confirm  q: abort This operation will create 1 stack(s), create
1 target(s) and create 13 resource(s).  Do you want to continue? (y/N)  ?: help
formae shows the plan and asks for confirmation before making any changes, then streams the run in a live view. Pass --yes to skip the confirmation prompt in a CI/CD job.

See also

  • Build self-service infrastructure: a full class-based offering exposed to developers through property flags.
  • Resolvable: how .res references let resources depend on each other, including across classes.
  • Properties: parameterize a module’s inputs as CLI flags.
  • Stack: how the resources you compose are grouped and reconciled together.
  • Apply modes: reconcile versus patch once your composed forma is ready to apply.