Cloud Functions
Audience: operator · Profile:
demo· Read this when: you need a stable HTTPS endpoint external systems can call to act inside your network.Enable it in Settings → Integrations → Cloud Functions (
cloud_functions_enabled), then configure a package store in the same panel. The Functions page is at/functions. Design notes: docs/design/cloud-functions.md.
What is it?
One handler, three clouds. You write (or pick) a workload — a single Python function — and the dashboard deploys it as an AWS Lambda, an Azure Linux Function App, or a GCP Cloud Run function, with the same behaviour on each. Every function gets a stable HTTPS endpoint protected by a shared bearer secret, and can optionally be attached to a VPC/VNet so it can reach private resources.
The handler source lives in the repo at web_dashboard/functions/ and ships inside
the dashboard image, so there is no build pipeline to run and no artifact to
publish yourself.
Use cases
| Diagnose serverless networking | echo_diag reports what the function can actually reach — resolving DNS and TCP separately, so "broken" becomes "DNS" or "security group" |
| Certificate expiry watch | cred_expiry_watch runs a TLS handshake sweep from inside the VPC, so it sees internal-only endpoints |
| An endpoint for external systems | Something outside your network needs to trigger an action inside it, synchronously — the case a one-shot container cannot serve |
| Entitle REST integrations (Phase 2) | Entitle POSTs Give/Revoke Access to the function, which performs the grant against a private target |
The dashboard already reaches private resources for its own work by running one-shot containers inside the target cloud (ECS / ACI / Cloud Run jobs). Reach is not what this adds — an always-on, externally callable endpoint is.
Prerequisites
| Cloud | Needs |
|---|---|
| AWS | An S3 bucket for packages. The dashboard's IAM identity needs s3:PutObject, plus lambda:*, iam:CreateRole/AttachRolePolicy/PutRolePolicy (the execution role and its inline secrets policy), secretsmanager:CreateSecret/PutSecretValue/DeleteSecret/DescribeSecret/TagResource (every function keeps its bearer secret there) and logs:* on the function's log group (including logs:DeleteLogGroup, or destroy leaves it behind). Two that are easy to miss: iam:PassRole on the execution role (lambda:CreateFunction passes it to Lambda) and lambda:AddPermission — a function URL with authorization_type = NONE needs a resource policy that CreateFunctionUrlConfig does not write, so without it every public invoke returns {"Message":"Forbidden"}. The module creates that permission itself; the deploying identity just needs to be allowed to. |
| Azure | The dashboard's existing storage account (storage_azure_account) and resource group, plus a Key Vault — every function keeps its bearer secret there. The service principal needs Microsoft.Web/*, Microsoft.Storage/storageAccounts/listKeys/action, Microsoft.Web/sites/host/listKeys/action, Microsoft.ManagedIdentity/userAssignedIdentities/*, secret set on the vault, and authority to grant on it (Microsoft.Authorization/roleAssignments/write for an RBAC vault, or Microsoft.KeyVault/vaults/accessPolicies/write for a policy-based one). |
| GCP | A GCS bucket for sources, the cloudfunctions + artifactregistry APIs enabled, and a noticeably larger IAM surface: roles/cloudfunctions.developer, roles/run.admin, roles/cloudbuild.builds.builder, roles/artifactregistry.writer, roles/storage.objectAdmin (project-wide, or bucket-scoped on the source bucket), roles/secretmanager.admin, plus roles/iam.serviceAccountUser on the runtime service account. Direct VPC egress itself needs no extra grant — those permissions belong to the Cloud Run service agent, which already holds them for a same-project VPC. |
| All | Terraform in the image (it is, by default). GCP additionally needs the google provider >= 7.21, which the image pre-caches. |
A missing GCP role surfaces as a 403 roughly 90 seconds into the build, after the plan succeeded. If a GCP deploy fails late, check the roles above first.
Setup
- Settings → Integrations → turn on Cloud Functions. The nav entry and
/api/functionsappear immediately; no restart. - Settings → Integrations → Cloud Functions (Configure) → set the package store for each cloud you intend to use. A cloud without one is shown in the deploy form but blocked, with the reason.
- (Optional) Fill in the VPC / VNet attachment section if you want functions that reach private resources. See Networking.
- Go to Functions → Deploy function, pick
echo_diag, and deploy. Then use Test invoke to confirm it works.
The adapter workloads (
db_grant,portainer_access,azure_role_grant) read their target out of the function's environment and do nothing without it — the deploy form names what each one needs and refuses a deploy that omits it, because the alternative is a function that builds successfully and then fails on every request.db_grantin particular should always be deployed for you — by the Function (DB grant) button on a database's row, or at provision time: see Cloud databases and theclouddb_adapter_pairjob.POST /check_configis the route to ask an already-deployed adapter whether it is configured; it answers{"data": {"valid": false, "problems": [...]}}rather than failing when it is not. Oncedb_grantis armed (FN_DB_DRY_RUN=0) it also opens one real connection, so it answers the question a dry run cannot: whether the function can actually reach the database.
Field reference
| Field | Notes |
|---|---|
function_package_s3_bucket |
AWS package bucket |
function_package_gcs_bucket |
GCP source bucket |
function_package_azure_container |
Blob container on the dashboard's storage account (default function-packages) |
azure_functions_plan_sku |
App Service plan SKU, default B1 |
aws_functions_subnet_ids / aws_functions_security_group_ids |
Comma-separated; used when a function is deployed in vpc mode |
azure_functions_subnet_id |
Must be delegated to Microsoft.Web/serverFarms |
gcp_functions_network / gcp_functions_subnetwork |
GCP Direct VPC egress — no connector, nothing billed while idle. Give the subnet as a bare name: direct egress is region-locked, and a bare name resolves in whichever region the function lands in. |
gcp_functions_service_account |
Runtime service account. Blank falls back to the broad default compute SA — and skips the per-secret accessor binding, so the function cannot read its own bearer secret. |
gcp_functions_vpc_connector |
Legacy fallback: an existing Serverless VPC Access connector. Only needed to reach another region from a region-pinned function. Ignored when the two keys above are set. |
What it enables in the dashboard
| Feature | Where |
|---|---|
| Deploy / list / destroy functions | Functions page, /api/functions |
| Workload catalog | GET /api/functions/workloads |
| Test invoke with credentials attached | Test invoke button, POST /api/functions/{id}/invoke (takes method + path, so an adapter's own routes are reachable) |
| Endpoint + secret for an external caller | Endpoint button (admin only), GET /api/functions/{id}/invoke-info |
| Progress + logs for a deploy | Jobs page (cloudfn_deploy / cloudfn_decommission) |
| Count of deployed functions, and how many are callable | Cloud Functions tile, Dashboard → Managed Services |
| Every function alongside the rest of the estate | Inventory page, kind function (GET /api/inventory?kind=function) |
Workloads
Each is one module in web_dashboard/functions/fnworkloads/. The filesystem is the
catalog — adding a module makes it deployable, no registry to edit. Sample request
payloads for each, and a template for writing your own, are in
examples/functions/.
| Workload | What it does |
|---|---|
echo_diag |
Echoes the request as the function saw it (redacted), reports where it landed, and DNS/TCP-probes any {host, port} you pass |
entitle_webhook_echo |
A no-op Entitle Remote Adapter — serves every route with valid empty responses and reports what arrived, without granting anything. Supports ?fail=401\|403\|404\|500\|slow\|timeout |
db_grant |
Entitle Remote Adapter for ephemeral just-in-time database accounts on MySQL and SQL Server. Dry run by default. See below. |
portainer_access |
Entitle Remote Adapter for just-in-time Portainer access, granted through team membership. Dry run by default. See below. |
azure_role_grant |
Entitle Remote Adapter for time-boxed Azure RBAC on a machine identity. Dry run by default. See below. |
Pages
Each workload, and the work around them, has a page:
| Page | Read it when |
|---|---|
| Wiring an Entitle REST integration | you are pointing an Entitle custom REST integration at a function the dashboard deployed |
| Pairing an adapter with what it grants | you are deploying a db_grant or portainer_access adapter and want it paired with its database or Portainer rather than built by hand |
| The db_grant workload | you want Entitle to grant and revoke database roles through a function inside your network |
| The portainer_access workload | you want Entitle to grant and revoke Portainer team membership |
| The azure_role_grant workload | you want Entitle to grant and revoke Azure role assignments through a function |
| Writing your own workload | none of the shipped workloads does what you need and you want to add one |
| How functions authenticate | you are deciding how callers prove who they are to a function, and how a function reaches its cloud and the dashboard |
| Troubleshooting Cloud Functions | a function will not deploy, answers 401 or 403, or cannot reach what it should act on |
Dashboard permissions
The fourth integration is not a function at all: granting dashboard permissions happens on a dashboard-hosted endpoint, because there the dashboard is the target system and there is no function to deploy. See Entitle dashboard permissions.
Credentials
A workload that needs a credential — a database admin password, a Portainer token, an Azure client secret — gets it by reference on every cloud. You name a secret that already lives in the cloud's own secret store, and the dashboard wires up that cloud's resolution mechanism:
| Cloud | What you pass | What happens |
|---|---|---|
| AWS | the Secrets Manager ARN | the id lands in <NAME>_SECRET_ID and the ARN on the function's role; the handler reads it at cold start |
| GCP | the Secret Manager secret id | injected as a secret_environment_variables entry, and the runtime service account is bound roles/secretmanager.secretAccessor on that secret alone |
| Azure | the Key Vault secret name | becomes an @Microsoft.KeyVault(SecretUri=…) app setting the platform resolves through the app's system-assigned identity (grant it get on the vault) |
On AWS it must be the full ARN: the role's policy names ARNs, and AWS appends a random suffix to every one, so a name cannot be turned into an ARN by concatenation.
POST /api/functions
{
"cloud": "gcp", "region": "us-central1", "name": "portainer-jit",
"workload": "portainer_access",
"environment": { "FN_PORTAINER_URL": "https://portainer.internal",
"FN_PORTAINER_DRY_RUN": "0" },
"secret_environment": { "FN_PORTAINER_API_KEY": "portainer-jit-token" }
}
environmentis for non-secret settings, and the dashboard enforces that. A credential-shaped value there is refused at deploy with a message pointing here. The reason it is an error rather than a warning:environmentis stored in the deploy job's metadata, streams into the job's Live Output as Terraform renders the plan, and stays readable on the function's own console page — so by the time you saw a warning, the credential would need rotating.
Automatic pairing already works this way; nothing to configure. A db_grant adapter
deployed by Register in Entitle stages the admin password in the cloud's secret
store and passes only the reference.
Provenance: what is actually running
A deployed function runs privileged code from inside your network, so the question "what exactly is running in there, and where did it come from?" needs to stay answerable months later — especially once workloads come from more than one image.
Every deploy records three things on the function:
| What it tells you | Availability | |
|---|---|---|
| source tree hash | what code — two functions with the same value run the same handlers | always |
| package hash | the exact deployed artifact, including vendored dependencies | always |
| git commit / ref / origin | who wrote and reviewed it | when the image was built with the build args |
The tree hash is computed from the files that produced the package, so it cannot be
wrong. Git metadata is best-effort — an image built without the build args records
"unknown" rather than a guess, because a confidently wrong commit is worse than
an honest blank. A build from a modified working tree is marked -dirty.
To get git provenance into your image:
docker build \
--build-arg GIT_SHA="$(git rev-parse HEAD)" \
--build-arg GIT_REF="$(git rev-parse --abbrev-ref HEAD)" \
--build-arg GIT_ORIGIN="$(git config --get remote.origin.url)" \
--build-arg BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" .
It also lands as the standard org.opencontainers.image.revision label.
Ask the function, don't trust the record. The same values are injected as
FN_SOURCE_COMMIT / FN_SOURCE_TREE, and echo_diag reports them in its
provenance block. So verifying a deployment is one request — and it catches the
case the dashboard row cannot: a deploy that half-failed, or a function someone
changed by hand.
curl -sS -X POST "$FN_URL" -H "Authorization: Bearer $SECRET" \
-d '{"egress": false}' | jq .provenance
Networking
network_mode: "public" deploys an internet-reachable endpoint. "vpc" also
attaches the function to your network.
| Cloud | Mechanism | Watch out for |
|---|---|---|
| AWS | vpc_config |
A VPC-attached Lambda has no outbound internet unless its subnets route through NAT |
| Azure | Regional VNet integration | Needs a subnet delegated to Microsoft.Web/serverFarms, and a plan that supports it — Y1 does not (plan-time failure) |
| GCP | Direct VPC egress (direct_vpc_network_interface) |
Region-locked — the subnet must be in the function's region, or traffic to an internal IP silently drops. Cloud Run reserves IPs in /28 blocks and needs a /26 minimum subnet; sharing one subnet across functions is supported. Needs provider >= 7.21. |
| GCP (legacy) | Serverless VPC Access connector | Only for cross-region reach from a region-pinned function. Reference an existing one; a connector costs ~$26/mo whether invoked or not |
Which subnet, in which region
Every cloud has a per-region functions network field, and it is what makes a function land on its own region's network rather than the default region's:
| Cloud | Per-region field(s) | Flat fallback |
|---|---|---|
| AWS | functions_subnet_ids, functions_security_group_ids |
aws_functions_subnet_ids, aws_functions_security_group_ids |
| Azure | functions_subnet_id |
azure_functions_subnet_id |
| GCP | functions_network, functions_subnetwork |
gcp_functions_network, gcp_functions_subnetwork |
Set them under Settings → Multi-region (the sandbox scripts emit both halves). A field a region leaves blank falls back to its flat key, so a single-region install needs none of this and behaves exactly as before.
The fields are deliberately purpose-specific rather than reusing the generic
default_subnet_id/subnetwork: a flat *_functions_* key answers two questions at once
— which subnet do functions use and in the default region — and overriding it with a
generic per-region subnet fixes the region by discarding the purpose, and only for
non-default regions. That asymmetry (the same feature choosing a different-purpose subnet
depending on whether the region happened to be the default) is the bug these fields exist
to avoid.
For a caller that supplies the region programmatically rather than from a picker — the cloud-DB adapter pairing uses the database's region — the deploy is refused when that region has no per-region config set at all, rather than quietly attaching to the default region's network.
Phase 2 — Entitle REST integrations
Entitle's REST integration posts Give Access / Revoke Access to a target system and expects a synchronous response. A function is the natural receiver: it is externally reachable, responds in milliseconds, and can sit inside the VPC beside the resource being granted.
Use entitle_webhook_echo to validate the whole path — network, front door, secret,
timeouts, retries — before any grant logic exists. It reports exactly which fields it
recognised in the payload, which is how the schema gets pinned against a real tenant.
Planned integrations: ephemeral MySQL/SQL Server database accounts, Azure machine identity, Portainer access, and dashboard user identity. See the design doc.