Skip to content

microsoftexpert/tf-mod-azure-source-control

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”— Azure Source Control Token Terraform Module

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, sensitive token inputs, and zero secret leakage in outputs. Built for AzureRM v4.x.

Terraform AzureRM module type


🧩 Overview

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 for Bitbucket, Dropbox, GitHub, or OneDrive
  • 🏷️ 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 = true on token and token_secret β€” never echoed in plan output
  • 🚫 No secret outputs β€” emits only id and type; the token is deliberately not re-exported
  • βœ… Case-sensitive enum validation β€” type is validated against the exact provider list at plan time (catching the common Github vs GitHub mistake)

πŸ’‘ 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.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


πŸ—ΊοΈ Where this fits in the family

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
Loading

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.


🧬 What this module builds

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
Loading

πŸ“ Module Structure

.
β”œβ”€β”€ 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

βš™οΈ Quick Start

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 token from a secret source (a var fed 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.


πŸ”Œ Typical wiring

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_control resource does not take the token as an argument β€” it consumes the token implicitly from the subscription. Wire ordering with depends_on = [module.github_token], not by passing a value.


🧠 Architecture Notes

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 type per 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 version 2023-01-01).
  • The resource exposes only three arguments β€” type, token, token_secret. There is no name, resource_group_name, location, or tags: the token is a tenant/subscription singleton, not a regionally-scoped, RG-bound resource, so the standard variable order collapses to type β†’ 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. token and token_secret are sensitive = true inputs. The module emits only id and type β€” 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.)
  • type casing is a plan-time gate. The provider's validation is case-sensitive. "github" or "Github" fail. The module's validation {} block fails the plan early with the legal values, instead of letting a malformed value reach the API.
  • token_secret is for paired-credential providers only. Single-value providers (GitHub PAT/OAuth) leave it null. It exists for OAuth1-style flows (e.g. Bitbucket) that issue a token and a secret. The module defaults it to null and renders nothing when unset.
  • Terraform can't see token expiry. azurerm exposes neither refresh_token nor expiration_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 something plan will warn you about.
  • type is not ForceNew in the schema β€” the resource implements an in-place update. In practice each type is its own singleton, so changing type targets a different token rather than mutating the existing one.
  • main.tf is a total renderer. No business logic β€” the only conditional is the dynamic "timeouts" block (rendered when var.timeouts is non-empty), with try(x, null) on each nested field.

πŸ“š Example Library (copy-paste)

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 repo and workflow scopes, 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 .tfvars and 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 .tfvars file β€” 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_secret only 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 four timeouts operations. 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_secret sensitive. Marking the caller's variable sensitive = true keeps 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_each map (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
}

⚠️ Each type is a singleton β€” the map must not contain two entries with the same type, 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's GitHub casing.

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 same type in two subscriptions requires two provider configurations (via alias), 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.


πŸ“¦ Inputs (high-level)

  • Identity: type β€” "Bitbucket" | "Dropbox" | "GitHub" | "OneDrive" (REQUIRED, case-sensitive)
  • Credentials: token (REQUIRED, sensitive), token_secret (optional, sensitive, default null β€” 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 {}

🧾 Outputs

Output Description Notes
id Resource ID β€” /providers/Microsoft.Web/sourceControls/<type> always present
type The provider type this token authorizes always present

πŸ”’ No sensitive outputs and no token output. The token/token_secret values are intentionally never exported β€” re-emitting them would leak the credential into consumers' state and plan output with no operational benefit.


🧱 Design Principles

  • Secrets in, no secrets out: token/token_secret are sensitive inputs; 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/identity surface β€” the module exposes exactly what azurerm_source_control_token supports, and no more.
  • Closed sets are validated: type is enforced with a validation {} block against the exact case-sensitive provider enum; token is checked non-empty.
  • Small blast radius: one singleton per call; per-app repo links live in azurerm_app_service_source_control and consume the token implicitly.
  • main.tf as a total renderer: a single dynamic "timeouts" block, try(x, null) on every optional nested field.

πŸš€ Runbook

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.


πŸ” Troubleshooting

  • 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 it Github β€” the working value is GitHub.
  • 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 repo and workflow. Re-issue the PAT/OAuth token with both, then update token.
  • Deployments suddenly start failing but terraform plan shows no changes β†’ The token expired. azurerm exposes no expiration_time/refresh_token, so Terraform cannot detect expiry. Rotate the PAT/OAuth token on a calendar; updating token re-registers it in place.
  • Token value visible where it shouldn't be β†’ Ensure the caller's variable is also sensitive = true. The module flags token/token_secret sensitive, but a non-sensitive upstream var can still surface the value in CLI output and CI logs.
  • Bitbucket auth fails despite a valid token β†’ OAuth1-style providers require the paired token_secret as well β€” set both.
  • token must be a non-empty string β†’ token resolved 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 destroy removes the account-wide token registration for that type; any app relying on it loses its credential. Confirm no app still depends on the token before destroying.

πŸ”— Related Docs

  • 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/expirationTime fields 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."

About

No description or website provided.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages