# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 terraform { required_providers { docker = { source = "kreuzwerker/docker" version = "~> 3.0" } enos = { source = "registry.terraform.io/hashicorp-forge/enos" } } } variable "min_vault_version" { type = string description = "The minimum Vault version to deploy (e.g., 1.15.0 or v1.15.0+ent)" } variable "vault_edition" { type = string description = "The edition of Vault to deploy (ent, ce, ent.fips1403)" default = "ent" validation { condition = contains(["ent", "ce", "ent.fips1403"], var.vault_edition) error_message = "vault_edition must be one of: ent, ce, ent.fips1403" } } variable "vault_license" { type = string description = "The Vault Enterprise license" default = null sensitive = true } variable "cluster_name" { type = string description = "The name of the Vault cluster" default = "vault" } variable "container_count" { type = number description = "Number of Vault containers to create" default = 3 } variable "vault_port" { type = number description = "The port Vault listens on" default = 8200 } variable "use_local_build" { type = bool description = "If true, build a local Docker image from the current branch instead of pulling from Docker Hub" default = false } # HCP-specific variables (ignored but accepted for compatibility) variable "network_name" { type = string description = "Ignored - for HCP compatibility only" default = "" } variable "tier" { type = string description = "Ignored - for HCP compatibility only" default = "" } # Generate a random suffix for the network name to avoid conflicts resource "random_string" "network_suffix" { length = 8 lower = true upper = false numeric = true special = false } # Create Docker network resource "docker_network" "cluster" { name = "${var.cluster_name}-network-${random_string.network_suffix.result}" } locals { # Parse min_vault_version to extract the version number # e.g., "v1.15.0+ent" -> "1.15.0" or "v1.15.0+ent-2cf0b2f" -> "1.15.0" vault_version = trimprefix(split("+", var.min_vault_version)[0], "v") image_map = { "ent" = "hashicorp/vault-enterprise" "ce" = "hashicorp/vault" "ent.fips1403" = "hashicorp/vault-enterprise-fips" } target_map = { "ent" = "ubi" "ce" = "ubi" "ent.fips1403" = "ubi-fips" } image = local.image_map[var.vault_edition] tag_suffix = var.vault_edition == "ce" ? "" : "-ent" image_tag = "${local.vault_version}${local.tag_suffix}" local_tag = "vault-local-${var.vault_edition}:${local.vault_version}" dockerfile = "Dockerfile" target = local.target_map[var.vault_edition] } # Pull image from Docker Hub (when not using local build) resource "docker_image" "vault_remote" { count = var.use_local_build ? 0 : 1 name = "${local.image}:${local.image_tag}" } # Build image from local Dockerfile (when using local build) resource "docker_image" "vault_local" { count = var.use_local_build ? 1 : 0 name = local.local_tag keep_locally = true build { context = "${path.module}/../../.." dockerfile = local.dockerfile target = local.target tag = [local.local_tag] pull_parent = true build_args = { BIN_NAME = "vault" TARGETOS = "linux" TARGETARCH = "amd64" NAME = "vault" PRODUCT_VERSION = local.vault_version PRODUCT_REVISION = "local" LICENSE_SOURCE = "LICENSE" LICENSE_DEST = "/usr/share/doc/vault" } } } locals { # Generate Vault configuration for each node vault_config_template = <<-EOF ui = true listener "tcp" { address = "0.0.0.0:${var.vault_port}" cluster_address = "0.0.0.0:8201" tls_disable = true } storage "raft" { path = "/vault/data" node_id = "node%s" } disable_mlock = true EOF } # Using tmpfs for Raft data (in-memory, no persistence needed for testing) resource "docker_container" "vault" { count = var.container_count name = "${var.cluster_name}-${count.index}" image = var.use_local_build ? docker_image.vault_local[0].name : docker_image.vault_remote[0].image_id networks_advanced { name = docker_network.cluster.name } ports { internal = var.vault_port external = var.vault_port + count.index } tmpfs = { "/vault/data" = "rw,noexec,nosuid,size=100m" } upload { content = format(local.vault_config_template, count.index) file = "/vault/config/vault.hcl" } user = "root" env = concat( [ "VAULT_API_ADDR=http://${var.cluster_name}-${count.index}:${var.vault_port}", "VAULT_CLUSTER_ADDR=http://${var.cluster_name}-${count.index}:8201", "SKIP_SETCAP=true", "SKIP_CHOWN=true", ], var.vault_license != null ? ["VAULT_LICENSE=${var.vault_license}"] : [] ) capabilities { add = ["IPC_LOCK"] } command = ["vault", "server", "-config=/vault/config/vault.hcl"] restart = "no" } locals { instance_indexes = [for idx in range(var.container_count) : tostring(idx)] leader_idx = 0 followers_idx = range(1, var.container_count) vault_address = "http://127.0.0.1:${var.vault_port}" leader_api_addr = "http://${var.cluster_name}-${local.leader_idx}:${var.vault_port}" } # Initialize Vault on the leader resource "enos_local_exec" "init_leader" { inline = [ <<-EOT # Wait for Vault to be ready (output to stderr to keep stdout clean) for i in 1 2 3 4 5 6 7 8 9 10; do if docker exec -e VAULT_ADDR=http://127.0.0.1:${var.vault_port} ${docker_container.vault[local.leader_idx].name} vault status 2>&1 | grep -q "Initialized.*false"; then break fi echo "Waiting for Vault to start (attempt $i/10)..." >&2 sleep 2 done # Initialize Vault and output JSON to stdout docker exec -e VAULT_ADDR=http://127.0.0.1:${var.vault_port} ${docker_container.vault[local.leader_idx].name} vault operator init \ -key-shares=1 \ -key-threshold=1 \ -format=json EOT ] depends_on = [docker_container.vault] } locals { init_data = jsondecode(enos_local_exec.init_leader.stdout) unseal_key = local.init_data.unseal_keys_b64[0] root_token = local.init_data.root_token } # Unseal the leader resource "enos_local_exec" "unseal_leader" { inline = [ "docker exec -e VAULT_ADDR=http://127.0.0.1:${var.vault_port} ${docker_container.vault[local.leader_idx].name} vault operator unseal ${local.unseal_key}" ] depends_on = [enos_local_exec.init_leader] } # Join followers to Raft cluster and unseal them resource "enos_local_exec" "join_followers" { count = length(local.followers_idx) inline = [ <<-EOT # Wait for Vault to be ready for i in 1 2 3 4 5; do docker exec -e VAULT_ADDR=http://127.0.0.1:${var.vault_port} ${docker_container.vault[local.followers_idx[count.index]].name} vault status > /dev/null 2>&1 && break || sleep 5 done # Join the Raft cluster docker exec -e VAULT_ADDR=http://127.0.0.1:${var.vault_port} ${docker_container.vault[local.followers_idx[count.index]].name} \ vault operator raft join ${local.leader_api_addr} # Unseal the follower docker exec -e VAULT_ADDR=http://127.0.0.1:${var.vault_port} ${docker_container.vault[local.followers_idx[count.index]].name} \ vault operator unseal ${local.unseal_key} EOT ] depends_on = [enos_local_exec.unseal_leader] } # Outputs that match HCP module interface output "cloud_provider" { value = "docker" description = "The cloud provider (docker for local)" } output "cluster_id" { value = var.cluster_name description = "The cluster identifier" } output "created_at" { value = timestamp() description = "Timestamp of cluster creation" } output "id" { value = var.cluster_name description = "The cluster identifier" } output "namespace" { value = "root" description = "The Vault namespace" } output "organization_id" { value = "docker-local" description = "The organization identifier" } output "region" { value = "local" description = "The region or location" } output "self_link" { value = "" description = "Self link to the cluster" } output "state" { value = "RUNNING" description = "The state of the cluster" } output "vault_private_endpoint_url" { value = "" description = "Private endpoint URL (not applicable for Docker)" } output "vault_proxy_endpoint_url" { value = "" description = "Proxy endpoint URL (not applicable for Docker)" } output "vault_public_endpoint_url" { value = "http://localhost:${var.vault_port}" description = "Public endpoint URL" } output "vault_version" { value = local.vault_version description = "The version of Vault deployed" } # Docker-specific outputs output "container_names" { value = docker_container.vault[*].name description = "The names of the Vault containers" } output "container_ids" { value = docker_container.vault[*].id description = "The IDs of the Vault containers" } output "vault_addresses" { value = [ for i in range(var.container_count) : "http://localhost:${var.vault_port + i}" ] description = "The addresses of the Vault containers" } output "primary_address" { value = "http://localhost:${var.vault_port}" description = "The address of the primary Vault container" } output "network_id" { value = docker_network.cluster.id description = "The ID of the created Docker network" } output "network_name" { value = docker_network.cluster.name description = "The name of the created Docker network" } output "image_name" { value = var.use_local_build ? (length(docker_image.vault_local) > 0 ? docker_image.vault_local[0].name : "none") : (length(docker_image.vault_remote) > 0 ? docker_image.vault_remote[0].name : "none") description = "The Docker image being used" } output "is_local_build" { value = var.use_local_build description = "Whether this is using a local build" } output "vault_root_token" { value = local.root_token sensitive = true description = "The root token for the Vault cluster" }