diff --git a/examples/braintrust-data-plane/main.tf b/examples/braintrust-data-plane/main.tf index 1073e2b..ef00386 100644 --- a/examples/braintrust-data-plane/main.tf +++ b/examples/braintrust-data-plane/main.tf @@ -67,7 +67,11 @@ module "braintrust-data-plane" { ### Advanced configuration # gcs_additional_allowed_origins = [] - # Optional GCS access logging for the managed buckets. Point these at an existing log bucket. + # Create a dedicated GCS access log bucket and send both bucket logs to it. + # gcs_brainstore_logging_config = {} + # gcs_api_logging_config = {} + # + # Optional GCS access logging with existing log buckets. # gcs_brainstore_logging_config = { # log_bucket = "my-access-logs-bucket" # log_object_prefix = "brainstore/" diff --git a/examples/braintrust-data-plane/outputs.tf b/examples/braintrust-data-plane/outputs.tf index 547698f..c8c7154 100644 --- a/examples/braintrust-data-plane/outputs.tf +++ b/examples/braintrust-data-plane/outputs.tf @@ -6,6 +6,10 @@ output "brainstore_bucket_name" { value = module.braintrust-data-plane.brainstore_bucket_name } +output "access_log_bucket_name" { + value = module.braintrust-data-plane.access_log_bucket_name +} + output "braintrust_service_account" { value = module.braintrust-data-plane.braintrust_service_account } diff --git a/modules/storage/main.tf b/modules/storage/main.tf index 8bdb2bc..a96e932 100644 --- a/modules/storage/main.tf +++ b/modules/storage/main.tf @@ -14,6 +14,12 @@ locals { common_labels = merge(var.custom_labels, { braintrustdeploymentname = var.deployment_name }) + + create_access_log_bucket = ( + var.gcs_brainstore_logging_config == null ? false : var.gcs_brainstore_logging_config.log_bucket == null + ) || ( + var.gcs_api_logging_config == null ? false : var.gcs_api_logging_config.log_bucket == null + ) } data "google_project" "current" {} @@ -28,6 +34,56 @@ resource "random_id" "gcs_suffix" { byte_length = 4 } +#---------------------------------------------------------------------------------------------- +# Google cloud storage (GCS) bucket - access logs +#---------------------------------------------------------------------------------------------- +resource "google_storage_bucket" "access_logs" { + count = local.create_access_log_bucket ? 1 : 0 + + name = "${var.deployment_name}-access-logs-${random_id.gcs_suffix.hex}" + location = data.google_client_config.current.region + storage_class = var.gcs_storage_class + uniform_bucket_level_access = true + force_destroy = var.gcs_force_destroy + public_access_prevention = "enforced" + + versioning { + enabled = var.gcs_versioning_enabled + } + + soft_delete_policy { + retention_duration_seconds = var.gcs_soft_delete_retention_days * 86400 + } + + dynamic "encryption" { + for_each = var.gcs_kms_cmek_id != null ? ["encryption"] : [] + + content { + default_kms_key_name = var.gcs_kms_cmek_id + } + } + + labels = local.common_labels + + lifecycle { + ignore_changes = [ + name, + ] + } + + depends_on = [ + google_kms_crypto_key_iam_member.gcp_project_gcs_cmek + ] +} + +resource "google_storage_bucket_iam_member" "access_log_writer" { + count = local.create_access_log_bucket ? 1 : 0 + + bucket = google_storage_bucket.access_logs[0].name + role = "roles/storage.objectCreator" + member = "group:cloud-storage-analytics@google.com" +} + #---------------------------------------------------------------------------------------------- # Google cloud storage (GCS) bucket - Brainstore #---------------------------------------------------------------------------------------------- @@ -103,7 +159,14 @@ resource "google_storage_bucket" "brainstore" { } dynamic "logging" { - for_each = var.gcs_brainstore_logging_config == null ? [] : [var.gcs_brainstore_logging_config] + for_each = var.gcs_brainstore_logging_config == null ? [] : [{ + log_bucket = var.gcs_brainstore_logging_config.log_bucket == null ? ( + google_storage_bucket.access_logs[0].name + ) : var.gcs_brainstore_logging_config.log_bucket + log_object_prefix = var.gcs_brainstore_logging_config.log_bucket == null ? ( + "brainstore" + ) : var.gcs_brainstore_logging_config.log_object_prefix + }] content { log_bucket = logging.value.log_bucket @@ -120,7 +183,8 @@ resource "google_storage_bucket" "brainstore" { } depends_on = [ - google_kms_crypto_key_iam_member.gcp_project_gcs_cmek + google_kms_crypto_key_iam_member.gcp_project_gcs_cmek, + google_storage_bucket_iam_member.access_log_writer, ] } @@ -220,7 +284,14 @@ resource "google_storage_bucket" "api" { } dynamic "logging" { - for_each = var.gcs_api_logging_config == null ? [] : [var.gcs_api_logging_config] + for_each = var.gcs_api_logging_config == null ? [] : [{ + log_bucket = var.gcs_api_logging_config.log_bucket == null ? ( + google_storage_bucket.access_logs[0].name + ) : var.gcs_api_logging_config.log_bucket + log_object_prefix = var.gcs_api_logging_config.log_bucket == null ? ( + "api" + ) : var.gcs_api_logging_config.log_object_prefix + }] content { log_bucket = logging.value.log_bucket @@ -237,7 +308,8 @@ resource "google_storage_bucket" "api" { } depends_on = [ - google_kms_crypto_key_iam_member.gcp_project_gcs_cmek + google_kms_crypto_key_iam_member.gcp_project_gcs_cmek, + google_storage_bucket_iam_member.access_log_writer, ] } diff --git a/modules/storage/outputs.tf b/modules/storage/outputs.tf index c6b8d62..c6b8554 100644 --- a/modules/storage/outputs.tf +++ b/modules/storage/outputs.tf @@ -6,6 +6,9 @@ output "brainstore_bucket_self_link" { value = google_storage_bucket.brainstore.self_link } +output "access_log_bucket_name" { + value = try(google_storage_bucket.access_logs[0].name, null) +} output "api_bucket_name" { value = google_storage_bucket.api.name diff --git a/modules/storage/variables.tf b/modules/storage/variables.tf index b2047bb..3c3bbca 100644 --- a/modules/storage/variables.tf +++ b/modules/storage/variables.tf @@ -52,30 +52,34 @@ variable "gcs_additional_allowed_origins" { } variable "gcs_brainstore_logging_config" { - description = "Optional access logging configuration for the Brainstore GCS bucket." + description = "Optional Brainstore access log configuration. Omit log_bucket to create a dedicated bucket." type = object({ - log_bucket = string + log_bucket = optional(string) log_object_prefix = optional(string) }) default = null validation { - condition = var.gcs_brainstore_logging_config == null ? true : trimspace(var.gcs_brainstore_logging_config.log_bucket) != "" - error_message = "`gcs_brainstore_logging_config.log_bucket` must be a non-empty bucket name." + condition = var.gcs_brainstore_logging_config == null ? true : ( + var.gcs_brainstore_logging_config.log_bucket == null ? true : trimspace(var.gcs_brainstore_logging_config.log_bucket) != "" + ) + error_message = "`gcs_brainstore_logging_config.log_bucket` must be null or a non-empty bucket name." } } variable "gcs_api_logging_config" { - description = "Optional access logging configuration for the API GCS bucket." + description = "Optional API access log configuration. Omit log_bucket to use the dedicated bucket." type = object({ - log_bucket = string + log_bucket = optional(string) log_object_prefix = optional(string) }) default = null validation { - condition = var.gcs_api_logging_config == null ? true : trimspace(var.gcs_api_logging_config.log_bucket) != "" - error_message = "`gcs_api_logging_config.log_bucket` must be a non-empty bucket name." + condition = var.gcs_api_logging_config == null ? true : ( + var.gcs_api_logging_config.log_bucket == null ? true : trimspace(var.gcs_api_logging_config.log_bucket) != "" + ) + error_message = "`gcs_api_logging_config.log_bucket` must be null or a non-empty bucket name." } } diff --git a/outputs.tf b/outputs.tf index 8c1defa..de6b29a 100644 --- a/outputs.tf +++ b/outputs.tf @@ -41,6 +41,11 @@ output "brainstore_bucket_name" { value = module.storage.brainstore_bucket_name } +output "access_log_bucket_name" { + value = module.storage.access_log_bucket_name + description = "Dedicated GCS access log bucket, or null when the module does not manage one." +} + #---------------------------------------------------------------------------------------------- # Service account #---------------------------------------------------------------------------------------------- diff --git a/variables.tf b/variables.tf index b8820d2..f230a7d 100644 --- a/variables.tf +++ b/variables.tf @@ -274,30 +274,34 @@ variable "gcs_additional_allowed_origins" { } variable "gcs_brainstore_logging_config" { - description = "Optional access logging configuration for the Brainstore GCS bucket." + description = "Optional Brainstore access log configuration. Omit log_bucket to create a dedicated bucket." type = object({ - log_bucket = string + log_bucket = optional(string) log_object_prefix = optional(string) }) default = null validation { - condition = var.gcs_brainstore_logging_config == null ? true : trimspace(var.gcs_brainstore_logging_config.log_bucket) != "" - error_message = "`gcs_brainstore_logging_config.log_bucket` must be a non-empty bucket name." + condition = var.gcs_brainstore_logging_config == null ? true : ( + var.gcs_brainstore_logging_config.log_bucket == null ? true : trimspace(var.gcs_brainstore_logging_config.log_bucket) != "" + ) + error_message = "`gcs_brainstore_logging_config.log_bucket` must be null or a non-empty bucket name." } } variable "gcs_api_logging_config" { - description = "Optional access logging configuration for the API GCS bucket." + description = "Optional API access log configuration. Omit log_bucket to use the dedicated bucket." type = object({ - log_bucket = string + log_bucket = optional(string) log_object_prefix = optional(string) }) default = null validation { - condition = var.gcs_api_logging_config == null ? true : trimspace(var.gcs_api_logging_config.log_bucket) != "" - error_message = "`gcs_api_logging_config.log_bucket` must be a non-empty bucket name." + condition = var.gcs_api_logging_config == null ? true : ( + var.gcs_api_logging_config.log_bucket == null ? true : trimspace(var.gcs_api_logging_config.log_bucket) != "" + ) + error_message = "`gcs_api_logging_config.log_bucket` must be null or a non-empty bucket name." } }