SSS DocGen — Deployment Runbook
The supported path for standing DocGen up in a new environment: what the solution carries, what has to be done per environment, and the platform behaviours worth knowing before you start. Read this before deploying.
Before you start
- Deploy from an exported solution, not from hand-authored source. The deliverable is a solution exported from an environment where everything already works — see the ALM section below. An exported solution carries its relationships and imports cleanly; that is what export/import is for.
- Run
infra/setup-azure.ps1in PowerShell 7 (pwsh) or Azure Cloud Shell. It is supported under Windows PowerShell 5.1 as well, but pwsh avoids a class of Windows console encoding issues in the Azure CLI's build output.
Azure side (infra/setup-azure.ps1)
The script provisions the resource group, container registry, Key Vault and container app, then grants the container app's managed identity the roles it needs.
The first deploy is expected to retry
On a brand-new environment the container app's managed identity does not exist until the first deployment creates it — so that first revision cannot pull its image (AcrPull) or read its secret (Key Vault Secrets User) yet, and Azure role assignments take time to propagate. The script handles this: the first deploy is non-fatal, it grants both roles to the new identity, waits for propagation, and redeploys. A first-run message about an expired operation followed by a successful redeploy is the normal path, not a failure.
Re-running the script
The script is safe to re-run. Resource creation is guarded by existence checks, and the container image build is skipped when the tag already exists in the registry.
What you get
- An app registration and service principal for the environment.
- A container app on a scale-to-zero plan. Scale-to-zero (min 0) is intentional and is what makes the run cost what it is; the trade is a ~10–20 second cold start on the first request after an idle period.
- A Key Vault holding the Entra client secret.
Dataverse side
Import the exported solution
Import the managed solution into the target environment. It carries the two tables
(sss_documenttemplate, sss_templatedataset) with their columns, both relationships and the
sss_code_key alternate key; the custom API sss_GenerateDocument and its request and response
parameters; the environment-variable definitions; the plugin assembly; and the
SSS DocGen API security role.
Set the environment-variable values
Definitions ship in the solution; values are per environment and are set after import. There are five — see Tenant Setup for the full table.
sss_DocGenClientSecret must be a Text variable, not a Key Vault-backed secret. The plugin
reads its configuration with the out-of-the-box RetrieveEnvironmentVariableValue request, which
does not resolve Key Vault-backed secrets — resolving those requires
RetrieveEnvironmentVariableSecretValue. A secret-typed variable therefore returns empty and the
plugin gets nothing.
The environment-variable type is immutable once created: if one was created as a secret, delete
and recreate it as Text, keeping the schema name sss_DocGenClientSecret. Read the value out of
Key Vault to set it:
az keyvault secret show --vault-name <kv> -n entra-client-secret --query value -o tsv
Because the value is then readable by anyone who can read environment-variable values in the environment, restrict that privilege to the administrators who need it. This is a known limitation with a known fix — see Plugin Registration — and it is covered in the security and compliance review, available on request.
Create the application user
Create an application user for the app registration's client id and assign it the
SSS DocGen API security role: read on the source tables, create on annotation and email, and
CRUD on sss_documenttemplate and sss_templatedataset.
Register the plugin
Run pac plugin push, then bind the custom API by setting customapi.plugintypeid on
sss_GenerateDocument to SssDocGen.Plugins.GenerateDocumentPlugin.
Two things that must be right, or nothing renders
Token version must be v2
The rendering API validates the v2.0 issuer, so the app registration must be created with
requestedAccessTokenVersion=2 (setup-azure.ps1 and main.bicep both set this). A v1 token —
iss=https://sts.windows.net/<tid>/, aud=api://<clientid> — is rejected with a
401.
Because v2 tokens carry aud=<clientid> as a bare GUID, the API's Entra__Audience must be
set to the bare client id, not api://<clientid>.
Target tables need notes enabled
The API stores the generated PDF as an annotation regarding the target record, so any
sss_targetentity you generate against must have notes (attachments) enabled, and the
application user's role needs Create on Note and Read on that table. Generating against a
table with notes disabled fails with
0x80040205 "Creating Entity with an invalid parent. Entity: Annotation". Notes are a table
setting in the maker portal and cannot be enabled by the solution.
ALM: pushing DocGen to the next environment
- In the source environment, finish building everything and add all components to the SSSDocGen solution.
- Export it with
pac solution export— unmanaged for dev, managed for UAT and production. - Commit the unpacked export as
solution/src. It round-trips throughpac solution packwith relationships intact and imports cleanly into the next environment. - Per environment, the following are not in the solution and are still done by hand:
- App registration and Azure infrastructure:
infra/setup-azure.ps1 -EnvName <env>. - Environment-variable values: the five
sss_DocGen*values. - Application user and security role, as above.
- Plugin:
pac plugin pushand thecustomapi.plugintypeidbinding.
- App registration and Azure infrastructure:
Verifying the environment
Call the custom API against a record on a notes-enabled table:
sss_GenerateDocument(Code=SMOKE, EntityId=<guid>, Attach=true)
A healthy environment returns HTTP 200 with a rendered PDF in fileBase64 and an
annotationId for the stored note — which exercises the whole path: plugin → v2 token → API →
Dataverse → Chromium → PDF → annotation. Check warnings in the response as well as the status
code; a truncated dataset or an unresolved parent key reports there rather than failing the call.