diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..1783ed7 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# Dockerfile +FROM docker.io/golang:latest + +RUN apt-get update &&\ + apt-get upgrade -y &&\ + apt-get install -y apt-transport-https ca-certificates curl wget gnupg &&\ + apt-get clean + +RUN install -m 0755 -d /etc/apt/keyrings &&\ + curl -fsSL https://get.opentofu.org/opentofu.gpg | tee /etc/apt/keyrings/opentofu.gpg >/dev/null &&\ + curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | gpg --no-tty --batch --dearmor -o /etc/apt/keyrings/opentofu-repo.gpg >/dev/null &&\ + chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg &&\ + echo "deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main" | tee /etc/apt/sources.list.d/opentofu.list > /dev/null &&\ + chmod a+r /etc/apt/sources.list.d/opentofu.list &&\ + apt-get update &&\ + apt-get install -y tofu &&\ + apt-get clean + +RUN mkdir -p /usr/local/bin && apt-get update && apt-get install -y unzip && curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash + +RUN wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | tee /usr/share/keyrings/trivy.gpg > /dev/null &&\ + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | tee -a /etc/apt/sources.list.d/trivy.list &&\ + apt-get update &&\ + apt-get install -y trivy &&\ + apt-get clean diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3ac43d4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +{ + "name": "Juicebox Software Realm", + "dockerFile": "Dockerfile", + "features": { + "ghcr.io/devcontainers-extra/features/pre-commit:2": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, + "ghcr.io/devcontainers/features/common-utils": { + "installZsh": true, + "installOhMyZsh": true, + "installOhMyZshConfig": true, + "configureZshAsDefaultShell": true, + "upgradePackages": true + } + }, + "postCreateCommand": "bash .devcontainer/post-create.sh" +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 0000000..f33825a --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Install pre-commit hooks +pre-commit install diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cc71e5c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # v5.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: 2f8bda194a420ad77a050a9de627d77a74841fdc # v1.99.4 + hooks: + - id: terraform_fmt + - id: terraform_validate + - id: terraform_tflint + - id: terraform_trivy diff --git a/README.md b/README.md index 7042cdc..106eba6 100644 --- a/README.md +++ b/README.md @@ -76,48 +76,26 @@ The realm software will determine which tenant key to validate on a request by a ## GCP -The following instructions will help you quickly deploy a realm to Google's App Engine Flex. - -Before you begin, setup a project for your realm to run in on [console.cloud.google.com](https://console.cloud.google.com) and make note of its ID. - -Next, setup your project environment with terraform as follows: -```sh -cd gcp -terraform init -terraform plan -var='tenant_secrets={"acme":"acme-tenant-key","anotherTenant":"another-tenant-key"}' -terraform apply -var='tenant_secrets={"acme":"acme-tenant-key","anotherTenant":"another-tenant-key"}' -``` - -Note: you should update the tenant secrets `var` to reflect the actual secrets you wish to support. - -After terraform has finished configuring your project environment, you should see an output like follows: -```sh -BIGTABLE_INSTANCE_ID = "jb-sw-realms" -GCP_PROJECT_ID = "your-project-id" -REALM_ID = "99b2da84-b707-6203-dc35-804bbbcb8cba" -SERVICE_ACCOUNT = "jb-sw-realms@your-project-id.iam.gserviceaccount.com" -``` - -Open the `cmd/jb-sw-realm/app.yaml` file and configure it with these values where appropriate, for example: -Replace `{{YOUR_BIGTABLE_INSTANCE_ID}}` with `jb-sw-realms`. - -Finally, you can deploy the realm software by running the following command from the `cmd/jb-sw-realm` directory of the repo: -```sh -gcloud app deploy --project {{YOUR_GCP_PROJECT_ID}} -``` - -Note: you will need to have the `gcloud` command line tools installed to execute this command. You can find instructions on installing these [here](https://cloud.google.com/sdk/docs/install). - -This may take a few minutes, but upon success you should be able to access your realm at: -https://{{YOUR_GCP_PROJECT_ID}}.wl.r.appspot.com - -If all was successful, you'll see a page render that looks something like: -```json -{"realmID":"99b2da84-b707-6203-dc35-804bbbcb8cba"} +The `gcp/` directory is now a Terraform module which can be leveraged in your own Terraform codebase. For example: + +```terraform +module "juicebox-software-realm" { + source = "github.com/rpcpool/juicebox-software-realm.git//gcp" + + project_id = "your-project-id" + realm_id = "99b2da84b7076203dc35804bbbcb8cba" + region = "europe-west3" + zone = "c" + tenant_secrets = {"acme":"acme-tenant-key","anotherTenant":"another-tenant-key"} + juicebox_image_url = "path/to/juicebox/container/image" + juicebox_image_version = "latest" + otelcol_image_url = "path/to/otel/collector/container/image" + otelcol_image_version = "latest" + otelcol_config_b64 = filebase64("otel-collector-config.yaml") +} ``` -If you wish to configure a custom domain for your new realm, visit: -https://console.cloud.google.com/appengine/settings/domains +This will deploy an instance of Juicebox Software Realm along with the necessary infrastructure that supports it. Please note that you will either need to deploy the Juicebox and OpenTelemetry Collector containers to a repository in GCP, or else use a remote repository which caches them from GitHub. ## AWS diff --git a/cmd/jb-sw-realm/app.yaml b/cmd/jb-sw-realm/app.yaml deleted file mode 100644 index ec37b71..0000000 --- a/cmd/jb-sw-realm/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -runtime: go -env: flex -service_account: {{YOUR_SERVICE_ACCOUNT_EMAIL}} - -runtime_config: - operating_system: ubuntu22 - runtime_version: "1.21" - -liveness_check: - path: "/" - check_interval_sec: 30 - timeout_sec: 4 - failure_threshold: 2 - success_threshold: 2 - -env_variables: - BIGTABLE_INSTANCE_ID: {{YOUR_BIGTABLE_INSTANCE_ID}} - GCP_PROJECT_ID: {{YOUR_GCP_PROJECT_ID}} - REALM_ID: {{YOUR_REALM_ID}} - PROVIDER: gcp diff --git a/gcp/.gitignore b/gcp/.gitignore index 9b8a46e..de9ed71 100644 --- a/gcp/.gitignore +++ b/gcp/.gitignore @@ -10,8 +10,8 @@ crash.log crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject # to change depending on the environment. *.tfvars *.tfvars.json @@ -32,3 +32,6 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc + +# This is a module, so ignore .lock.hcl +.terraform.lock.hcl diff --git a/gcp/.terraform.lock.hcl b/gcp/.terraform.lock.hcl deleted file mode 100644 index 04e4417..0000000 --- a/gcp/.terraform.lock.hcl +++ /dev/null @@ -1,22 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/google" { - version = "4.64.0" - hashes = [ - "h1:e9YVOqH5JQTR0LbT+VkOlJb1pDoZEvzXkqaA0Xsn5Mo=", - "h1:oT2shsj9Mb4dGGwzlbWQPMTGSex6yDtJZcF5xQJ7rdE=", - "zh:097fcb0a45fa41c2476deeb7a9adeadf5142e35e4d1a9eeb7b1720900a06807c", - "zh:177e6e34f10efb5cec16b4106af5aef5240f20c33d91d40f3ea73fdc6ce9a24a", - "zh:3331b0f62f900f8f1447e654a7318f3db03723739ac5dcdc446f1a1b1bf5fd0b", - "zh:39e5a19693f8d598d35968660837d1b55ca82d7c314cd433fd957d1c2a5b6616", - "zh:44d09cb871e7ec242610d84f93367755d0c532f744e5871a032cdba430e39ec7", - "zh:77769c0f8ace0be3f85b702b7d4cc0fd43d89bfbea1493166c4f288338222f0a", - "zh:a83ca3e204a85d1d04ee7a6432fdabc7b7e2ef7f46513b6309d8e30ea9e855a3", - "zh:bbf1e983d24877a690886aacd48085b37c8c61dc65e128707f36b7ae6de11abf", - "zh:c359fcf8694af0ec490a1784575eeb355d6e5a922b225f49d5307a06e9715ad0", - "zh:f0df551e19cf8cc9a021a4148518a610b856a50a55938710837fa55b4fbd252f", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:fb171d37178d46d711f3e09107492343f8356c1237bc6df23114920dc23c4528", - ] -} diff --git a/gcp/bigtable.tf b/gcp/bigtable.tf new file mode 100644 index 0000000..0bd5150 --- /dev/null +++ b/gcp/bigtable.tf @@ -0,0 +1,23 @@ +resource "google_bigtable_instance" "instance" { + name = "jb-sw-realms" + display_name = "Juicebox Software Realms" + + cluster { + cluster_id = "jb-sw-realms-cluster" + zone = "${var.region}-${var.zone}" + autoscaling_config { + min_nodes = 1 + max_nodes = 5 + cpu_target = 80 + } + } +} + +resource "google_bigtable_instance_iam_binding" "access" { + instance = google_bigtable_instance.instance.name + role = "roles/bigtable.admin" + + members = [ + "serviceAccount:${google_service_account.service_account.email}" + ] +} diff --git a/gcp/main.tf b/gcp/main.tf index 32efbf2..86bb7eb 100644 --- a/gcp/main.tf +++ b/gcp/main.tf @@ -1,130 +1,4 @@ -# Configure the Google Cloud provider -provider "google" { - project = var.project_id - region = var.region -} - -# Enable required APIs -resource "google_project_service" "app_engine" { - service = "appengine.googleapis.com" -} - -resource "google_project_service" "secrets_manager" { - project = var.project_id - service = "secretmanager.googleapis.com" -} - -resource "google_project_service" "pub_sub" { - project = var.project_id - service = "pubsub.googleapis.com" -} - -# Create app engine service account resource "google_service_account" "service_account" { account_id = "jb-sw-realms" display_name = "Juicebox Software Realms" } - -# Create each tenant secret -resource "google_secret_manager_secret" "secret" { - for_each = var.tenant_secrets - project = var.project_id - secret_id = "jb-sw-tenant-${each.key}" - replication { - automatic = true - } -} - -# Add the secret data for each tenant secret -resource "google_secret_manager_secret_version" "secret" { - for_each = var.tenant_secrets - secret = google_secret_manager_secret.secret[each.key].id - secret_data = each.value -} - -# Grant access to the app engine for each tenant secret -resource "google_secret_manager_secret_iam_binding" "access" { - for_each = var.tenant_secrets - project = var.project_id - secret_id = google_secret_manager_secret.secret[each.key].id - role = "roles/secretmanager.secretAccessor" - - members = [ - "serviceAccount:${google_service_account.service_account.email}" - ] -} - -# Create Bigtable instance -resource "google_bigtable_instance" "instance" { - project = var.project_id - name = "jb-sw-realms" - display_name = "Juicebox Software Realms" - - cluster { - cluster_id = "jb-sw-realms-cluster" - zone = var.zone - autoscaling_config { - min_nodes = 1 - max_nodes = 5 - cpu_target = 80 - } - } -} - -# Grant access to the app engine for the Bigtable instance -resource "google_bigtable_instance_iam_binding" "access" { - project = var.project_id - instance = google_bigtable_instance.instance.name - role = "roles/bigtable.admin" - - members = [ - "serviceAccount:${google_service_account.service_account.email}" - ] -} - -# Create App Engine application -resource "google_app_engine_application" "app" { - project = var.project_id - location_id = var.region -} - -# Grant log writer permissions to app engine -resource "google_project_iam_binding" "logs_writer_binding" { - project = var.project_id - role = "roles/logging.logWriter" - members = [ - "serviceAccount:${google_service_account.service_account.email}" - ] -} - -# Grant object reader permissions to app engine so it can access Google Container Registry -resource "google_project_iam_binding" "storage_object_viewer_binding" { - project = var.project_id - role = "roles/storage.objectViewer" - members = [ - "serviceAccount:${google_service_account.service_account.email}" - ] -} - -# Define a custom role with the specific pub/sub perms needed. -resource "google_project_iam_custom_role" "pubsub_role" { - project = var.project_id - role_id = "pubsub_role" - title = "Role for managing pub/sub from a software realm" - description = "Role for managing pub/sub from a software realm" - permissions = ["pubsub.subscriptions.create", - "pubsub.topics.attachSubscription", - "pubsub.topics.create", - "pubsub.topics.publish", - "pubsub.subscriptions.consume", - ] -} - -# Grant pub/sub access to the service account -resource "google_project_iam_binding" "pubsub_binding" { - project = var.project_id - role = google_project_iam_custom_role.pubsub_role.name - members = [ - "serviceAccount:${google_service_account.service_account.email}" - ] -} diff --git a/gcp/outputs.tf b/gcp/outputs.tf index f18353c..e69de29 100644 --- a/gcp/outputs.tf +++ b/gcp/outputs.tf @@ -1,15 +0,0 @@ -output "REALM_ID" { - value = var.realm_id -} - -output "BIGTABLE_INSTANCE_ID" { - value = google_bigtable_instance.instance.name -} - -output "GCP_PROJECT_ID" { - value = var.project_id -} - -output "SERVICE_ACCOUNT" { - value = google_service_account.service_account.email -} diff --git a/gcp/pubsub.tf b/gcp/pubsub.tf new file mode 100644 index 0000000..6aca8fc --- /dev/null +++ b/gcp/pubsub.tf @@ -0,0 +1,21 @@ +# Define a custom role with the specific pub/sub perms needed. +resource "google_project_iam_custom_role" "pubsub_role" { + role_id = "pubsub_role" + title = "Role for managing pub/sub from a software realm" + description = "Role for managing pub/sub from a software realm" + permissions = ["pubsub.subscriptions.create", + "pubsub.topics.attachSubscription", + "pubsub.topics.create", + "pubsub.topics.publish", + "pubsub.subscriptions.consume", + ] +} + +# Grant pub/sub access to the service account +resource "google_project_iam_binding" "pubsub_binding" { + project = var.project_id + role = google_project_iam_custom_role.pubsub_role.name + members = [ + "serviceAccount:${google_service_account.service_account.email}" + ] +} diff --git a/gcp/run.tf b/gcp/run.tf new file mode 100644 index 0000000..98a760f --- /dev/null +++ b/gcp/run.tf @@ -0,0 +1,83 @@ +# Create Cloud Run Service +resource "google_cloud_run_v2_service" "juicebox" { + name = "juicebox" + location = var.region + deletion_protection = true + ingress = "INGRESS_TRAFFIC_ALL" + scaling { + min_instance_count = 4 + } + template { + timeout = "300s" + service_account = google_service_account.service_account.email + containers { + name = "juicebox-1" + ports { + name = "http1" + container_port = 8080 + } + resources { + cpu_idle = true + startup_cpu_boost = true + limits = { + cpu = "1000m" + memory = "512Mi" + } + } + startup_probe { + timeout_seconds = 240 + period_seconds = 240 + failure_threshold = 1 + tcp_socket { + port = 8080 + } + } + image = "${var.juicebox_image_url}:${var.juicebox_image_version}" + env { + name = "BIGTABLE_INSTANCE_ID" + value = google_bigtable_instance.instance.name + } + env { + name = "GCP_PROJECT_ID" + value = var.project_id + } + env { + name = "PROVIDER" + value = "gcp" + } + env { + name = "REALM_ID" + value = var.realm_id + } + dynamic "env" { + for_each = var.juicebox_vars + content { + name = env.key + value = env.value + } + } + } + } + lifecycle { + ignore_changes = [ + client + ] + } +} + +resource "google_project_iam_binding" "logs_writer_binding" { + project = var.project_id + role = "roles/logging.logWriter" + members = [ + "serviceAccount:${google_service_account.service_account.email}" + ] +} + +resource "google_cloud_run_v2_service_iam_binding" "allow_unauthenticated_users" { + project = var.project_id + name = google_cloud_run_v2_service.juicebox.name + role = "roles/run.invoker" + members = [ + "allUsers", + ] +} diff --git a/gcp/secret-manager.tf b/gcp/secret-manager.tf new file mode 100644 index 0000000..f3c8753 --- /dev/null +++ b/gcp/secret-manager.tf @@ -0,0 +1,23 @@ +resource "google_secret_manager_secret" "secret" { + for_each = var.tenant_secrets + secret_id = "jb-sw-tenant-${each.key}" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "secret" { + for_each = var.tenant_secrets + secret = google_secret_manager_secret.secret[each.key].id + secret_data = each.value +} + +resource "google_secret_manager_secret_iam_binding" "access" { + for_each = var.tenant_secrets + secret_id = google_secret_manager_secret.secret[each.key].id + role = "roles/secretmanager.secretAccessor" + + members = [ + "serviceAccount:${google_service_account.service_account.email}" + ] +} diff --git a/gcp/variables.tf b/gcp/variables.tf index c19281a..0d0b47f 100644 --- a/gcp/variables.tf +++ b/gcp/variables.tf @@ -11,16 +11,30 @@ variable "realm_id" { variable "region" { description = "Google Cloud Region" type = string - default = "us-west2" } variable "zone" { description = "Google Cloud Zone" type = string - default = "us-west2-a" } variable "tenant_secrets" { description = "The names of any tenants you will be allowing access to (alphanumeric) mapped to their auth signing key. Read the 'Tenant Auth Secrets' section of the README for more details." type = map(string) } + +variable "juicebox_image_url" { + description = "The url of the juicebox docker image" + type = string +} + +variable "juicebox_image_version" { + description = "The version of the juicebox docker image" + type = string +} + +variable "juicebox_vars" { + description = "Environment variables for the juicebox container" + type = map(string) + default = {} +} diff --git a/gcp/versions.tf b/gcp/versions.tf new file mode 100644 index 0000000..0ec01a9 --- /dev/null +++ b/gcp/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = "~> 1.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 6.40" + } + } +}