terraform/jitgroups-appengine/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 = "AppEngine location, see https://cloud.google.com/about/locations#region"
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"
}
#------------------------------------------------------------------------------
# Provider.
#------------------------------------------------------------------------------
terraform {
provider_meta "google" {
module_name = "cloud-solutions/jitgroups-appengine-deploy-v2.0"
}
}
#------------------------------------------------------------------------------
# Local variables.
#------------------------------------------------------------------------------
locals {
sources = "${path.module}/../../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
}
#------------------------------------------------------------------------------
# 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
}
}
#
# Create an OAuth client ID for IAP.
#
resource "google_iap_client" "iap_client" {
display_name = "JIT Groups"
brand = google_iap_brand.iap_brand.name
}
#
# 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)
}
#------------------------------------------------------------------------------
# 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}"
}
#------------------------------------------------------------------------------
# AppEngine.
#------------------------------------------------------------------------------
#
# Initialize AppEngine.
#
resource "google_app_engine_application" "appengine_app" {
project = var.project_id
location_id = var.location
iap {
enabled = true
oauth2_client_id = google_iap_client.iap_client.client_id
oauth2_client_secret = google_iap_client.iap_client.secret
}
}
#------------------------------------------------------------------------------
# AppEngine default service account.
#------------------------------------------------------------------------------
#
# Grant the GAE default service account access to Cloud Storage and Artifact Registry,
# required for deploying and building new versions.
#
# Force-remove Editor role bindings as it's unnecessarily broad.
#
resource "google_project_iam_member" "project_binding_appengine_createonpushwriter" {
depends_on = [ google_app_engine_application.appengine_app ]
project = var.project_id
role = "roles/artifactregistry.createOnPushWriter"
member = "serviceAccount:${var.project_id}@appspot.gserviceaccount.com"
}
resource "google_project_iam_member" "project_binding_appengine_storageadmin" {
depends_on = [ google_app_engine_application.appengine_app ]
project = var.project_id
role = "roles/storage.admin"
member = "serviceAccount:${var.project_id}@appspot.gserviceaccount.com"
}
resource "google_project_iam_member_remove" "project_binding_appengine_editor" {
depends_on = [ google_app_engine_application.appengine_app ]
project = var.project_id
role = "roles/editor"
member = "serviceAccount:${var.project_id}@appspot.gserviceaccount.com"
}
resource "time_sleep" "project_binding_appengine" {
depends_on = [
google_project_iam_member.project_binding_appengine_createonpushwriter,
google_project_iam_member.project_binding_appengine_storageadmin,
google_project_iam_member_remove.project_binding_appengine_editor
]
# Give IAM some time to process the IAM policy update before we use it.
create_duration = "10s"
}
#------------------------------------------------------------------------------
# Deploy GAE application.
#------------------------------------------------------------------------------
#
# Create ZIP with Java source code.
#
data "archive_file" "sources_zip" {
type = "zip"
source_dir = "${local.sources}"
output_path = "${path.module}/target/jitgroups-sources.zip"
}
#
# Upload ZIP file to the AppEngine storage bucket.
#
resource "google_storage_bucket_object" "appengine_sources_object" {
name = "jitgroups.${data.archive_file.sources_zip.output_sha256}.zip"
bucket = google_app_engine_application.appengine_app.default_bucket
source = data.archive_file.sources_zip.output_path
}
#
# Crate an AppEngine version from the uploaded source code.
#
# Keep existing versions to allow rollback/traffic migration.
#
resource "google_app_engine_standard_app_version" "appengine_app_version" {
depends_on = [ time_sleep.project_binding_appengine ]
version_id = "rev-${substr(data.archive_file.sources_zip.output_sha256, 0, 16)}"
service = "default"
project = var.project_id
runtime = "java17"
instance_class = "F2"
service_account = google_service_account.jitgroups.email
env_variables = merge({
"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)
threadsafe = true
noop_on_destroy = true
deployment {
zip {
source_url = "https://storage.googleapis.com/${google_app_engine_application.appengine_app.default_bucket}/${google_storage_bucket_object.appengine_sources_object.name}"
}
}
entrypoint {
shell = ""
}
}
#
# Force traffic to new version
#
resource "google_app_engine_service_split_traffic" "appengine_app_version" {
service = google_app_engine_standard_app_version.appengine_app_version.service
migrate_traffic = false
split {
shard_by = "IP"
allocations = {
(google_app_engine_standard_app_version.appengine_app_version.version_id) = 1.0
}
}
}
#------------------------------------------------------------------------------
# Outputs.
#------------------------------------------------------------------------------
output "url" {
description = "URL to application"
value = "https://${google_app_engine_application.appengine_app.default_hostname}/"
}
output "service_account" {
description = "Service account used by the application"
value = google_service_account.jitgroups.email
}