Invite and manage a Cloudflare account member with scoped roles or fine-grained policies β targeting
cloudflare/cloudflare ~> 5.0.
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.
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!
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;
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;
Resource inventory
| Resource | Name | Cardinality | Role |
|---|---|---|---|
cloudflare_account_member |
this |
1 (keystone) | An account membership + access grant. |
| 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
rolesorpolicies. This module rejects a member with neither. Prefer scopedpoliciesfor least privilege. β οΈ policies,permission_groups, andresource_groupsare sets. Order doesn't matter; ids must be real.- βΉοΈ
statusdefaults topending(an invitation is sent). Theuserdetails (name, MFA state) are provider-computed and read-only. - βΉοΈ
emailchanges identify a different person β treat a change as removing one member and adding another.
Account SettingsΒ· ReadAccount SettingsΒ· Write
- Administrative access to the target account and its
account_id. - The role ids and/or permission group / resource group ids you intend to grant.
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
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]
}
}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 |
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.
| 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}
}| 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. |
- Roles vs policies. Legacy
rolesgrant broad predefined access;policiesgrant 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, andresource_groupsare 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
userdetails.
| 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. |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin by immutable tag
?ref=v1.0.0, never a branch.
- β
terraform validateβ parsesmember, 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.
$ terraform output
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
email = "new.hire@example.com"
id = "4536bcfad5faccb111b47003c79917fa"
status = "pending"
| 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. |
- 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."