terraform/jitgroups-cloudrun/main.tf (276 lines of code) (raw):

# # Copyright 2024 Google LLC # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # #------------------------------------------------------------------------------ # Input variables. #------------------------------------------------------------------------------ variable "project_id" { description = "Project to deploy to" type = string } variable "location" { description = "Cloud Run location, see https://cloud.google.com/run/docs/locations" type = string } variable "customer_id" { description = "Cloud Identity/Workspace customer ID" type = string validation { condition = startswith(var.customer_id, "C") error_message = "customer_id must be a valid customer ID, starting with C" } } variable "primary_domain" { description = "Primary domain of the Cloud Identity/Workspace account" type = string } variable "organization_id" { description = "Organization ID of the Google Cloud organization" type = string } variable "groups_domain" { description = "Domain to use for JIT groups, this can be the primary or a secondary domain" type = string default = null } variable "admin_email" { description = "Contact email address, must be a Cloud Identity/Workspace user" type = string } variable "resource_scope" { description = "JIT Access 1.x compatibility: Project, folder, or organization that JIT Access can manage access for" type = string default = "" # Disabled validation { condition = var.resource_scope == "" || ( startswith(var.resource_scope, "organizations/") || startswith(var.resource_scope, "folders/") || startswith(var.resource_scope, "projects/")) error_message = "resource_scope must be in the format organizations/ID, folders/ID, or projects/ID" } } variable "environments" { description = "Environment service accounts, prefixed with 'serviceAccount:" type = list(string) default = [] validation { condition = alltrue([for e in var.environments : startswith(lower(e), "serviceaccount:")]) error_message = "environments must use the format 'serviceAccount:jit-NAME@PROJECT.iam.gserviceaccount.com'" } } variable "iap_users" { description = "Users and groups to allow IAP-access to the application, prefixed with 'user:', 'group:', or domain:" type = list(string) default = [] } variable "options" { description = "Configuration options" type = map(string) default = {} } variable "smtp_user" { description = "SMTP host" type = string default = null } variable "smtp_password" { description = "SMTP password" type = string default = null } variable "smtp_host" { description = "SMTP host" type = string default = "smtp.gmail.com" } variable "image_tag" { description = "Docker image tag to deploy. If not specified, the image is built from source." type = string default = null } #------------------------------------------------------------------------------ # Provider. #------------------------------------------------------------------------------ terraform { provider_meta "google" { module_name = "cloud-solutions/jitgroups-cloudrun-deploy-v2.0" } } provider "google-beta" { project = var.project_id } #------------------------------------------------------------------------------ # Local variables. #------------------------------------------------------------------------------ locals { sources = "${path.module}/../../sources" image_name = "${var.location}-docker.pkg.dev/${var.project_id}/jitgroups/jitgroups" # Effective image tag to use. image_tag = var.image_tag != null ? var.image_tag : data.external.git.result.sha } # # Get current commit SHA. # data "external" "git" { program = [ "sh", "-c", var.image_tag != null ? "echo {\\\"sha\\\": \\\"${var.image_tag}\\\"}" : "echo {\\\"sha\\\": \\\"$(git rev-parse HEAD)\\\"}" ] working_dir = local.sources } #------------------------------------------------------------------------------ # Required APIs #------------------------------------------------------------------------------ resource "google_project_service" "cloudasset" { project = var.project_id service = "cloudasset.googleapis.com" disable_on_destroy = false } resource "google_project_service" "iam" { project = var.project_id service = "iam.googleapis.com" disable_on_destroy = false } resource "google_project_service" "cloudresourcemanager" { project = var.project_id service = "cloudresourcemanager.googleapis.com" disable_on_destroy = false } resource "google_project_service" "iap" { project = var.project_id service = "iap.googleapis.com" disable_on_destroy = false } resource "google_project_service" "containerregistry" { project = var.project_id service = "containerregistry.googleapis.com" disable_on_destroy = false } resource "google_project_service" "iamcredentials" { project = var.project_id service = "iamcredentials.googleapis.com" disable_on_destroy = false } resource "google_project_service" "cloudidentity" { project = var.project_id service = "cloudidentity.googleapis.com" disable_on_destroy = false } resource "google_project_service" "groupssettings" { project = var.project_id service = "groupssettings.googleapis.com" disable_on_destroy = false } resource "google_project_service" "secretmanager" { project = var.project_id service = "secretmanager.googleapis.com" disable_on_destroy = false } resource "google_project_service" "artifactregistry" { project = var.project_id service = "artifactregistry.googleapis.com" disable_on_destroy = false } resource "google_project_service" "run" { project = var.project_id service = "run.googleapis.com" disable_on_destroy = false } #------------------------------------------------------------------------------ # Project. #------------------------------------------------------------------------------ data "google_project" "project" { project_id = var.project_id } # # Force-remove Editor role from Compute Engine service account, if present. # resource "google_project_iam_member_remove" "project_binding_gce_default" { project = var.project_id role = "roles/editor" member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" } #------------------------------------------------------------------------------ # App service account. #------------------------------------------------------------------------------ # # Service account used by application. # resource "google_service_account" "jitgroups" { depends_on = [ google_project_service.iam ] project = var.project_id account_id = "jitgroups" display_name = "JIT Groups Application" } # # Grant the service account the Token Creator role so that it can sign JWTs. # resource "google_service_account_iam_member" "service_account_member" { service_account_id = google_service_account.jitgroups.name role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:${google_service_account.jitgroups.email}" } #------------------------------------------------------------------------------ # IAP. #------------------------------------------------------------------------------ # # Create an OAuth consent screen for IAP. # resource "google_iap_brand" "iap_brand" { depends_on = [ google_project_service.iap ] project = var.project_id support_email = var.admin_email application_title = "JIT Groups" lifecycle { # This resource can't be deleted. prevent_destroy = true } } # # Allow users to access IAP. # resource "google_project_iam_binding" "iap_binding_users" { project = var.project_id role = "roles/iap.httpsResourceAccessor" members = concat([ "user:${var.admin_email}" ], var.iap_users) } # # Force-create service identity. Enabling the IAP API should do that automatically, # but it doesn't. # resource "google_project_service_identity" "iap" { provider = google-beta project = var.project_id service = "iap.googleapis.com" } #------------------------------------------------------------------------------ # Secret containing SMTP password. #------------------------------------------------------------------------------ # # Create secret to store SMTP password. # resource "google_secret_manager_secret" "smtp" { depends_on = [ google_project_service.secretmanager ] secret_id = "smtp" replication { auto {} } } # # Allow the service account to access the secret. # resource "google_secret_manager_secret_iam_member" "secret_binding" { project = google_secret_manager_secret.smtp.project secret_id = google_secret_manager_secret.smtp.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.jitgroups.email}" } #------------------------------------------------------------------------------ # Docker image. #------------------------------------------------------------------------------ # # Create a Docker repository. # resource "google_artifact_registry_repository" "registry" { depends_on = [ google_project_service.artifactregistry ] format = "DOCKER" repository_id = "jitgroups" project = var.project_id location = var.location } # # Build a Docker image if no tag was provided. # resource "null_resource" "docker_image" { depends_on = [google_artifact_registry_repository.registry] count = var.image_tag != null ? 0 : 1 provisioner "local-exec" { command = join("&&", [ "docker build -t ${local.image_name}:${local.image_tag} ${local.sources}", "docker push ${local.image_name}:${local.image_tag}" ]) interpreter = ["bash", "-c"] } } #------------------------------------------------------------------------------ # Cloud Run. #------------------------------------------------------------------------------ # # Create a new revision of a Cloud Run service. # resource "google_cloud_run_v2_service" "service" { depends_on = [null_resource.docker_image, google_project_service.run] provider = google-beta launch_stage = "BETA" iap_enabled = true location = var.location name = "default" project = var.project_id ingress = "INGRESS_TRAFFIC_ALL" template { service_account = google_service_account.jitgroups.email execution_environment = "EXECUTION_ENVIRONMENT_GEN2" scaling { max_instance_count = 2 } containers { image = "${local.image_name}:${local.image_tag}" dynamic "env" { for_each = merge({ "IAP_VERIFY_AUDIENCE" = "false" "RESOURCE_SCOPE" = var.resource_scope "CUSTOMER_ID" = var.customer_id "PRIMARY_DOMAIN" = var.primary_domain "ORGANIZATION_ID" = var.organization_id "GROUPS_DOMAIN" = var.groups_domain "SMTP_HOST" = var.smtp_host "SMTP_SENDER_ADDRESS" = var.smtp_user "SMTP_USERNAME" = var.smtp_user "SMTP_SECRET" = "${google_secret_manager_secret.smtp.name}/versions/latest" "ENVIRONMENTS" = join(",", var.environments) }, var.options) content { name = env.key value = env.value } } } } } #------------------------------------------------------------------------------ # Outputs. #------------------------------------------------------------------------------ output "url" { description = "URL to application" value = google_cloud_run_v2_service.service.uri } output "service_account" { description = "Service account used by the application" value = google_service_account.jitgroups.email }