The Platform Engineering Way to Manage Google Workspace Users
By Weston Platter
Table of Contents
- Growing Pains
- Why This Matters for Scaling Teams
- Managing Google Workspace with Terraform
- Getting Started With the Module
- Module Design Decisions
- Wrapping Up
Growing Pains
As Masterpoint has been around for almost 10 years now, we’ve experienced growing pains transitioning from a one-person consultancy to a team of full-time Infrastructure as Code engineers.
During that time, we’ve focused on client projects far more than our internal systems. When I joined in December, we started to feel the friction of onboarding a new team member, including adding their Google Workspace account. Matt (our CEO/CTO) had to:
- remember what permissions a new Google Workspace user gets by default
- how to provision SSO permissions for Masterpoint’s AWS Accounts
- how to get a user set up for client-specific SSOs
Since the last onboarding was about 6 months prior, important details weren’t fresh in mind, which resulted in my delayed access to the Masterpoint AWS accounts. In the end, he had to take time and focus away from other work due to these tedious administrative tasks.
Why This Matters for Scaling Teams
In our small company and possibly in your own organization, it’s not a big deal for the founder or early engineer to create a new employee’s Google Workspace account or give them SSO account access via the Admin UI. But as a company scales:
- Founders and engineers are focused on product, customers, and strategy.
- Nobody remembers why or when a permission was changed.
- Group membership becomes inconsistent.
- Onboarding/offboarding gets slower and error-prone.
- The consequences for getting security permissions incorrect increase.
As a company grows past ten, dozens, or a hundred people, teams (and dare we say departments) become more distributed. It is harder to manage user accounts across many different SaaS tools. It’s important to have systems in place that provide clarity and enable individuals to get the right access to get their work done in a timely manner. We believe efficient systems are part of a company evolving toward higher levels of organizational maturity.
One aspect of that is finding a better way to manage your Google Workspace accounts.
Managing Google Workspace with Terraform
We looked at a couple of open source solutions and decided on using the Terraform Google Workspace provider. We often use Terraform to provision and manage user accounts within SaaS products (for example, we created a module to provision user accounts for DataDog), so using Terraform to provision and manage Google users felt like an easy decision.
We heard great things about GAM (an imperative command line tool) from a few colleagues, but we didn’t need its full range of capabilities. We prefer to use declarative systems so we know the user account and group settings we see in config files are indeed the values in production.
While there are a few Terraform modules out there for managing Google Workspaces, we decided to create our own Terraform module to use the provider. The motivation to build a module from scratch stemmed from our desire to:
- Easily manage user specific SSO settings allowing engineers to sign into multiple AWS accounts using their Google Identity.
- Organize
group
andgroup_setting
variable inputs in clear and straight forward approach (more details on this in Design Decision #2) - Create default values for
user
,group
, andgroup_setting
making our config files easier to work with (see this example).
Here’s the GitHub link for our Google Workspace module: https://github.com/masterpointio/terraform-googleworkspace-users-groups-automation
Getting Started With the Module
To make the module easy to get up and running with your own Google Workspace, we included two practical examples:
Import existing Google Workspace users and groups We expect most people will use the module with an existing Google Workspace. To make this easy, we included the Terraform and YAML configuration files we used to import our own workspace users and groups.
The key component is the
imports.tf
file, which shows how to map existing Google Workspace resources to the module’s resources. We also included a Python script to help debug by printing out the JSON objects as rendered by the Google APIs.Since importing cloud resources into Terraform modules can be tricky, we documented our complete approach in import-existing-org. The example includes these key files:
- main.tf - Module configuration
- imports.tf - Import mappings
- users.yaml - User definitions with YAML anchors
- groups.yaml - Group definitions with YAML anchors
Create new users and groups For users who don’t need to import users or groups, follow this example. This is a great starting point if you’re setting up a new org or only creating new users and groups.
After the initial import or setup, your Google Workspace root module could end up being as simple as this:
locals {
oauth_scopes = [
"https://www.googleapis.com/auth/admin.directory.group",
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/admin.directory.userschema",
"https://www.googleapis.com/auth/apps.groups.settings",
"https://www.googleapis.com/auth/iam",
]
}
provider "googleworkspace" {
customer_id = "my_customer"
credentials = local.secrets["googleworkspace_admin_credentials_json"]
impersonated_user_email = var.impersonated_user_email
oauth_scopes = local.oauth_scopes
}
# Get users and groups from YAML files. You might choose to use other config files.
# Note: path.module refers to the directory where terraform/tofu plan/apply is run
# var.googleworkspace_configs is a variable for the directory containing config files
locals {
config_path = "${path.module}/${var.googleworkspace_configs}"
all_groups = yamldecode(file("${local.config_path}/groups.yaml"))
all_users = yamldecode(file("${local.config_path}/users.yaml"))
# Skip objects that start with "_", which we use as default or prototype objects
groups = { for k, v in local.all_groups : k => v if !startswith(k, "_") }
users = { for k, v in local.all_users : k => v if !startswith(k, "_") }
}
module "googleworkspace_users_groups" {
source = "masterpointio/users-groups-automation/googleworkspace"
# version = "X.X.X" # it's a best practice to version pin modules
groups = local.groups
users = local.users
}
The above code references users.yaml
and groups.yaml
files that contain your actual user and group configurations.
Here’s an example users.yaml
file, which uses YAML anchors to share user defaults and Google + AWS SSO configurations across team members:
---
_default_user: &default_user
is_admin: false
groups:
company: { role: member }
engineering: { role: member }
_custom_schemas_client1: &_custom_schemas_client1
schema_name: AWS_SSO_for_Client1
schema_values:
Role: '["arn:aws:iam::111111111111:role/GoogleAppsAdmin","arn:aws:iam::111111111111:saml-provider/GoogleApps"]'
user1.last@example.com:
<<: *default_user
primary_email: user1.last@example.com
given_name: User1
family_name: Last
custom_schemas:
- <<: *_custom_schemas_client1
user2.last@example.com:
<<: *default_user
primary_email: user2.last@example.com
given_name: User2
family_name: Last
custom_schemas:
- <<: *_custom_schemas_client1
And here’s an example groups.yaml
file, also leveraging YAML anchors to apply sane defaults to multiple groups.
---
_default_active_settings: &_default_active_settings
allow_external_members: false
allow_web_posting: true
archive_only: false
custom_roles_enabled_for_settings_to_be_merged: false
enable_collaborative_inbox: false
is_archived: false
primary_language: en_US
who_can_join: ALL_IN_DOMAIN_CAN_JOIN
who_can_assist_content: OWNERS_AND_MANAGERS
who_can_view_group: ALL_IN_DOMAIN_CAN_VIEW
who_can_view_membership: ALL_IN_DOMAIN_CAN_VIEW
company:
email: company@example.com
name: All Company Team
description: Includes everyone in the company
is_admin: false
settings:
<<: *_default_active_settings
engineering:
email: engineering@example.com
name: Engineering Team
description: Engineering Team that all technical employees are members of by default
is_admin: false
settings:
<<: *_default_active_settings
Module Design Decisions
Below we point out a few Terraform module design decisions we made to reduce friction as others use the module.
Design Decision #1 - Implementing Integration and Validation Tests
To ensure the Terraform module remains reliable after changes—whether by contributors or automated processes—we’ve added approximately 20 tests. Thankfully Terraform and OpenTofu both include a built-in testing framework that allows us to write tests directly in HCL, replacing the previous Go-based approach. The tests validate that the module behaves as expected and prevent new changes from unintentionally breaking functionality that downstream consumers rely on.
We’ll skip over the happy path tests (example 1, example 2, example 3) we added to validate things like email and password validation, since these basic unit tests can be easily generated using Cursor, GitHub Copilot, or other AI code generation tools.
Below are more complex examples validating integration between different provider resources.
Validate that user and group inputs result in a user being a group member. We view this as an integration test because we are testing if a user and a group correctly integrate and result in a user_to_group instance.
run "groups_member_role_success" { command = apply providers = { googleworkspace = googleworkspace.mock } variables { users = { "first.last@example.com" = { primary_email = "first.last@example.com" family_name = "Last" given_name = "First" groups = { "team" = { role = "MEMBER" } } } } groups = { "team" = { name = "Team" email = "team@example.com" } } } assert { condition = googleworkspace_group_member.user_to_groups["team@example.com/first.last@example.com"].role == "MEMBER" error_message = "Expected 'role' to be 'MEMBER'." } }
Test validations in the
variables.tf
file. We added variable validations to catch bad inputs early. This provides a fast feedback loop during aterraform plan
rather than only getting this feedback from the Google Admin SDK APIs when runningterraform apply
. During Infrastructure as Code audits, we look at Terraform CI/CD workflows to ensure PRs areterraform plan
-ed before merging to catch issues where a variable input has a bad value.# users variable declaration variable "users" { description = "List of users" type = map(object({ # other variable fields groups: optional(map(object({ role: optional(string, "MEMBER"), delivery_settings: optional(string, "ALL_MAIL"), type: optional(string, "USER"), })), {}), primary_email : string, })) # validate users.groups.[group_key].type validation { condition = alltrue(flatten([ for user in var.users: [ for group in values(try(user.groups, {})): ( group.type == null || contains(["USER", "GROUP", "CUSTOMER"], upper(group.type)) ) ] ])) error_message = "group type must be either 'USER', 'GROUP', or 'CUSTOMER'" } } run "group_member_type_invalid" { command = plan providers = { googleworkspace = googleworkspace.mock } variables { users = { "invalid.type@example.com" = { primary_email = "invalid.type@example.com" family_name = "Type" given_name = "Invalid" groups = { "test-group" = { type = "INVALID-TYPE" } } } } groups = { "test-group" = { name = "Test Group" email = "test-group@example.com" } } } # we expect the users variable to fail the "users.groups.[group_key].type" validation block expect_failures = [var.users] }
There are 4 more complex tests, helping us ensure this module won’t break in the future.
Design Decision #2 - Choosing Intuitive Terraform Variable Structures
In the Google Workspace provider, provisioning a group involves declaring two resources: group
and group_settings
.
resource "googleworkspace_group" "sales" {
email = "sales@example.com"
}
resource "googleworkspace_group_settings" "sales_settings" {
email = googleworkspace_group.sales.email
who_can_join = "INVITED_CAN_JOIN"
}
This group
and group_setting
resource design mirrors Google’s Admin SDK REST API structure. However, we felt that group settings more intuitively belonged as a nested attribute inside a group. So in our group variable, we added settings
. Then, later, we extracted settings in the module’s local
block to meet the provider’s expectations:
# groups variable declaration
variable "groups" {
description = "List of groups"
type = map(object({
name: string,
email: string,
settings: optional(object({
who_can_join: optional(string),
}))
}))
}
# locals block
locals {
group_settings = {
for k, v in var.groups : k => merge(v.settings, { email = v.email })
}
}
# group_settings resource
resource "googleworkspace_group_settings" "defaults" {
for_each = local.group_settings
email = each.value.email
who_can_join = "INVITED_CAN_JOIN"
}
Yes, it’s an abstraction which can sometimes lead to issues, but we think reducing cognitive friction for the module user is worth the added business logic in the Terraform code. Here’s an example of a simpler configuration for groups:
locals {
default_group_settings = {
who_can_join = "ALL_IN_DOMAIN_CAN_JOIN"
}
}
module "googleworkspace" {
source = "masterpointio/users-groups-automation/googleworkspace"
version = "X.X.X"
groups = {
support = {
name = "Support"
email = "support@example.com"
settings = merge(local.default_group_settings, {})
}
engineers = {
name = "Engineering"
email = "engineering@example.com"
settings = merge(local.default_group_settings, {
who_can_join = "INVITED_CAN_JOIN"
})
}
}
}
Wrapping Up
With this setup in place and a CODEOWNERS
file, we’re now transparently, consistently, and securely managing users and groups within Google Workspace. This helps our engineering leader, Matt, avoid having to keep account setup details in his head. It also enables us as a team to avoid waiting on any one person. Need an adjustment to your Google user - like enabling AWS SSO for a specific user? Open a PR, get the right approval from the CODEOWNERS
, merge it, and automation takes care of the rest. We love this workflow and we hope it’s useful to you and your team!
Want help automating your Google Workspace configuration with Terraform? Need to make sure your Workspace policies stay consistent and auditable? Want to avoid onboarding chokepoints like Matt used to be?
We do short, high-impact automation projects like this for engineering teams all the time—with long-term impact. If your onboarding still runs on sticky notes and muscle memory, reach out and we’ll streamline your onboarding process.
👋 If you're ready to take your infrastructure to the next level, we're here to help. We love to work together with engineering teams to help them build well-documented, scalable, automated IaC that make their jobs easier. Get in touch!