| name | add-new-template |
| description | Step-by-step guide for adding a new Akamai product template to this repository. Use when: creating a new template type (new psm1 module, new Terraform directory, new deploy.ps1 registration); ensuring the mandatory class/function structure is followed; wiring a new template key into the deploy.ps1 dispatch system. Covers naming conventions, mandatory exported functions, param policy rules, and Terraform directory layout. |
| argument-hint | Name of the new template (e.g. "MyProduct" or describe what it configures) |
Adding a New Template
Overview
Every template in this repository is a three-part unit:
| Part | Location | Purpose |
|---|
| Terraform config | new-{name}/ | HCL resources, variables, outputs |
| PowerShell module | lib/templates/{Name}.psm1 | Deploy/destroy orchestration |
| deploy.ps1 entries | deploy.ps1 | CLI registration and routing |
All three must be created together. Missing any part causes a runtime failure.
Naming Conventions
Before writing any code, fix the three naming identifiers:
| Concept | Format | Examples |
|---|
| Template type key | lowercase, short, no spaces | aap, pm, bmp, ds2 |
| Module name | PascalCase, matches module file stem | AAP, PropertyManager, BMP, DS2 |
| Terraform folder | new-{descriptive-name} | new-aap-configuration, new-property, new-edns |
The module name is what deploy.ps1 uses to derive all function names automatically:
New-{Name}Template
Get-{Name}ParamPolicy
Invoke-{Name}Template
Get-{Name}TemplateFolder (optional)
These names are not configurable — they are constructed by string interpolation in deploy.ps1.
Checklist
Work through these steps in order. Each step has a reference section below.
Step 2 — Terraform Directory Structure
Scaffolding only. The files created here are structural placeholders. The actual HCL resource and module logic is sourced from terraform-templates-modules and added separately once the product requirements are known. Focus on getting the correct file layout, standard boilerplate, and documented .tfvars.dist examples in place.
new-{name}/
├── main.tf # TODO: module calls — stub only at scaffold time
├── variables.tf # Common variables pre-filled; add product-specific vars
├── outputs.tf # TODO: outputs consumed by psm1 — stub only at scaffold time
├── provider.tf # Complete — identical across all templates
├── versions.tf # Complete — identical across all templates
├── README.md # Auto-generated by terraform-docs (do not hand-edit)
└── environments/
├── dev/
│ └── dev.tfvars.dist # Documented example — always committed
├── qa/
│ └── qa.tfvars.dist
└── prod/
└── prod.tfvars.dist
Key rules:
- Do not create
.tfvars files at scaffold time — those contain customer-specific values and are added later.
config.backend is auto-generated by deploy.ps1 on every run — do not create it manually.
- State files (
{env}-terraform.tfstate) are local and environment-scoped — never at the template root.
versions.tf must pin the Akamai provider to ~> 9.0 and require Terraform >= 1.9.0.
- The
.tfvars.dist filename prefix must match the environment name (e.g. prod.tfvars.dist for -Env prod).
Copy the ready-to-use scaffold files from ./assets/terraform/ — provider.tf and versions.tf are complete; main.tf, variables.tf, outputs.tf, and the .tfvars.dist example contain TODO markers for product-specific additions.
See psm1-anatomy.md for the full annotated module structure.
Step 3 — PowerShell Module Structure
The module file lives at lib/templates/{Name}.psm1. It must contain exactly these components in order:
1. using module imports (3 core modules)
2. class {Name}Template — state container + orchestration methods
3. function New-{Name}Template — factory (exported)
4. function Get-{Name}ParamPolicy — param validation policy (exported)
5. function Invoke-{Name}Template — deploy.ps1 dispatch entry point (exported)
6. Export-ModuleMember — exposes the three functions above
Optionally add function Get-{Name}TemplateFolder (exported) only when the Terraform folder cannot be determined from a static string — for example when the folder depends on a runtime parameter (see CPS.psm1).
Class mandatory members
class {Name}Template {
[string]$Environment # set by constructor
[string]$TemplateFolder # set by constructor
[hashtable]$DeployParams # populated in Deploy()
{Name}Template([string]$environment, [string]$templateFolder) { ... }
[void] ValidatePrerequisites() { ... } # check tfvars exists; optionally validate product IDs
[hashtable] BuildTerraformVars() { ... } # return vars hashtable passed to Invoke-TerraformPlan
[void] Deploy([hashtable]$params) { ... }
[void] Destroy() { ... }
# Optional: [void] HandleApplyFailure() { ... } for quirk-specific import logic
}
Get-{Name}ParamPolicy return shape
return @{
Required = @("Environment") # params that MUST be present
RequiredHints = @{ Environment = "Use: -Env <env>" } # error hints for Required params
Allowed = @("Environment", "Save", "ActivateStaging", "ActivateProduction",
"Destroy", "VersionNotes", "SkipValidation", "Dry", "Debug", "Force")
MustHaveOneOf = @("Save", "ActivateStaging", "ActivateProduction", "Destroy")
}
Required and RequiredHints are optional — omit them if no param is unconditionally required (e.g. CPS omits Environment).
Allowed must list every parameter the template accepts. Any bound parameter not in this list causes Assert-TemplateParameters to reject the command.
MustHaveOneOf enforces that the user picks at least one action.
Available core helpers
All are imported automatically via the using module statements at the top of the file.
| Function | Module | Purpose |
|---|
Get-Username | Logger | Current OS username (for email defaults) |
Initialize-TerraformBackend | TerraformRunner | Writes config.backend, runs terraform init, optionally runs drift check |
Invoke-TerraformPlan | TerraformRunner | terraform plan with -var args and -out; returns exit code |
Invoke-TerraformApply | TerraformRunner | terraform apply <planfile>; returns exit code |
Invoke-TerraformDestroy | TerraformRunner | terraform destroy; supports -AutoApprove and -NoRefresh |
Test-TerraformResourceExists | TerraformRunner | Check whether a resource address is in state |
Get-TerraformOutput | TerraformRunner | terraform output -json parsed to object |
Test-AkamaiProductId | Validation | Validates product ID in .tfvars against expected list |
Assert-TemplateParameters | Validation | Called by deploy.ps1 using the policy returned by Get-{Name}ParamPolicy |
Confirm-DestroyOperation | Validation | Interactive confirmation prompt before destructive operations |
Enable-TerraformDebugLogging | Logger | Sets TF_LOG=DEBUG and AKAMAI_HTTP_TRACE_ENABLED=true |
Disable-TerraformDebugLogging | Logger | Clears debug env vars |
See ./assets/NewTemplate.psm1 for a ready-to-copy starter module.
Step 4 — Register in deploy.ps1
Three edits are required. Open deploy.ps1 and apply all three:
See deploy-registration.md for the exact diff for each location.
4a. ValidateSet on $TemplateType (~line 177)
Add the new key to the [ValidateSet(...)] attribute. The value must be lowercase.
4b. $templateModuleMap (~line 286)
$templateModuleMap = @{
...
"mykey" = "MyName" # key → module name (used to derive function names)
}
4c. $templateFolderMap (~line 302) — skip if using Get-{Name}TemplateFolder
$templateFolderMap = @{
...
"mykey" = "new-myname-configuration"
}
If the template folder depends on a runtime parameter, do not add an entry here. Instead export Get-{Name}TemplateFolder from the module (see CPS.psm1 for reference).
Step 5 — New deploy.ps1 Parameters (if needed)
If the template needs CLI parameters that don't already exist (e.g. a new -ZoneType or -CpsType style selector), add:
- A
.PARAMETER doc block near the top of deploy.ps1.
- A
[Parameter(...)] declaration in the Param(...) block with an appropriate ParameterSetName.
- The new parameter name to the
Allowed list in Get-{Name}ParamPolicy.
Do not add new parameters for things already covered by the standard set: Environment, Save, ActivateStaging, ActivateProduction, Destroy, VersionNotes, Dry, Debug, Force, SkipValidation.
Step 6 — Smoke Test
# Verify deploy.ps1 can resolve the module and policy without errors
pwsh deploy.ps1 mykey -Env dev -Dry -Save
# Verify help reflects the new template type
Get-Help ./deploy.ps1 -Full
If deploy.ps1 cannot find Invoke-{Name}Template it throws:
Template dispatch function not found: Invoke-MyNameTemplate
This means either the Export-ModuleMember line is missing or the function name doesn't match the module name in $templateModuleMap.
Step 7 — Pester Tests
All tests live in tests/deploy.Tests.ps1. A new template requires additions in four places within that file — search for the Describe block names below to find each location.
7a. Module loading (Describe "Template Modules - Module Loading")
Add one It inside the existing Context "All template modules should be available":
It "Should load {Name} module" {
{ Import-Module "$PSScriptRoot/../lib/templates/{Name}.psm1" -Force } | Should -Not -Throw
}
Also add Import-Module "$PSScriptRoot/../lib/templates/{Name}.psm1" -Force to the BeforeAll block of the Param Policy Contract Describe block immediately below.
7b. Param Policy Contract (Describe "Template Modules - Param Policy Contract")
Three It blocks, one per Context:
# Context "Every template module exports a Get-*ParamPolicy function"
It "{Name} module exports Get-{Name}ParamPolicy" {
Get-Command "Get-{Name}ParamPolicy" -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
# Context "Each Get-*ParamPolicy returns a valid hashtable with an Allowed list"
It "Get-{Name}ParamPolicy returns a hashtable with Allowed" {
$p = Get-{Name}ParamPolicy
$p | Should -BeOfType [hashtable]
$p.Allowed | Should -Not -BeNullOrEmpty
}
# Context "Every template module exports an Invoke-*Template dispatch function"
It "{Name} module exports Invoke-{Name}Template" {
Get-Command "Invoke-{Name}Template" -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
7c. Custom folder logic (only if Get-{Name}TemplateFolder is implemented)
Add inside Context "Modules with custom folder logic export Get-*TemplateFolder":
It "{Name} module exports Get-{Name}TemplateFolder" {
Get-Command "Get-{Name}TemplateFolder" -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
It "Get-{Name}TemplateFolder derives the folder from the runtime param" {
Get-{Name}TemplateFolder -BoundParams @{ SomeParam = "value" } | Should -Be "new-expected-folder"
}
It "Get-{Name}TemplateFolder throws when the required param is missing" {
{ Get-{Name}TemplateFolder -BoundParams @{} } | Should -Throw -ExpectedMessage "*SomeParam is required*"
}
7d. CLI parameter validation (Describe "deploy.ps1 - CLI Parameter Validation")
Add a new Context block for the new template. Adjust the assertions to match the template's actual Allowed list and any custom parameters.
See ./references/tests-reference.md for a copy-paste block with all standard cases.
Running the tests
# From the repository root
Invoke-Pester -Path ./tests/deploy.Tests.ps1 -Output Detailed
# Or using the config file
cd tests && Invoke-Pester -Configuration (./pester.config.ps1)
All new It blocks must pass before the PR is submitted. Tests run automatically on PR via the GitHub Actions PR Validation workflow.
Step 8 — GitHub Actions Workflow Registration
Three files require additions. All follow append-in-group patterns — new steps go at the end of their respective group, not at the end of the file.
8a. pr-validation.yml — Terraform Validate group
Locate the last Terraform Validate - step (currently Terraform Validate - EDNS) and insert immediately after it, before the Setup TFLint step:
- name: Terraform Validate - {Display Name}
working-directory: ./{terraform-folder}
run: |
terraform init -backend=false
terraform validate
8b. pr-validation.yml — TFLint group
Locate the last Run TFLint - step (currently Run TFLint - BMP) and insert immediately after it, before the Run Trivy Security Scan step:
- name: Run TFLint - {Display Name}
working-directory: ./{terraform-folder}
run: tflint --recursive
8c. tf-docs.yml — terraform-docs group
Append after the last Generate terraform-docs for step (currently Generate terraform-docs for EDNS):
- name: Generate terraform-docs for {Display Name}
uses: terraform-docs/gh-actions@v1.4.1
with:
working-dir: ./{terraform-folder}
config-file: ../.terraform-docs.yaml
output-file: README.md
output-method: inject
git-push: true
CPS-style note: If the template uses multiple Terraform directories (like CPS uses new-dv-san-cert/ and new-third-party-cert/), each directory needs its own step in all three groups.
See ./references/workflow-registration.md for copy-paste blocks with all three snippets.