Registers the OAuth token App Service & Function Apps use to pull from a source control provider β a subscription-level singleton keyed only by provider
type, with case-sensitive enum validation,sensitivetoken inputs, and zero secret leakage in outputs. Built for AzureRM v4.x.
This module manages a single App Service source control token (azurerm_source_control_token) β the OAuth credential Azure App Service and Function Apps use to authenticate to a Git provider when you wire continuous deployment from a repository.
- π One token per provider
typeβ registers the OAuth access token forBitbucket,Dropbox,GitHub, orOneDrive - π·οΈ Subscription-level singleton β the resource ID is
/providers/Microsoft.Web/sourceControls/<type>; there is exactly one token per provider per subscription - π€ Bound to the running identity β the token belongs to the principal currently running Terraform; the service cannot manage another user's token
- π
sensitive = trueontokenandtoken_secretβ never echoed in plan output - π« No secret outputs β emits only
idandtype; the token is deliberately not re-exported - β
Case-sensitive enum validation β
typeis validated against the exact provider list at plan time (catching the commonGithubvsGitHubmistake)
π‘ Why it matters: App Service's "Deployment Center β external Git (GitHub)" flow needs an account-wide OAuth token registered before any app can be wired to a repo. This module makes that prerequisite explicit, versioned, and reviewable β instead of a token someone clicked into the portal once and nobody can find again.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
This is a subscription-level singleton prerequisite β it has no resource group, no location, and no upstream module dependency; it exists purely to unblock App Service source-control wiring downstream.
flowchart LR
EXT["External OAuth credential<br/>(GitHub PAT / Bitbucket token+secret)"]
THIS["tf-mod-azure-source-control<br/>(THIS MODULE)"]
ASC["azurerm_app_service_source_control<br/>(consumes token implicitly, via depends_on)"]
EXT -->|"token Β· token_secret"| THIS
THIS -->|"id (depends_on ordering)"| ASC
style THIS fill:#0078D4,color:#fff
This module consumes only an externally-sourced OAuth token / token_secret (Key Vault secret or pipeline variable β not another tf-mod-azure-* module); it emits id and type used purely for depends_on ordering so azurerm_app_service_source_control can consume the registered token implicitly β see the Typical wiring table.
A single, subscription-scoped singleton with no children:
flowchart TD
subgraph mod["tf-mod-azure-source-control"]
THIS["azurerm_source_control_token.this<br/>(keystone)<br/>OAuth token singleton keyed by provider type"]
TO["timeouts<br/>(dynamic Β· 0..1 when var.timeouts non-empty)"]
end
THIS --> TO
style THIS fill:#0078D4,color:#fff
.
βββ providers.tf # Terraform & azurerm version pins (no provider{} block)
βββ variables.tf # type (validated enum), token + token_secret (sensitive), timeouts
βββ main.tf # azurerm_source_control_token.this (total renderer)
βββ outputs.tf # id, type (no secret outputs)
βββ SCOPE.md # In-scope resource + emits table + provider gotchas
βββ README.md
provider "azurerm" { features {} }
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub" # case-sensitive: Bitbucket | Dropbox | GitHub | OneDrive
token = var.github_pat # PAT/OAuth token with `repo` and `workflow` scopes
}π Pass
tokenfrom a secret source (avarfed by a pipeline secret, or a Key Vault data lookup) β never a literal in source. It is stored in Terraform state, so protect the state backend accordingly.
Derived from the SCOPE.md Emits table β one row per output in outputs.tf.
| This module output | Feeds into |
|---|---|
id |
depends_on ordering for tf-mod-azure-app-service-source-control / azurerm_app_service_source_control β guarantees the account-wide token exists before an app is wired to a repo; audit/inventory references |
type |
Informational β confirms which provider the singleton authorizes; useful as a stable for_each map key when registering tokens for multiple providers |
βΉοΈ The downstream
azurerm_app_service_source_controlresource does not take the token as an argument β it consumes the token implicitly from the subscription. Wire ordering withdepends_on = [module.github_token], not by passing a value.
The design decisions and provider gotchas that save the next engineer hours:
- This is a singleton, not a per-app credential. There is one token per provider
typeper subscription, at/providers/Microsoft.Web/sourceControls/<type>. Registering it twice β from two modules, two stacks, or the portal plus Terraform β means the last writer wins and the others drift. Own each provider token in exactly one place. - Validated against AzureRM 4.77.0 β the underlying ARM resource provider is
Microsoft.Web(API version2023-01-01). - The resource exposes only three arguments β
type,token,token_secret. There is noname,resource_group_name,location, ortags: the token is a tenant/subscription singleton, not a regionally-scoped, RG-bound resource, so the standard variable order collapses totypeβ secrets βtimeouts. - No managed identity, no network surface, and no tag surface on this resource β by design of the underlying
azurerm_source_control_token, not an omission in the module. - The token belongs to the identity running Terraform. The service explicitly cannot manage another user's token. In CI/CD, the service principal / managed identity that runs the apply is the token owner β supply a PAT/OAuth token that that identity is entitled to use, not a personal developer token.
- Secrets in, no secrets out.
tokenandtoken_secretaresensitive = trueinputs. The module emits onlyidandtypeβ it deliberately does not re-export the token, so the credential never lands in a downstream module's plan output or state via this module's outputs. (It is still in this module's state β see Troubleshooting.) typecasing is a plan-time gate. The provider's validation is case-sensitive."github"or"Github"fail. The module'svalidation {}block fails the plan early with the legal values, instead of letting a malformed value reach the API.token_secretis for paired-credential providers only. Single-value providers (GitHub PAT/OAuth) leave itnull. It exists for OAuth1-style flows (e.g. Bitbucket) that issue a token and a secret. The module defaults it tonulland renders nothing when unset.- Terraform can't see token expiry. azurerm exposes neither
refresh_tokennorexpiration_time. A PAT/OAuth token that expires keeps showing as "present" to Terraform while downstream deployments silently start failing. Treat rotation as an operational calendar item, not somethingplanwill warn you about. typeis notForceNewin the schema β the resource implements an in-place update. In practice eachtypeis its own singleton, so changingtypetargets a different token rather than mutating the existing one.main.tfis a total renderer. No business logic β the only conditional is thedynamic "timeouts"block (rendered whenvar.timeoutsis non-empty), withtry(x, null)on each nested field.
1οΈβ£ Minimal β GitHub (the common case)
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat # needs `repo` and `workflow` scopes
}π The GitHub PAT/OAuth token must carry the
repoandworkflowscopes, or App Service deployments will fail to read the repo / trigger Actions.
2οΈβ£ GitHub token from Key Vault β no secret in source
data "azurerm_key_vault_secret" "github_pat" {
name = "github-deploy-pat"
key_vault_id = module.kv.id
}
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = data.azurerm_key_vault_secret.github_pat.value
}π Sourcing the token from Key Vault keeps it out of
.tfvarsand source control. It is still written to Terraform state β protect the state backend.
3οΈβ£ Token via CI/CD pipeline secret variable β no Key Vault dependency
variable "github_pat" {
type = string
sensitive = true
# populated at runtime as TF_VAR_github_pat by the pipeline β
# never committed to a.tfvars file or source control
}
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat
}π In Azure DevOps / GitHub Actions, mark the pipeline variable secret and let the runner export it as
TF_VAR_github_pat. The value never appears in pipeline logs or a checked-in.tfvarsfile β a simpler alternative to a Key Vault lookup for pipelines that already have a secure variable group.
4οΈβ£ Bitbucket β paired token + secret
module "bitbucket_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "Bitbucket"
token = var.bitbucket_token
token_secret = var.bitbucket_token_secret # required for OAuth1-style providers
}π‘
token_secretonly applies to providers that issue a paired credential. Leave it unset for GitHub.
5οΈβ£ Dropbox
module "dropbox_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "Dropbox"
token = var.dropbox_token
}6οΈβ£ OneDrive
module "onedrive_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "OneDrive"
token = var.onedrive_token
}7οΈβ£ Custom timeouts
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat
timeouts = {
create = "10m"
delete = "10m"
}
}βΉοΈ Each operation defaults to 5 minutes in the provider; override only if you hit timeouts.
8οΈβ£ Fully-loaded Bitbucket β paired credential + all four timeouts
module "bitbucket_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "Bitbucket"
token = var.bitbucket_token
token_secret = var.bitbucket_token_secret
timeouts = {
create = "10m"
read = "5m"
update = "10m"
delete = "10m"
}
}π‘ The complete input surface exercised in one call: a paired credential (
token+token_secret) plus all fourtimeoutsoperations. Contrast with the minimal example (1οΈβ£), which relies entirely on provider defaults.
9οΈβ£ Hardened / secure-by-default (explicit)
variable "github_pat" {
type = string
sensitive = true # mark the upstream variable sensitive too
}
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat
}π The module already marks
token/token_secretsensitive. Marking the caller's variablesensitive = truekeeps it out of CLI output and CI logs end-to-end. Scope the PAT to the least it needs (repo,workflow) and set a short expiry with a rotation reminder.
π All four providers β explicit sibling modules (no for_each)
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat
}
module "bitbucket_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "Bitbucket"
token = var.bitbucket_token
token_secret = var.bitbucket_token_secret
}
module "dropbox_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "Dropbox"
token = var.dropbox_token
}
module "onedrive_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "OneDrive"
token = var.onedrive_token
}π‘ Prefer explicit named module blocks over a
for_eachmap (next example) when the registrations have meaningfully different lifecycles or reviewers β each block gets its own plan diff and blast radius.
1οΈβ£1οΈβ£ for_each across providers β register several tokens from a map
variable "source_control_tokens" {
type = map(object({
type = string
token = string
}))
sensitive = true
# e.g. { gh = { type = "GitHub", token = "..." }, bb = { type = "Bitbucket", token = "..." } }
}
module "scm_tokens" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
for_each = var.source_control_tokens
type = each.value.type
token = each.value.token
}
β οΈ Eachtypeis a singleton β the map must not contain two entries with the sametype, or they fight over the one registration.
1οΈβ£2οΈβ£ Importing an existing token β adopt what the portal created
import {
to = module.github_token.azurerm_source_control_token.this
id = "/providers/Microsoft.Web/sourceControls/GitHub"
}
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat
}π‘ Or the CLI form:
terraform import 'module.github_token.azurerm_source_control_token.this' /providers/Microsoft.Web/sourceControls/GitHub. Note the ID'sGitHubcasing.
1οΈβ£3οΈβ£ CI/CD service-principal ownership
# When the pipeline's service principal runs the apply, IT becomes the token owner.
# Supply a PAT the SP is entitled to use β not a personal developer token.
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.ci_github_machine_user_pat # from a GitHub machine user / app installation
}
β οΈ The service does not support managing another user's token. A token tied to a departing employee's account breaks when that account is deprovisioned β use a machine user / GitHub App.
1οΈβ£4οΈβ£ Multi-subscription registration β via provider alias
provider "azurerm" {
features {}
}
provider "azurerm" {
alias = "shared_services"
subscription_id = var.shared_services_subscription_id
features {}
}
module "github_token_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat
}
module "github_token_shared" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
providers = {
azurerm = azurerm.shared_services
}
type = "GitHub"
token = var.github_pat_shared_services
}
β οΈ The token is a per-subscription singleton β/providers/Microsoft.Web/sourceControls/<type>is scoped to whichever subscription the provider block targets. Registering the sametypein two subscriptions requires two provider configurations (viaalias), not two calls against the same provider.
ποΈ End-to-end composition (finale) β token β App Service wired to a repo
provider "azurerm" { features {} }
# 1) Register the account-wide GitHub token (this module).
module "github_token" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-source-control?ref=v1.0.0"
type = "GitHub"
token = var.github_pat # repo + workflow scopes
}
# 2) An App Service to deploy.
module "app" {
source = "git::https://github.com/microsoftexpert/tf-mod-azure-linux-web-app?ref=v1.0.0"
name = "app-portal-prod"
resource_group_name = module.rg.name
location = module.rg.location
service_plan_id = module.plan.id
}
# 3) Wire the app to its repo β the token is consumed IMPLICITLY from the subscription,
# so order with depends_on rather than passing a value.
resource "azurerm_app_service_source_control" "portal" {
app_id = module.app.id
repo_url = "https://github.com/financialpartners/portal"
branch = "main"
use_manual_integration = false # uses the registered OAuth token
depends_on = [module.github_token] # β the only "wiring" β ensures the token exists first
}
output "scm_token_id" { value = module.github_token.id }π‘ The token registration and the per-app source-control link are separate resources on purpose: one account-wide credential, many apps wired to repos.
- Identity:
typeβ"Bitbucket"|"Dropbox"|"GitHub"|"OneDrive"(REQUIRED, case-sensitive) - Credentials:
token(REQUIRED,sensitive),token_secret(optional,sensitive, defaultnullβ paired-credential providers only) - Universal tail:
timeouts { create, read, update, delete }(optional, default{})
Full schemas for the inputs (mirroring variables.tf)
type = string # REQUIRED β "Bitbucket" | "Dropbox" | "GitHub" | "OneDrive" (case-sensitive)
token = string # REQUIRED, sensitive β OAuth access token (GitHub: repo + workflow scopes)
token_secret = string # optional, sensitive, default null β OAuth access token secret (e.g. Bitbucket)
timeouts = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
}) # default {}| Output | Description | Notes |
|---|---|---|
id |
Resource ID β /providers/Microsoft.Web/sourceControls/<type> |
always present |
type |
The provider type this token authorizes | always present |
π No
sensitiveoutputs and no token output. Thetoken/token_secretvalues are intentionally never exported β re-emitting them would leak the credential into consumers' state and plan output with no operational benefit.
- Secrets in, no secrets out:
token/token_secretaresensitiveinputs; the module emits identifiers only. - Secure by default: the only configurable secret surface is sensitive-flagged; the README steers callers to Key Vault / pipeline-secret sourcing and least-privilege scopes.
- Honest to the resource: no invented
name/resource_group_name/tags/identitysurface β the module exposes exactly whatazurerm_source_control_tokensupports, and no more. - Closed sets are validated:
typeis enforced with avalidation {}block against the exact case-sensitive provider enum;tokenis checked non-empty. - Small blast radius: one singleton per call; per-app repo links live in
azurerm_app_service_source_controland consume the token implicitly. main.tfas a total renderer: a singledynamic "timeouts"block,try(x, null)on every optional nested field.
terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output
β οΈ Pin the version. Reference the module by immutable tag β?ref=v1.0.0β never a branch. Branch references produce non-deterministic plans.
expected type to be one of ["Bitbucket" "Dropbox" "GitHub" "OneDrive"]β The value is mis-cased or misspelled. Validation is case-sensitive: use"GitHub"(not"Github"or"github"),"Bitbucket","Dropbox","OneDrive". The registry docs' Arguments table mislabels itGithubβ the working value isGitHub.- Plan/apply fights another writer; the token keeps drifting β The token is a subscription singleton per
type. Something else (the portal, another stack, a second module instance) is managing the same/providers/Microsoft.Web/sourceControls/<type>. Consolidate ownership to one place; import the existing token rather than creating a duplicate. Managing tokens for another user is not supportedβ The token is bound to the identity running Terraform. In CI/CD the service principal / managed identity that runs the apply is the owner β supply a token that identity is entitled to use (a GitHub machine user / App), not a personal developer PAT.- App Service deployment fails to read the repo or trigger Actions β The GitHub token lacks scopes. It needs
repoandworkflow. Re-issue the PAT/OAuth token with both, then updatetoken. - Deployments suddenly start failing but
terraform planshows no changes β The token expired. azurerm exposes noexpiration_time/refresh_token, so Terraform cannot detect expiry. Rotate the PAT/OAuth token on a calendar; updatingtokenre-registers it in place. - Token value visible where it shouldn't be β Ensure the caller's variable is also
sensitive = true. The module flagstoken/token_secretsensitive, but a non-sensitive upstreamvarcan still surface the value in CLI output and CI logs. - Bitbucket auth fails despite a valid token β OAuth1-style providers require the paired
token_secretas well β set both. token must be a non-empty stringβtokenresolved to""(often an unset pipeline secret or a failed Key Vault lookup). Confirm the secret is populated before the apply.- Destroying the module breaks live deployments β
terraform destroyremoves the account-wide token registration for thattype; any app relying on it loses its credential. Confirm no app still depends on the token before destroying.
- Terraform Registry β
azurerm_source_control_token(resource schema:type,token,token_secret) - Terraform Registry β
azurerm_source_control_token(data source β read-only lookup of an existing token) - Terraform Registry β
azurerm_app_service_source_control(wires an individual app to a repo using the registered token) - Microsoft Learn β Microsoft.Web/sourcecontrols resource reference (ARM/Bicep schema, including the
refreshToken/expirationTimefields azurerm does not surface) - Microsoft Learn β Continuous deployment to Azure App Service (Deployment Center, external Git providers)
- GitHub Docs β Personal access tokens and required scopes (
repo,workflow)
π "Infrastructure as Code should be standardized, consistent, and secure."