Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ‘€ Cloudflare Account Member Terraform Module

Invite and manage a Cloudflare account member with scoped roles or fine-grained policies β€” targeting cloudflare/cloudflare ~> 5.0.

Terraform Provider Module Version Type Resources

🧩 Overview

This module manages one cloudflare_account_member:

  • πŸ‘€ Invite by email β€” the member receives an invitation at the address you specify.
  • 🎯 Least-privilege access β€” grant fine-grained policies (permission groups scoped to resource groups) rather than broad legacy roles.
  • πŸ”’ No credentials handled β€” the module manages membership and access grants only; it never touches the member's password or MFA.

πŸ’‘ Why it matters: account membership is an IAM control. This module validates that every member is granted something (a role or a policy) and steers you toward scoped policies over blanket roles.

❀️ 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

graph LR
  acct["Cloudflare account (account_id)"]:::ext
  mem["terraform-cloudflare-account-member (this module)"]:::this
  res["cloudflare_account_member"]:::keystone
  pg["Roles / permission groups / resource groups (by id)"]:::ext
  user["Invited user (email)"]:::ext

  acct -->|"account_id"| mem
  pg -->|"role / group ids"| mem
  mem -->|"manages"| res
  mem -->|"invitation"| user

  classDef this fill:#F38020,color:#fff,stroke:#F38020;
  classDef keystone fill:#FBAD41,color:#000,stroke:#FBAD41;
  classDef ext fill:#eeeeff,color:#333,stroke:#9999ff;
Loading

🧬 What this module builds

graph TD
  aid["account_id"]:::in
  m["member = email, roles, policies, status"]:::in
  this["cloudflare_account_member.this"]:::this
  oid["output: id"]:::out
  oem["output: email"]:::out
  ost["output: status"]:::out

  aid --> this
  m --> this
  this --> oid
  this --> oem
  this --> ost

  classDef this fill:#F38020,color:#fff,stroke:#F38020;
  classDef in fill:#f5f5f5,color:#333,stroke:#cccccc;
  classDef out fill:#eeeeff,color:#333,stroke:#9999ff;
Loading

Resource inventory

Resource Name Cardinality Role
cloudflare_account_member this 1 (keystone) An account membership + access grant.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider cloudflare/cloudflare ~> 5.0
Provider block None β€” the caller configures the provider and supplies CLOUDFLARE_API_TOKEN out of band.
Scope account_id (per-resource input, not provider config).

Schema notes that bite (verified against the live provider schema):

  • πŸ”’ A member needs roles or policies. This module rejects a member with neither. Prefer scoped policies for least privilege.
  • ⚠️ policies, permission_groups, and resource_groups are sets. Order doesn't matter; ids must be real.
  • ℹ️ status defaults to pending (an invitation is sent). The user details (name, MFA state) are provider-computed and read-only.
  • ℹ️ email changes identify a different person β€” treat a change as removing one member and adding another.

πŸ”‘ Required Cloudflare API Token Permissions

  • Account Settings Β· Read
  • Account Settings Β· Write

Cloudflare Prerequisites

  • Administrative access to the target account and its account_id.
  • The role ids and/or permission group / resource group ids you intend to grant.

πŸ“ Module Structure

terraform-cloudflare-account-member/
β”œβ”€β”€ providers.tf     # terraform{} + required_providers (cloudflare ~> 5.0); no provider block
β”œβ”€β”€ variables.tf     # account_id + member{} β€” email + roles/policies (at least one required)
β”œβ”€β”€ main.tf          # cloudflare_account_member.this
β”œβ”€β”€ outputs.tf       # id first, then account_id, email, status
β”œβ”€β”€ README.md        # this document
β”œβ”€β”€ SCOPE.md         # cross-module contract
β”œβ”€β”€ LICENSE          # MIT
└── .gitignore       # canonical library ignore set

βš™οΈ Quick Start

provider "cloudflare" {}
# export CLOUDFLARE_API_TOKEN=... (scoped to Account Settings read/write)

module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email = "new.hire@example.com"
    roles = [var.analyst_role_id]
  }
}

πŸ”Œ Cross-Module Contract

Consumes

Input Type Typical source
account_id string the account
member object({...}) caller (role / permission-group / resource-group ids)

Emits

Output Description Consumed by
id Membership identifier audit
account_id Account scope (echoed) composition
email Member email audit
status Invitation status onboarding checks

πŸ“š Example Library

1 Β· Minimal β€” invite with a role
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member     = { email = "alex@example.com", roles = [var.analyst_role_id] }
}
2 Β· Fine-grained policy (preferred)
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email = "dns.admin@example.com"
    policies = [{
      access            = "allow"
      permission_groups = [{ id = var.dns_write_pg_id }]
      resource_groups   = [{ id = var.prod_zones_rg_id }]
    }]
  }
}

πŸ”’ Scoped policies beat broad roles β€” grant exactly the permission groups on exactly the resource groups needed.

3 Β· Multiple roles
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member     = { email = "lead@example.com", roles = [var.analyst_role_id, var.billing_role_id] }
}
4 Β· Deny policy (carve-out)
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email = "contractor@example.com"
    policies = [
      { access = "allow", permission_groups = [{ id = var.dns_read_pg_id }], resource_groups = [{ id = var.all_zones_rg_id }] },
      { access = "deny", permission_groups = [{ id = var.dns_read_pg_id }], resource_groups = [{ id = var.secret_zone_rg_id }] },
    ]
  }
}
5 Β· Read-only analyst
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email = "analyst@example.com"
    policies = [{
      access            = "allow"
      permission_groups = [{ id = var.analytics_read_pg_id }]
      resource_groups   = [{ id = var.all_zones_rg_id }]
    }]
  }
}
6 Β· Multiple permission groups in one policy
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email = "ops@example.com"
    policies = [{
      access            = "allow"
      permission_groups = [{ id = var.dns_write_pg_id }, { id = var.cache_purge_pg_id }]
      resource_groups   = [{ id = var.prod_zones_rg_id }]
    }]
  }
}
7 Β· Roles + policies together
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email    = "hybrid@example.com"
    roles    = [var.billing_role_id]
    policies = [{ access = "allow", permission_groups = [{ id = var.dns_read_pg_id }], resource_groups = [{ id = var.all_zones_rg_id }] }]
  }
}
8 Β· Many members from one definition (for_each)
locals {
  team = {
    "alex@example.com" = [var.analyst_role_id]
    "sam@example.com"  = [var.analyst_role_id, var.billing_role_id]
  }
}

module "members" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  for_each   = local.team
  account_id = var.cloudflare_account_id
  member     = { email = each.key, roles = each.value }
}
9 Β· Reading the membership id
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member     = { email = "alex@example.com", roles = [var.analyst_role_id] }
}

output "membership_id" { value = module.member.id }
output "invite_status" { value = module.member.status }
10 Β· Environment-scoped access
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member = {
    email = "dev@example.com"
    policies = [{
      access            = "allow"
      permission_groups = [{ id = var.dns_write_pg_id }]
      resource_groups   = [{ id = var.nonprod_zones_rg_id }] # dev can't touch prod
    }]
  }
}
11 Β· Explicit pending invite
module "member" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  account_id = var.cloudflare_account_id
  member     = { email = "future@example.com", roles = [var.analyst_role_id], status = "pending" }
}
12 Β· πŸ—οΈ End-to-end composition β€” onboard a team with scoped access
provider "cloudflare" {}

variable "cloudflare_account_id" { type = string }
variable "dns_write_pg_id" { type = string }
variable "prod_zones_rg_id" { type = string }

locals {
  dns_admins = ["alex@example.com", "sam@example.com"]
}

module "dns_admins" {
  source     = "git::https://github.com/microsoftexpert/terraform-cloudflare-account-member.git?ref=v1.0.0"
  for_each   = toset(local.dns_admins)
  account_id = var.cloudflare_account_id
  member = {
    email = each.value
    policies = [{
      access            = "allow"
      permission_groups = [{ id = var.dns_write_pg_id }]
      resource_groups   = [{ id = var.prod_zones_rg_id }]
    }]
  }
}

output "invited" { value = { for e, m in module.dns_admins : e => m.status } }

πŸ—οΈ A whole team is onboarded with identical, least-privilege, auditable access in one definition.

πŸ“₯ Inputs

Name Type Required Default Description
account_id string βœ… β€” Account to add the member to.
member object({...}) βœ… β€” Email + roles/policies (at least one) + optional status.
Full input schema (from variables.tf)
variable "member" {
  type = object({
    email = string
    roles = optional(set(string), [])
    policies = optional(list(object({
      access            = string # "allow" | "deny"
      permission_groups = list(object({ id = string }))
      resource_groups   = list(object({ id = string }))
    })), [])
    status = optional(string)
  })
  # validation: email non-empty; at least one role or policy; each policy.access ∈ {allow,deny}
}

🧾 Outputs

Output Description Notes
id Membership identifier Primary reference.
account_id Account scope (echoed) For composition.
email Member email β€”
status Invitation status e.g. pending, accepted.

🧠 Architecture Notes

  • Roles vs policies. Legacy roles grant broad predefined access; policies grant permission groups scoped to resource groups. Validation guarantees at least one is present, so a member is never created with no access at all.
  • Sets, not lists. policies, permission_groups, and resource_groups are sets β€” order is irrelevant and duplicates collapse.
  • Identity, not credentials. The module manages the membership and its grants; the user's authentication (password, MFA) is theirs, reflected read-only in the computed user details.

🧱 Design Principles

Concern Secure default How to opt out (deliberately)
Access grant At least one role or policy required n/a β€” a member with no access is rejected.
Least privilege Scoped policies encouraged over broad roles Use a broad role explicitly.
Secrets None accepted or emitted n/a.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin by immutable tag ?ref=v1.0.0, never a branch.

πŸ§ͺ Testing

  • βœ… terraform validate β€” parses member, enforces email presence, at-least-one grant, and the access enum.
  • βœ… terraform fmt -check β€” canonical formatting.
  • β›” Not offline: role / permission-group / resource-group id validity is only checked at a real plan/apply.

πŸ’¬ Example Output

$ terraform output
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
email      = "new.hire@example.com"
id         = "4536bcfad5faccb111b47003c79917fa"
status     = "pending"

πŸ” Troubleshooting

Symptom Cause Fix
member must be granted access via at least one role or one policy Both empty Provide a role or a policy.
each policy access must be "allow" or "deny" Bad access value Use allow or deny.
Invite never arrives Wrong email / already a member Verify the email and existing memberships.
Unknown role/permission id Made-up id Look up ids via the roles / permission-groups data sources.
Provider auth error No/insufficient token Configure the provider with an Account Settings token.

πŸ”— Related Docs

  • Cloudflare provider β€” cloudflare_account_member
  • Sibling module: terraform-cloudflare-api-token (token-based IAM)
  • This module's SCOPE.md β€” the cross-module contract.

🧑 "Infrastructure as Code should be standardized, consistent, and secure."

About

Terraform module: terraform-cloudflare-account-member

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages