diff --git a/aws/keycloak-flatcar-spike/.gitignore b/aws/keycloak-flatcar-spike/.gitignore new file mode 100644 index 0000000..eaecc08 --- /dev/null +++ b/aws/keycloak-flatcar-spike/.gitignore @@ -0,0 +1,10 @@ +.terraform/ +*.tfstate +*.tfstate.* +tfplan +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json diff --git a/aws/keycloak-flatcar-spike/.terraform.lock.hcl b/aws/keycloak-flatcar-spike/.terraform.lock.hcl new file mode 100644 index 0000000..b80adaa --- /dev/null +++ b/aws/keycloak-flatcar-spike/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.90" + hashes = [ + "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", + "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", + "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", + "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", + "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", + "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", + "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", + "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", + "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", + "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", + "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", + ] +} diff --git a/aws/keycloak-flatcar-spike/Justfile b/aws/keycloak-flatcar-spike/Justfile new file mode 100644 index 0000000..4894a50 --- /dev/null +++ b/aws/keycloak-flatcar-spike/Justfile @@ -0,0 +1,29 @@ +set shell := ["bash", "-euo", "pipefail", "-c"] + +default: + @just --list + +# Offline validation: no backend or AWS credentials required. +check: + ./scripts/check.sh + +# Format Tofu files in place. +fmt: + tofu fmt -recursive + +# Initialize the working directory against the S3 backend. +init: + test -n "${GLAB_AWS_STATE_BUCKET:-}" || { echo "Set GLAB_AWS_STATE_BUCKET to the pre-created S3 backend bucket." >&2; exit 1; } + tofu init -reconfigure -backend-config="bucket=${GLAB_AWS_STATE_BUCKET}" + +# Render and save a plan to `tfplan`. +plan: + tofu plan -out=tfplan + +# Apply the saved plan produced by `just plan`. +apply: + tofu apply tfplan + +# Show outputs from the last applied state. +output: + tofu output diff --git a/aws/keycloak-flatcar-spike/README.md b/aws/keycloak-flatcar-spike/README.md new file mode 100644 index 0000000..889b611 --- /dev/null +++ b/aws/keycloak-flatcar-spike/README.md @@ -0,0 +1,32 @@ +# AWS Keycloak Flatcar Spike + +Temporary OpenTofu root for proving the Flatcar bootstrap path before moving the live Keycloak host off AL2023. + +This stack intentionally creates only a sidecar EC2 instance: + +- Flatcar stable arm64 in the existing lab public subnet +- outbound-only security group +- temporary IAM role and instance profile +- encrypted 16 GiB gp3 root volume +- no DNS records +- no persistent application data volume + +On boot, Ignition enables `glab-keycloak-bootstrap.service`. The unit runs the pinned `labctl` container with host networking and writes the decrypted `stack_env` field from `services/keycloak/bootstrap.sops.yaml` to `/run/glab/keycloak/stack.env`. + +## Validation + +```sh +just check +AWS_PROFILE=lab-admin AWS_REGION=us-west-2 just init +AWS_PROFILE=lab-admin AWS_REGION=us-west-2 just plan +``` + +After apply, inspect only non-secret metadata through SSM: + +```sh +systemctl is-active glab-keycloak-bootstrap.service +stat -c '%a %s %n' /run/glab/keycloak/stack.env +cut -d= -f1 /run/glab/keycloak/stack.env | sort +``` + +Do not print the full `/run/glab/keycloak/stack.env` contents. diff --git a/aws/keycloak-flatcar-spike/backend.tf b/aws/keycloak-flatcar-spike/backend.tf new file mode 100644 index 0000000..533b40b --- /dev/null +++ b/aws/keycloak-flatcar-spike/backend.tf @@ -0,0 +1,8 @@ +terraform { + backend "s3" { + key = "aws/keycloak-flatcar-spike.tfstate" + region = "us-west-2" + encrypt = true + use_lockfile = true + } +} diff --git a/aws/keycloak-flatcar-spike/compute.tf b/aws/keycloak-flatcar-spike/compute.tf new file mode 100644 index 0000000..7faf23f --- /dev/null +++ b/aws/keycloak-flatcar-spike/compute.tf @@ -0,0 +1,27 @@ +resource "aws_instance" "flatcar" { + ami = var.flatcar_ami_id + associate_public_ip_address = true + iam_instance_profile = aws_iam_instance_profile.flatcar.name + instance_type = var.instance_type + subnet_id = data.aws_subnet.public.id + user_data = local.ignition_config + user_data_replace_on_change = true + vpc_security_group_ids = [aws_security_group.flatcar.id] + + metadata_options { + http_endpoint = "enabled" + http_put_response_hop_limit = 2 + http_tokens = "required" + } + + root_block_device { + delete_on_termination = true + encrypted = true + volume_size = var.root_volume_size + volume_type = "gp3" + } + + tags = merge(local.common_tags, { + Name = var.instance_name + }) +} diff --git a/aws/keycloak-flatcar-spike/data.tf b/aws/keycloak-flatcar-spike/data.tf new file mode 100644 index 0000000..0675d85 --- /dev/null +++ b/aws/keycloak-flatcar-spike/data.tf @@ -0,0 +1,22 @@ +data "aws_lambda_function" "github_token_broker" { + function_name = var.github_token_broker_function_name +} + +data "aws_subnet" "public" { + filter { + name = "tag:Name" + values = [var.public_subnet_name] + } + + filter { + name = "vpc-id" + values = [data.aws_vpc.lab.id] + } +} + +data "aws_vpc" "lab" { + filter { + name = "tag:Name" + values = [var.vpc_name] + } +} diff --git a/aws/keycloak-flatcar-spike/iam.tf b/aws/keycloak-flatcar-spike/iam.tf new file mode 100644 index 0000000..6119b13 --- /dev/null +++ b/aws/keycloak-flatcar-spike/iam.tf @@ -0,0 +1,30 @@ +resource "aws_iam_role" "flatcar" { + assume_role_policy = local.flatcar_assume_role_policy + name = var.iam_role_name + + tags = merge(local.common_tags, { + Name = var.iam_role_name + }) +} + +resource "aws_iam_role_policy" "github_token_broker_invoke" { + name = "${var.iam_role_name}-github-token-broker-invoke" + policy = local.github_token_broker_invoke_policy + role = aws_iam_role.flatcar.id +} + +resource "aws_iam_role_policy" "sops_keycloak_decrypt" { + name = "${var.iam_role_name}-sops-keycloak-decrypt" + policy = local.sops_keycloak_decrypt_policy + role = aws_iam_role.flatcar.id +} + +resource "aws_iam_role_policy_attachment" "ssm_managed_instance_core" { + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" + role = aws_iam_role.flatcar.name +} + +resource "aws_iam_instance_profile" "flatcar" { + name = var.iam_role_name + role = aws_iam_role.flatcar.name +} diff --git a/aws/keycloak-flatcar-spike/locals.tf b/aws/keycloak-flatcar-spike/locals.tf new file mode 100644 index 0000000..3f5a8d0 --- /dev/null +++ b/aws/keycloak-flatcar-spike/locals.tf @@ -0,0 +1,98 @@ +locals { + common_tags = merge(var.tags, { + "glab:project" = "glab" + "glab:domain" = "aws" + "glab:purpose" = "keycloak-flatcar-spike" + }) + + flatcar_assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + }, + ] + }) + + github_token_broker_invoke_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "lambda:InvokeFunction", + ] + Effect = "Allow" + Resource = [ + data.aws_lambda_function.github_token_broker.arn, + ] + Sid = "AllowInvokeGitHubTokenBroker" + }, + ] + }) + + sops_keycloak_decrypt_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "kms:Decrypt", + ] + Condition = { + StringEquals = { + "kms:EncryptionContext:Repo" = var.sops_kms_context_repo + "kms:EncryptionContext:Scope" = var.sops_kms_context_scope + } + } + Effect = "Allow" + Resource = [ + var.sops_kms_key_arn, + ] + Sid = "AllowDecryptKeycloakSopsSecrets" + }, + ] + }) + + bootstrap_unit_name = "glab-keycloak-bootstrap.service" + + bootstrap_unit = <<-UNIT + [Unit] + Description=Fetch Keycloak bootstrap secrets with labctl + Wants=network-online.target + After=network-online.target docker.service + Requires=docker.service + + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStartPre=/usr/bin/mkdir -p ${var.bootstrap_runtime_dir} + ExecStartPre=/usr/bin/chmod 0700 ${var.bootstrap_runtime_dir} + ExecStartPre=/usr/bin/rm -f ${var.bootstrap_output_path} + ExecStart=/usr/bin/docker run --rm --network host --user 0:0 -v ${var.bootstrap_runtime_dir}:${var.bootstrap_runtime_dir} ${var.labctl_image} secrets get ${var.bootstrap_secret_path} --source github --field ${var.bootstrap_field} --output ${var.bootstrap_output_path} --aws-region ${var.aws_region} --broker-function ${var.github_token_broker_function_name} + + [Install] + WantedBy=multi-user.target + UNIT + + ignition_config = jsonencode({ + ignition = { + version = "3.3.0" + } + systemd = { + units = [ + { + enabled = true + name = "amazon-ssm-agent.service" + }, + { + contents = local.bootstrap_unit + enabled = true + name = local.bootstrap_unit_name + }, + ] + } + }) +} diff --git a/aws/keycloak-flatcar-spike/moon.yml b/aws/keycloak-flatcar-spike/moon.yml new file mode 100644 index 0000000..2421f13 --- /dev/null +++ b/aws/keycloak-flatcar-spike/moon.yml @@ -0,0 +1,24 @@ +layer: 'application' +tags: + - 'aws' + - 'tofu' + +project: + title: 'AWS Keycloak Flatcar Spike' + description: 'Temporary OpenTofu stack for proving Flatcar Keycloak bootstrap with labctl.' + owner: 'GilmanLab' + maintainers: + - 'josh' + +tasks: + check: + command: './scripts/check.sh' + toolchains: 'system' + inputs: + - '*.tf' + - '.terraform.lock.hcl' + - 'scripts/check.sh' + - 'tests/**/*.tftest.hcl' + options: + cache: false + runInCI: true diff --git a/aws/keycloak-flatcar-spike/network.tf b/aws/keycloak-flatcar-spike/network.tf new file mode 100644 index 0000000..aa6c0c7 --- /dev/null +++ b/aws/keycloak-flatcar-spike/network.tf @@ -0,0 +1,21 @@ +resource "aws_security_group" "flatcar" { + description = "Outbound-only security group for the temporary Flatcar Keycloak spike." + name = var.security_group_name + vpc_id = data.aws_vpc.lab.id + + tags = merge(local.common_tags, { + Name = var.security_group_name + }) +} + +resource "aws_vpc_security_group_egress_rule" "flatcar_ipv4" { + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" + security_group_id = aws_security_group.flatcar.id +} + +resource "aws_vpc_security_group_egress_rule" "flatcar_ipv6" { + cidr_ipv6 = "::/0" + ip_protocol = "-1" + security_group_id = aws_security_group.flatcar.id +} diff --git a/aws/keycloak-flatcar-spike/outputs.tf b/aws/keycloak-flatcar-spike/outputs.tf new file mode 100644 index 0000000..cba297d --- /dev/null +++ b/aws/keycloak-flatcar-spike/outputs.tf @@ -0,0 +1,34 @@ +output "bootstrap_unit_name" { + description = "Systemd unit that fetches the Keycloak bootstrap secret." + value = local.bootstrap_unit_name +} + +output "github_token_broker_function_arn" { + description = "ARN of the GitHub token broker Lambda invoked by the spike instance." + value = data.aws_lambda_function.github_token_broker.arn +} + +output "iam_role_arn" { + description = "IAM role ARN used by the Flatcar spike instance." + value = aws_iam_role.flatcar.arn +} + +output "instance_id" { + description = "EC2 instance ID of the Flatcar spike host." + value = aws_instance.flatcar.id +} + +output "private_ip" { + description = "Private IPv4 address of the Flatcar spike host." + value = aws_instance.flatcar.private_ip +} + +output "public_ip" { + description = "Public IPv4 address associated with the Flatcar spike host for outbound bootstrap traffic." + value = aws_instance.flatcar.public_ip +} + +output "security_group_id" { + description = "Security group attached to the Flatcar spike host." + value = aws_security_group.flatcar.id +} diff --git a/aws/keycloak-flatcar-spike/providers.tf b/aws/keycloak-flatcar-spike/providers.tf new file mode 100644 index 0000000..b0102e8 --- /dev/null +++ b/aws/keycloak-flatcar-spike/providers.tf @@ -0,0 +1,10 @@ +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + "glab:managed-by" = "tofu" + "glab:stack" = "aws/keycloak-flatcar-spike" + } + } +} diff --git a/aws/keycloak-flatcar-spike/scripts/check.sh b/aws/keycloak-flatcar-spike/scripts/check.sh new file mode 100755 index 0000000..f4403ce --- /dev/null +++ b/aws/keycloak-flatcar-spike/scripts/check.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +TF_DATA_DIR="$(mktemp -d)" +trap 'rm -rf "$TF_DATA_DIR"' EXIT +export TF_DATA_DIR + +tofu fmt -check -recursive +tofu init -backend=false -input=false +tofu validate +tofu test diff --git a/aws/keycloak-flatcar-spike/terraform.tf b/aws/keycloak-flatcar-spike/terraform.tf new file mode 100644 index 0000000..b7a66eb --- /dev/null +++ b/aws/keycloak-flatcar-spike/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.10, < 2.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.90" + } + } +} diff --git a/aws/keycloak-flatcar-spike/tests/main.tftest.hcl b/aws/keycloak-flatcar-spike/tests/main.tftest.hcl new file mode 100644 index 0000000..c8919fa --- /dev/null +++ b/aws/keycloak-flatcar-spike/tests/main.tftest.hcl @@ -0,0 +1,200 @@ +mock_provider "aws" { + alias = "mock" + + mock_data "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:us-west-2:123456789012:function:glab-github-token-broker" + } + } + + mock_resource "aws_iam_role" { + defaults = { + arn = "arn:aws:iam::123456789012:role/mock-role" + id = "mock-role" + } + } + + mock_data "aws_subnet" { + defaults = { + id = "subnet-00000000" + } + } + + mock_data "aws_vpc" { + defaults = { + id = "vpc-00000000" + } + } +} + +run "plan_defaults" { + command = plan + + providers = { + aws = aws.mock + } + + assert { + condition = aws_instance.flatcar.ami == "ami-0ce605082061bbb10" + error_message = "The spike should default to the current Flatcar stable arm64 AMI for us-west-2." + } + + assert { + condition = aws_instance.flatcar.instance_type == "t4g.small" + error_message = "The spike should default to the small Graviton instance class." + } + + assert { + condition = aws_instance.flatcar.metadata_options[0].http_tokens == "required" + error_message = "The Flatcar host should require IMDSv2." + } + + assert { + condition = aws_instance.flatcar.metadata_options[0].http_put_response_hop_limit == 2 + error_message = "The Flatcar host should allow containerized labctl to reach IMDSv2." + } + + assert { + condition = aws_instance.flatcar.root_block_device[0].encrypted == true && aws_instance.flatcar.root_block_device[0].volume_type == "gp3" && aws_instance.flatcar.root_block_device[0].volume_size == 16 + error_message = "The spike root volume should be an encrypted 16 GiB gp3 volume matching the Flatcar AMI mapping." + } + + assert { + condition = strcontains(local.ignition_config, "\"version\":\"3.3.0\"") + error_message = "The spike should pass raw Ignition v3 JSON as EC2 user data." + } + + assert { + condition = strcontains(local.ignition_config, "amazon-ssm-agent.service") + error_message = "The spike should enable the Flatcar AWS SSM agent." + } + + assert { + condition = strcontains(local.bootstrap_unit, "ghcr.io/gilmanlab/platform/labctl@sha256:4638b36a168df88d4206d5ff23aed62a6d8459ba7a2481c0b7c65c696445c1ec") + error_message = "The bootstrap unit should use the pinned labctl 0.2.0 image digest." + } + + assert { + condition = strcontains(local.bootstrap_unit, "--user 0:0") + error_message = "The bootstrap container should run as root so it can write into the root-owned 0700 /run directory." + } + + assert { + condition = strcontains(local.bootstrap_unit, "secrets get services/keycloak/bootstrap.sops.yaml --source github --field /stack_env --output /run/glab/keycloak/stack.env") + error_message = "The bootstrap unit should fetch only the Keycloak stack_env field into /run." + } + + assert { + condition = strcontains(local.bootstrap_unit, "--aws-region us-west-2 --broker-function glab-github-token-broker") + error_message = "The bootstrap unit should call the live broker in us-west-2." + } + + assert { + condition = strcontains(aws_iam_role_policy.github_token_broker_invoke.policy, "lambda:InvokeFunction") && strcontains(aws_iam_role_policy.github_token_broker_invoke.policy, "arn:aws:lambda:us-west-2:123456789012:function:glab-github-token-broker") + error_message = "The instance role should be able to invoke only the configured token broker." + } + + assert { + condition = strcontains(aws_iam_role_policy.sops_keycloak_decrypt.policy, "kms:Decrypt") && strcontains(aws_iam_role_policy.sops_keycloak_decrypt.policy, "kms:EncryptionContext:Repo") && strcontains(aws_iam_role_policy.sops_keycloak_decrypt.policy, "GilmanLab/secrets") && strcontains(aws_iam_role_policy.sops_keycloak_decrypt.policy, "kms:EncryptionContext:Scope") && strcontains(aws_iam_role_policy.sops_keycloak_decrypt.policy, "keycloak") + error_message = "The KMS decrypt grant should be constrained to the Keycloak SOPS encryption context." + } +} + +run "plan_overrides" { + command = plan + + providers = { + aws = aws.mock + } + + variables { + flatcar_ami_id = "ami-00000000000000000" + github_token_broker_function_name = "glab-keycloak-staging-github-token-broker" + instance_name = "glab-aws-keycloak-flatcar-staging" + instance_type = "t4g.medium" + } + + assert { + condition = aws_instance.flatcar.ami == "ami-00000000000000000" + error_message = "The Flatcar AMI override should propagate." + } + + assert { + condition = aws_instance.flatcar.instance_type == "t4g.medium" + error_message = "The instance type override should propagate." + } + + assert { + condition = aws_instance.flatcar.tags["Name"] == "glab-aws-keycloak-flatcar-staging" + error_message = "The instance Name tag should honor the instance_name override." + } + + assert { + condition = strcontains(local.bootstrap_unit, "--broker-function glab-keycloak-staging-github-token-broker") + error_message = "The broker function override should propagate into the bootstrap unit." + } +} + +run "reject_unpinned_labctl_image" { + command = plan + + providers = { + aws = aws.mock + } + + variables { + labctl_image = "ghcr.io/gilmanlab/platform/labctl:0.2.0" + } + + expect_failures = [ + var.labctl_image, + ] +} + +run "reject_persistent_bootstrap_output_path" { + command = plan + + providers = { + aws = aws.mock + } + + variables { + bootstrap_output_path = "/etc/keycloak/stack.env" + } + + expect_failures = [ + var.bootstrap_output_path, + ] +} + +run "reject_non_keycloak_secret_path" { + command = plan + + providers = { + aws = aws.mock + } + + variables { + bootstrap_secret_path = "network/vyos/bootstrap.sops.yaml" + } + + expect_failures = [ + var.bootstrap_secret_path, + ] +} + +run "reject_root_volume_growth" { + command = plan + + providers = { + aws = aws.mock + } + + variables { + root_volume_size = 12 + } + + expect_failures = [ + var.root_volume_size, + ] +} diff --git a/aws/keycloak-flatcar-spike/variables.tf b/aws/keycloak-flatcar-spike/variables.tf new file mode 100644 index 0000000..2165190 --- /dev/null +++ b/aws/keycloak-flatcar-spike/variables.tf @@ -0,0 +1,158 @@ +variable "aws_region" { + description = "AWS region in which the Flatcar Keycloak spike instance is created." + type = string + default = "us-west-2" +} + +variable "bootstrap_field" { + description = "RFC 6901 field path extracted from the SOPS file by labctl." + type = string + default = "/stack_env" + + validation { + condition = startswith(var.bootstrap_field, "/") + error_message = "bootstrap_field must be an RFC 6901 pointer beginning with '/'." + } +} + +variable "bootstrap_output_path" { + description = "Path where labctl writes the decrypted dotenv payload on the Flatcar host." + type = string + default = "/run/glab/keycloak/stack.env" + + validation { + condition = startswith(var.bootstrap_output_path, "/run/") + error_message = "bootstrap_output_path must stay under /run for this spike." + } +} + +variable "bootstrap_runtime_dir" { + description = "Ephemeral host directory mounted into the labctl container." + type = string + default = "/run/glab/keycloak" + + validation { + condition = startswith(var.bootstrap_runtime_dir, "/run/") && !endswith(var.bootstrap_runtime_dir, "/") + error_message = "bootstrap_runtime_dir must stay under /run and must not end with '/'." + } +} + +variable "bootstrap_secret_path" { + description = "Path to the SOPS-encrypted Keycloak bootstrap secret in GilmanLab/secrets." + type = string + default = "services/keycloak/bootstrap.sops.yaml" + + validation { + condition = can(regex("^services/keycloak/[^[:space:]]+\\.sops\\.yaml$", var.bootstrap_secret_path)) + error_message = "bootstrap_secret_path must point at a services/keycloak/*.sops.yaml file." + } +} + +variable "flatcar_ami_id" { + description = "Flatcar stable arm64 AMI ID for us-west-2. Recheck the official Flatcar AWS EC2 table before live apply." + type = string + default = "ami-0ce605082061bbb10" + + validation { + condition = can(regex("^ami-[0-9a-f]{17}$", var.flatcar_ami_id)) + error_message = "flatcar_ami_id must be a literal EC2 AMI ID." + } +} + +variable "github_token_broker_function_name" { + description = "Name of the GitHub token broker Lambda used to mint short-lived GitHub Contents credentials." + type = string + default = "glab-github-token-broker" + + validation { + condition = can(regex("^[A-Za-z0-9_-]{1,64}$", var.github_token_broker_function_name)) + error_message = "github_token_broker_function_name must be 1-64 characters and contain only letters, numbers, hyphens, and underscores." + } +} + +variable "iam_role_name" { + description = "IAM role and instance profile name used by the spike instance." + type = string + default = "glab-aws-keycloak-flatcar-spike" +} + +variable "instance_name" { + description = "Name tag for the temporary Flatcar Keycloak spike instance." + type = string + default = "glab-aws-keycloak-flatcar-spike" +} + +variable "instance_type" { + description = "EC2 instance type for the Flatcar spike host." + type = string + default = "t4g.small" +} + +variable "labctl_image" { + description = "Pinned labctl container image used for the bootstrap secret fetch." + type = string + default = "ghcr.io/gilmanlab/platform/labctl@sha256:4638b36a168df88d4206d5ff23aed62a6d8459ba7a2481c0b7c65c696445c1ec" + + validation { + condition = startswith(var.labctl_image, "ghcr.io/gilmanlab/platform/labctl@sha256:") + error_message = "labctl_image must be pinned by digest." + } +} + +variable "public_subnet_name" { + description = "Name tag used to discover the lab foundation public subnet." + type = string + default = "glab-lab-public" +} + +variable "root_volume_size" { + description = "Size, in GiB, of the encrypted gp3 root volume attached to the spike instance." + type = number + default = 16 + + validation { + condition = var.root_volume_size >= 13 && var.root_volume_size <= 100 + error_message = "root_volume_size must be between 13 and 100 GiB for the current Flatcar AMI snapshot." + } +} + +variable "security_group_name" { + description = "Name for the outbound-only security group attached to the spike instance." + type = string + default = "glab-aws-keycloak-flatcar-spike" +} + +variable "sops_kms_context_repo" { + description = "KMS encryption context Repo value allowed for Keycloak SOPS decrypts." + type = string + default = "GilmanLab/secrets" +} + +variable "sops_kms_context_scope" { + description = "KMS encryption context Scope value allowed for Keycloak SOPS decrypts." + type = string + default = "keycloak" +} + +variable "sops_kms_key_arn" { + description = "Customer-managed KMS key used by the secrets repository SOPS rules." + type = string + default = "arn:aws:kms:us-west-2:186067932323:key/2aba1d94-6eaf-4d80-8d26-2077f32fd7c5" + + validation { + condition = can(regex("^arn:aws[a-zA-Z-]*:kms:[a-z0-9-]+:[0-9]{12}:key/[A-Za-z0-9-]+$", var.sops_kms_key_arn)) + error_message = "sops_kms_key_arn must be a literal KMS key ARN." + } +} + +variable "tags" { + description = "Extra tags to merge onto all created resources." + type = map(string) + default = {} +} + +variable "vpc_name" { + description = "Name tag used to discover the lab foundation VPC." + type = string + default = "glab-lab-vpc" +}