Skip to content

Commit

Permalink
Circleci OIDC (#148)
Browse files Browse the repository at this point in the history
* feat(google_deployment_accounts): support circleci oidc

* chore(google_deployment_accounts): add a fixme for 2024

* feat(google_deployment_accounts): add opinionated circleci interface

* feat(google_deployment_accounts): allow overriding display name

* chore(google_deployment_accounts): update docs

* chore(google_deployment_accounts): add examples

* chore(google_deployment_accounts): fix up docs and interface

* chore(google_deployment_accounts): fixups post rebase
  • Loading branch information
whd authored Feb 27, 2024
1 parent 0bb62e3 commit 30b7e2d
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 18 deletions.
21 changes: 13 additions & 8 deletions google_deployment_accounts/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Terraform Module: Service Accounts for deployment from GitHub Actions
Creates a Cloud IAM service accounts which let GitHub Actions workflows authenticate to GKE.
# Terraform Module: Service Accounts for deployment from GitHub Actions and CircleCI
Creates a Cloud IAM service account which lets CI workflows authenticate to GCP.

## Requirements

Expand All @@ -23,20 +23,25 @@ No modules.
| Name | Type |
|------|------|
| [google_service_account.account](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource |
| [google_service_account_iam_binding.circleci-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource |
| [google_service_account_iam_binding.github-actions-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_account_id"></a> [account\_id](#input\_account\_id) | Name of the service account. Defaults to deploy-<env> | `string` | `null` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Environment e.g., stage. | `string` | n/a | yes |
| <a name="input_account_id"></a> [account\_id](#input\_account\_id) | Name of the service account. Defaults to deploy-ENV. | `string` | `null` | no |
| <a name="input_circleci_attribute_specifiers"></a> [circleci\_attribute\_specifiers](#input\_circleci\_attribute\_specifiers) | (CircleCI only) Set of attribute specifiers to allow deploys from, in the form ATTR/ATTR\_VALUE. If specified, this overrides the github\_repository variable and any other CircleCI-specific variables. | `set(string)` | `[]` | no |
| <a name="input_circleci_branches"></a> [circleci\_branches](#input\_circleci\_branches) | (CircleCI only) Branches to allow deployments from. If unspecified, allow deployment from all branches. | `set(string)` | `[]` | no |
| <a name="input_circleci_context_ids"></a> [circleci\_context\_ids](#input\_circleci\_context\_ids) | (CircleCI only) Contexts to allow deployments from. Not recommended when using merge queues since CircleCI Contexts are only accessible to members of your organization. | `set(string)` | `[]` | no |
| <a name="input_display_name"></a> [display\_name](#input\_display\_name) | Display name for the service account. Defaults to "Deployment to the ENV environment". | `string` | `null` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Environment e.g., stage. Not used for OIDC configuration in CircleCI. | `string` | n/a | yes |
| <a name="input_gha_environments"></a> [gha\_environments](#input\_gha\_environments) | Github environments from which to deploy. If specified, this overrides the environment variable. | `list(string)` | `[]` | no |
| <a name="input_github_repositories"></a> [github\_repositories](#input\_github\_repositories) | The Github repositories running the deployment workflows in the format org/repository, overwriting the github\_deployment variable | `list(string)` | `[]` | no |
| <a name="input_github_repository"></a> [github\_repository](#input\_github\_repository) | The Github repository running the deployment workflows in the format org/repository | `string` | n/a | yes |
| <a name="input_github_repositories"></a> [github\_repositories](#input\_github\_repositories) | The Github repositories running the deployment workflows in the format org/repository, will be used if github\_repository is not defined. | `list(string)` | `[]` | no |
| <a name="input_github_repository"></a> [github\_repository](#input\_github\_repository) | The Github repository running the deployment workflows in the format org/repository. Optional for CircleCI or when github\_repositories is specified. | `string` | `null` | no |
| <a name="input_project"></a> [project](#input\_project) | n/a | `string` | `null` | no |
| <a name="input_wip_name"></a> [wip\_name](#input\_wip\_name) | The name of the workload identity provider | `string` | `"github-actions"` | no |
| <a name="input_wip_project_number"></a> [wip\_project\_number](#input\_wip\_project\_number) | The project number of the project the workload identity provider lives in | `number` | n/a | yes |
| <a name="input_wip_name"></a> [wip\_name](#input\_wip\_name) | The name of the workload identity provider. This value implicitly controls whether to provision access to github-actions or circleci. | `string` | `"github-actions"` | no |
| <a name="input_wip_project_number"></a> [wip\_project\_number](#input\_wip\_project\_number) | The project number of the project the workload identity provider lives in. | `number` | n/a | yes |

## Outputs

Expand Down
18 changes: 18 additions & 0 deletions google_deployment_accounts/examples/circleci.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Allow OIDC access from CircleCI jobs triggered in a specific repo
data "terraform_remote_state" "wip_project" {
backend = "gcs"

config = {
bucket = "my-wip-project"
prefix = "wip-project/prefix"
}
}

module "google_deployment_accounts" {
source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main"
project = "my-project"
environment = "stage"
github_repository = "org/project"
wip_name = "circleci"
wip_project_number = data.terraform_remote_state.wip_project.number
}
20 changes: 20 additions & 0 deletions google_deployment_accounts/examples/circleci2.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Allow OIDC access from CircleCI jobs triggered on the main branch only of a
# specific repo
data "terraform_remote_state" "wip_project" {
backend = "gcs"

config = {
bucket = "my-wip-project"
prefix = "wip-project/prefix"
}
}

module "google_deployment_accounts" {
source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main"
project = "my-project"
environment = "stage"
github_repository = "org/project"
wip_name = "circleci"
wip_project_number = data.terraform_remote_state.wip_project.number
circleci_branches = ["main"]
}
30 changes: 30 additions & 0 deletions google_deployment_accounts/examples/circleci3.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# A more complex example using attribute specifiers directly. Allow OIDC access
# from CircleCI jobs triggered on the main branch of org/repo1 and org/repo2,
# as well as any job using the fake-context context
data "terraform_remote_state" "wip_project" {
backend = "gcs"

config = {
bucket = "my-wip-project"
prefix = "wip-project/prefix"
}
}

locals {
allowed_repos = formatlist("attribute.vcs/github.com/org/%s:/refs/heads/main", ["repo1", "repo2"])
allowed_contexts = formatlist("attribute.context_id/%s",
one(values({ "fake-context" = "6e1515f7-40f0-4063-a74a-d77d22ee9f7e" }
)))
}

module "google_deployment_accounts" {
source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main"
project = "my-project"
environment = "prod"
wip_name = "circleci"
wip_project_number = data.terraform_remote_state.wip_project.number
circleci_attribute_specifiers = setunion(
local.allowed_repos,
local.allowed_contexts,
)
}
17 changes: 17 additions & 0 deletions google_deployment_accounts/examples/gha.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
data "terraform_remote_state" "wip_project" {
backend = "gcs"

config = {
bucket = "my-wip-project"
prefix = "wip-project/prefix"
}
}

module "google_deployment_accounts" {
source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main"
project = "my-project"
environment = "stage"
github_repository = "org/project"
wip_name = "github-actions"
wip_project_number = data.terraform_remote_state.wip_project.number
}
47 changes: 44 additions & 3 deletions google_deployment_accounts/main.tf
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
/**
* # Terraform Module: Service Accounts for deployment from GitHub Actions
* Creates a Cloud IAM service accounts which let GitHub Actions workflows authenticate to GKE.
* # Terraform Module: Service Accounts for deployment from GitHub Actions and CircleCI
* Creates a Cloud IAM service account which lets CI workflows authenticate to GCP.
*/

locals {
gha_count = var.wip_name == "github-actions" ? 1 : 0
circleci_count = var.wip_name == "circleci" ? 1 : 0
}

resource "google_service_account" "account" {
account_id = coalesce(var.account_id, "deploy-${var.environment}")
display_name = "Deployment to the ${var.environment} environment"
display_name = coalesce(var.display_name, "Deployment to the ${var.environment} environment")
project = var.project
}

resource "google_service_account_iam_binding" "github-actions-access" {
count = local.gha_count
service_account_id = google_service_account.account.name
role = "roles/iam.workloadIdentityUser"
members = local.github_deploy_members
}

locals {
circleci = var.wip_name == "circleci"
# explicit attributes replace all other kinds of assertions
circleci_attribute_assertions = local.circleci ? [for attribute_specifier in var.circleci_attribute_specifiers :
"principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/${attribute_specifier}"
] : []
# single repo, all branches
circleci_vcs_origin_assertions = local.circleci && var.github_repository != null && length(var.circleci_branches) == 0 ? ["principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.vcs_origin/github.com/${var.github_repository}",
] : []
# single repo, specific branches
circleci_vcs_assertions = var.wip_name == "circleci" && var.github_repository != null && length(var.circleci_branches) > 0 ? [
for branch in var.circleci_branches :
"principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.vcs/github.com/${var.github_repository}:refs/heads/${branch}"
] : []
# specific CircleCI Context
circleci_context_id_assertions = local.circleci && length(var.circleci_context_ids) > 0 ? [
for context in var.circleci_context_ids :
"principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.context_id/${context}"
] : []
}

resource "google_service_account_iam_binding" "circleci-access" {
count = local.circleci_count
service_account_id = google_service_account.account.name
role = "roles/iam.workloadIdentityUser"
# test value generated via GUI, assertions should look something like:
# "principalSet://iam.googleapis.com/projects/12141114016/locations/global/workloadIdentityPools/circleci-2/attribute.aud/c3874144-7d38-44e8-8b38-f6b8778a4eb0"
members = length(local.circleci_attribute_assertions) > 0 ? local.circleci_attribute_assertions : setunion(
local.circleci_attribute_assertions,
local.circleci_vcs_origin_assertions,
local.circleci_vcs_assertions,
local.circleci_context_id_assertions,
)
}
65 changes: 58 additions & 7 deletions google_deployment_accounts/variables.tf
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
variable "account_id" {
type = string
description = "Name of the service account. Defaults to deploy-<env>"
description = "Name of the service account. Defaults to deploy-ENV."
default = null
}

variable "display_name" {
type = string
description = "Display name for the service account. Defaults to \"Deployment to the ENV environment\"."
default = null
}

variable "environment" {
description = "Environment e.g., stage."
description = "Environment e.g., stage. Not used for OIDC configuration in CircleCI."
type = string
}

Expand All @@ -20,30 +26,75 @@ variable "gha_environments" {
default = []
}

# For CircleCI, the default options are to deploy from certain repositories
# (any branch) or allow deploys via a CircleCI Context. You can also limit
# CircleCI to deploy from specific branches. For more complex use
# cases (such as CI access to a service account across multiple repositories)
# you can specify those attribute specifiers explicitly instead of the
# convenience variables.
variable "circleci_branches" {
description = "(CircleCI only) Branches to allow deployments from. If unspecified, allow deployment from all branches."
type = set(string)
default = []
}

variable "circleci_context_ids" {
description = "(CircleCI only) Contexts to allow deployments from. Not recommended when using merge queues since CircleCI Contexts are only accessible to members of your organization."
type = set(string)
default = []
}

variable "circleci_attribute_specifiers" {
description = "(CircleCI only) Set of attribute specifiers to allow deploys from, in the form ATTR/ATTR_VALUE. If specified, this overrides the github_repository variable and any other CircleCI-specific variables."
type = set(string)
default = []
validation {
condition = alltrue(
[for attribute_specifier in var.circleci_attribute_specifiers :
contains(
[
"subject",
"attribute.aud",
"attribute.vcs",
"attribute.project",
"attribute.vcs_origin",
"attribute.vcs_ref",
"attribute.context_id"
], split("/", attribute_specifier)[0])
]
)
error_message = "Attribute specifiers must contain a valid attribute prefix."
}
}

variable "project" {
type = string
default = null
}

variable "wip_project_number" {
type = number
description = "The project number of the project the workload identity provider lives in"
description = "The project number of the project the workload identity provider lives in."
}

variable "wip_name" {
type = string
description = "The name of the workload identity provider"
description = "The name of the workload identity provider. This value implicitly controls whether to provision access to github-actions or circleci."
default = "github-actions"
validation {
condition = contains(["github-actions", "circleci"], var.wip_name)
error_message = "The value of wip_name must be either github-actions or circleci."
}
}

variable "github_repository" {
type = string
description = "The Github repository running the deployment workflows in the format org/repository"
description = "The Github repository running the deployment workflows in the format org/repository. Optional for CircleCI or when github_repositories is specified."
default = null
}

variable "github_repositories" {
type = list(string)
description = "The Github repositories running the deployment workflows in the format org/repository, will be used if github_repository is not defined"
description = "The Github repositories running the deployment workflows in the format org/repository, will be used if github_repository is not defined."
default = []
}
}

0 comments on commit 30b7e2d

Please sign in to comment.