# Terraform Integration

Use KeyEnv secrets in your Terraform configurations.

Source: https://keyenv.dev/docs/guides/terraform/

Use KeyEnv secrets in your Terraform configurations using the external data source or CLI.

## Overview

KeyEnv provides a native Terraform provider for seamless secrets integration:

```hcl
terraform {
  required_providers {
    keyenv = {
      source  = "keyenv/keyenv"
      version = "~> 1.0"
    }
  }
}
```

You can also integrate without the provider using the external data source or HTTP data source:

| Method | Best For | Requirements |
|--------|----------|--------------|
| **Native Provider** | All workflows | Service token only |
| **External Data Source** | Legacy workflows | KeyEnv CLI installed |
| **HTTP Data Source** | CI/CD environments | Service token only |

---

## Native Terraform Provider (Recommended)

The `keyenv/keyenv` provider fetches secrets directly via the KeyEnv API.

### Setup

```hcl
terraform {
  required_providers {
    keyenv = {
      source  = "keyenv/keyenv"
      version = "~> 1.0"
    }
  }
}

provider "keyenv" {
  # Or set KEYENV_TOKEN environment variable
  token = var.keyenv_token
}
```

### Fetch All Secrets

```hcl
data "keyenv_environment" "prod" {
  project_id = var.keyenv_project_id
  name       = "production"
}

data "keyenv_secrets" "prod" {
  project_id     = var.keyenv_project_id
  environment_id = data.keyenv_environment.prod.id
}

resource "aws_db_instance" "main" {
  username = data.keyenv_secrets.prod.secrets["DB_USERNAME"]
  password = data.keyenv_secrets.prod.secrets["DB_PASSWORD"]
}
```

### Fetch a Single Secret

```hcl
data "keyenv_secret" "db_url" {
  project_id     = var.keyenv_project_id
  environment_id = data.keyenv_environment.prod.id
  key            = "DATABASE_URL"
}

resource "some_resource" "example" {
  connection_string = data.keyenv_secret.db_url.value
}
```

### Available Data Sources

| Data Source | Description |
|-------------|-------------|
| `keyenv_project` | Fetch project metadata by ID |
| `keyenv_environment` | Look up an environment by name |
| `keyenv_secrets` | Fetch all secrets as a map (sensitive) |
| `keyenv_secret` | Fetch a single secret by key (sensitive) |

---

## Features

- Fetch secrets at plan/apply time
- Support for any Terraform workflow
- Works with Terraform Cloud and local execution
- No state file secrets exposure (values are marked sensitive)

---

## Option 1: External Data Source (Recommended)

Use Terraform's `external` data source to call the KeyEnv CLI.

### Prerequisites

1. Install the KeyEnv CLI:
   ```bash
   curl -fsSL https://keyenv.dev/install.sh | bash
   ```

2. Authenticate with KeyEnv:
   ```bash
   keyenv auth login
   ```

### Basic Usage

```hcl
# main.tf

# Fetch all secrets from an environment
data "external" "keyenv_secrets" {
  program = ["bash", "-c", <<-EOF
    keyenv export --env production --format json 2>/dev/null
  EOF
  ]
}

# Use secrets in your resources
resource "aws_db_instance" "main" {
  identifier = "myapp-db"
  engine     = "postgres"

  username = data.external.keyenv_secrets.result["DB_USERNAME"]
  password = data.external.keyenv_secrets.result["DB_PASSWORD"]
}
```

### With Service Token

For CI/CD environments, use a service token:

```hcl
# main.tf

variable "keyenv_token" {
  type        = string
  sensitive   = true
  description = "KeyEnv service token"
}

variable "keyenv_project" {
  type        = string
  description = "KeyEnv project ID"
}

variable "environment" {
  type    = string
  default = "production"
}

data "external" "keyenv_secrets" {
  program = ["bash", "-c", <<-EOF
    KEYENV_TOKEN="${var.keyenv_token}" \
    keyenv export \
      --project "${var.keyenv_project}" \
      --env "${var.environment}" \
      --format json 2>/dev/null
  EOF
  ]
}
```

### Wrapper Script

For better error handling, create a wrapper script:

```bash
#!/bin/bash
# scripts/fetch-secrets.sh

set -euo pipefail

ENV="${1:-production}"
PROJECT="${KEYENV_PROJECT:-}"

# Build command
CMD="keyenv export --env $ENV --format json"
if [ -n "$PROJECT" ]; then
  CMD="$CMD --project $PROJECT"
fi

# Execute and handle errors
if ! OUTPUT=$($CMD 2>&1); then
  echo '{"error": "Failed to fetch secrets"}' >&2
  exit 1
fi

echo "$OUTPUT"
```

```hcl
# main.tf

data "external" "keyenv_secrets" {
  program = ["bash", "${path.module}/scripts/fetch-secrets.sh", var.environment]
}

output "secrets_loaded" {
  value     = length(keys(data.external.keyenv_secrets.result))
  sensitive = false
}
```

---

## Option 2: HTTP Data Source

Fetch secrets directly from the KeyEnv API without the CLI.

### Setup

```hcl
# main.tf

variable "keyenv_token" {
  type        = string
  sensitive   = true
  description = "KeyEnv service token (starts with env_)"
}

variable "keyenv_project_id" {
  type        = string
  description = "KeyEnv project ID"
}

variable "environment" {
  type    = string
  default = "production"
}

data "http" "keyenv_secrets" {
  url = "https://api.keyenv.dev/api/v1/projects/${var.keyenv_project_id}/environments/${var.environment}/secrets/export"

  request_headers = {
    Authorization = "Bearer ${var.keyenv_token}"
    Content-Type  = "application/json"
  }
}

locals {
  secrets_response = jsondecode(data.http.keyenv_secrets.response_body)

  # Convert array to map for easy access
  secrets = {
    for secret in local.secrets_response.data :
    secret.key => secret.value
  }
}

# Use secrets
resource "aws_db_instance" "main" {
  identifier = "myapp-db"
  engine     = "postgres"

  username = local.secrets["DB_USERNAME"]
  password = local.secrets["DB_PASSWORD"]
}
```

### Error Handling

Add validation for the API response:

```hcl
locals {
  secrets_response = jsondecode(data.http.keyenv_secrets.response_body)

  # Validate response
  secrets = data.http.keyenv_secrets.status_code == 200 ? {
    for secret in local.secrets_response.data :
    secret.key => secret.value
  } : {}
}

# Check for errors
resource "null_resource" "validate_secrets" {
  count = data.http.keyenv_secrets.status_code != 200 ? 1 : 0

  provisioner "local-exec" {
    command = "echo 'Error fetching secrets: ${data.http.keyenv_secrets.status_code}' && exit 1"
  }
}
```

---

## Terraform Cloud / Enterprise

### Using Environment Variables

In Terraform Cloud, set your service token as a sensitive environment variable:

1. Go to your workspace settings
2. Add a variable:
   - **Key**: `TF_VAR_keyenv_token`
   - **Value**: Your service token (starts with `env_`)
   - **Category**: Environment variable
   - **Sensitive**: Yes

### Workspace Example

```hcl
# variables.tf
variable "keyenv_token" {
  type        = string
  sensitive   = true
  description = "KeyEnv service token"
}

variable "keyenv_project_id" {
  type        = string
  description = "KeyEnv project ID"
}
```

---

## Module Pattern

Create a reusable module for KeyEnv integration:

```hcl
# modules/keyenv-secrets/main.tf

variable "project_id" {
  type        = string
  description = "KeyEnv project ID"
}

variable "environment" {
  type        = string
  description = "Environment name"
}

variable "token" {
  type        = string
  sensitive   = true
  description = "KeyEnv service token"
}

variable "api_url" {
  type    = string
  default = "https://api.keyenv.dev"
}

data "http" "secrets" {
  url = "${var.api_url}/api/v1/projects/${var.project_id}/environments/${var.environment}/secrets/export"

  request_headers = {
    Authorization = "Bearer ${var.token}"
    Content-Type  = "application/json"
  }
}

locals {
  response = jsondecode(data.http.secrets.response_body)
}

output "secrets" {
  value = {
    for secret in local.response.data :
    secret.key => secret.value
  }
  sensitive = true
}

output "count" {
  value = length(local.response.data)
}
```

Usage:

```hcl
# main.tf

module "keyenv" {
  source = "./modules/keyenv-secrets"

  project_id  = "proj_abc123"
  environment = "production"
  token       = var.keyenv_token
}

resource "aws_db_instance" "main" {
  username = module.keyenv.secrets["DB_USERNAME"]
  password = module.keyenv.secrets["DB_PASSWORD"]
}
```

---

## Security Best Practices

### 1. Mark Outputs as Sensitive

Always mark secret outputs as sensitive:

```hcl
output "database_password" {
  value     = local.secrets["DB_PASSWORD"]
  sensitive = true
}
```

### 2. Use Minimal Token Scopes

Create service tokens with only the required permissions:
- **Read-only** access to secrets
- Scoped to specific projects
- Scoped to specific environments

### 3. Avoid State File Exposure

Secrets fetched via data sources are stored in Terraform state. Consider:
- Using Terraform Cloud with encrypted state
- Enabling state encryption in your backend
- Using `terraform state rm` to remove sensitive data if needed

### 4. Rotate Tokens Regularly

Use short-lived service tokens and rotate them periodically:

```hcl
variable "keyenv_token" {
  type        = string
  sensitive   = true
  description = "KeyEnv service token - rotate monthly"

  validation {
    condition     = can(regex("^env_", var.keyenv_token))
    error_message = "Token must start with 'env_'"
  }
}
```

---

## Troubleshooting

### "Authentication failed"

- Verify your service token is correct
- Check the token hasn't expired
- Ensure the token has access to the specified project

### "Project or environment not found"

- Verify the project ID is correct
- Verify the environment name matches exactly (case-sensitive)
- Check the project exists in KeyEnv

### "External command failed"

- Ensure the KeyEnv CLI is installed and in PATH
- Run `keyenv export --env <env> --format json` manually to debug
- Check for shell escaping issues in the external data source

### State file contains secrets

This is expected behavior with Terraform data sources. Mitigate by:
- Using encrypted state backends
- Restricting state file access
- Using Terraform Cloud with secure state storage

---

## More Resources

- [terraform-provider-keyenv on GitHub](https://github.com/keyenv/terraform-provider-keyenv)
- [KeyEnv Service Tokens](/docs/tokens) — create a read-only token for Terraform
- [Terraform Registry](https://registry.terraform.io/providers/keyenv/keyenv)
