Terraform

reading time 5 mins

This guide will show you how to use Doppler to securely read and write secrets in Terraform.

Prerequisites

  • You have created a project in Doppler
  • You are familiar with using Terraform to manage infrastructure

❗️

Rename Actions Not Supported

The Doppler provider doesn't currently support rename actions. If you attempt to rename a project or config from Terraform, then what will happen is the original project or config will be deleted and then recreated with the new name. This means that any secrets managed outside of Terraform and any syncs that had been setup will also have been destroyed and need to be recreated.

If you must rename a resource, you should remove the old resources from your Terraform code and then remove those resources from the Terraform state. You can then add resource blocks for the newly named resources and import them back into your Terraform state. The resource ID format used for Doppler objects in Terraform is described at the bottom of each resource in the Terraform registry docs.

❗

create_before_destroy lifecycle argument

Do not use the create_before_destroy lifecycle argument with Doppler resources. Unlike other provider resources where you can create a new resource alongside an existing resource, Doppler's resources don't behave in this fashion. There can only be one STRIPE_API_KEY secret in a config, or one ci environment. If create_before_destroy is used, it will first update the existing resource (rather than create a new one) and will subsequently destroy the only existing copy. After which it will report that everything succeeded, even though the end state is not the desired state.

🚧

Secure your Terraform state

If you're using the Doppler Terraform provider for managing secrets, it's important to note that secret values will end up being stored in your Terraform state files. All sensitive fields are marked "sensitive" in Terraform, meaning that they can't be printed to console without jumping through some explicit hoops, but the data itself is still stored in the state file. This is the same behavior you'll find in the AWS Secrets Manager, GCP Secret Manager, and HashiCorp Vault providers. We recommend that you follow HashiCorp's recommendation when it comes to sensitive state data and ensure that your state files are secure – including storing them somewhere with encryption at rest.

Passing Doppler Secrets as Input Variables

The Doppler CLI can be used to pass secrets directly to Terraform without the need for a provider at all.

# Automatically converts Doppler secrets to lowercase Terraform variables
doppler run --name-transformer tf-var -- terraform plan

The --name-transformer tf-var flag will transform the names of your Doppler secrets into the Terraform Environment Variable format.

For example, a variable named API_KEY will be converted into TF_VAR_api_key by the name transformer and Terraform will automatically find this value if you have a Terraform variable named api_key.

Read on to learn about the Doppler Terraform provider and more complex use cases.

Provider Overview

πŸ“˜

The Doppler provider is fully compatible with OpenTofu (previously named OpenTF), the open-source fork of Terraform. You can replace terraform with tofu in any example commands to accomplish the same outcomes.

There are two main ways to use the Doppler Terraform Provider:

  • Reading Secrets
    • Fetch secrets from Doppler and use them in your Terraform configuration
    • Use the doppler_secrets Data Source
  • Managing Resources
    • Create, update, and delete Doppler secrets, projects, environments, configs, or service tokens from Terraform
    • Use the doppler_secret, doppler_project, doppler_environment, doppler_project, and doppler_service_token Resources
  • Managing Projects

Reading Secrets

In this example, we'll use the doppler_secrets data source to read secrets from a config:

# Install the Doppler provider
terraform {
  required_providers {
    doppler = {
      source = "DopplerHQ/doppler"
    }
  }
}

# Define a variable so we can pass in our token
variable "doppler_token" {
  type = string
  description = "A token to authenticate with Doppler"
}

# Configure the Doppler provider with the token
provider "doppler" {
  doppler_token = var.doppler_token
}

# Define our data source to fetch secrets
data "doppler_secrets" "this" {}

# Access individual secrets
output "stripe_key" {
  # nonsensitive used for demo purposes only
  value = nonsensitive(data.doppler_secrets.this.map.STRIPE_KEY)
}

To read Doppler secrets from Terraform, we just need to create a Service Token to provide read-only access to a specific config.

There are several ways to pass variables to Terraform, but for now, we can just provide our Doppler token when prompted by Terraform.

First, run terraform init to setup your Terraform project, then run terraform apply to test the configuration:

Terraform used the Doppler provider to fetch the secrets from your config.

All of the secrets that you load from Doppler will be loaded into Terraform as strings. You can use Terraform's built-in parsing functions to convert secrets to their Terraform types:

# Use `tonumber` and `tobool` to parse string values into Terraform primatives
output "max_workers" {
  value = nonsensitive(tonumber(data.doppler_secrets.this.map.MAX_WORKERS))
}

# JSON values can be decoded direcly in Terraform
# e.g. FEATURE_FLAGS = `{ "AUTOPILOT": true, "TOP_SPEED": 130 }`
output "json_parsing_values" {
  value = nonsensitive(jsondecode(data.doppler_secrets.this.map.FEATURE_FLAGS)["TOP_SPEED"])
}

Reading Secrets from Multiple Projects

In this example, we use multiple Doppler providers with aliases to access secrets from two separate configs. This can be useful when you need to use multiple access tokens that are scoped to a single project and config in your Terraform run.

terraform {
  required_providers {
    doppler = {
      source = "DopplerHQ/doppler"
    }
  }
}

variable "doppler_token_dev" {
  type = string
  description = "A token to authenticate with Doppler for the dev config"
}

variable "doppler_token_prd" {
  type = string
  description = "A token to authenticate with Doppler for the prd config"
}

provider "doppler" {
  doppler_token = var.doppler_token_dev
  alias = "dev"
}

provider "doppler" {
  doppler_token = var.doppler_token_prd
  alias = "prd"
}

data "doppler_secrets" "dev" {
  provider = doppler.dev
}

data "doppler_secrets" "prd" {
  provider = doppler.prd
}

output "port-dev" {
  value = nonsensitive(data.doppler_secrets.dev.map.PORT)
}

output "port-prd" {
  value = nonsensitive(data.doppler_secrets.prd.map.PORT)
}

Managing Secrets

In this example, we'll use the doppler_secret resource to write to secrets in our Doppler config:

# Install the Doppler provider
terraform {
  required_providers {
    doppler = {
      source = "DopplerHQ/doppler"
    }
  }
}

# Define a variable so we can pass in our token
variable "doppler_token" {
  type = string
  description = "A token to authenticate with Doppler"
}

# Configure the Doppler provider with the token
provider "doppler" {
  doppler_token = var.doppler_token
}

# Generate a random password
resource "random_password" "db_password" {
  length = 32
  special = true
}

# Save the random password to Doppler
resource "doppler_secret" "db_password" {
  project = "rocket"
  config = "dev"
  name = "DB_PASSWORD"
  value = random_password.db_password.result
}

# Access the secret value
output "resource_value" {
  # nonsensitive used for demo purposes only
  value = nonsensitive(doppler_secret.db_password.value)
}

To write Doppler secrets, we'll need to provide a Doppler Personal Token in our Terraform configuration. You can generate a new Personal Token from the Doppler dashboard on the "Tokens" page.

Similar to our example for reading secrets, we'll run terraform init and then terraform apply to test the configuration:

Terraform generated a new password and saved it to our Doppler config as DB_PASSWORD.

Terraform will still store all of this data in Terraform state, but having your secrets in Doppler allows for much easier access to these secrets for all developers on your team.

Managing Projects, Environments, and Configs

In this example, we'll use the doppler_project, doppler_environment, and doppler_config resources to manage a project in our Doppler workplace:

# Install the Doppler provider
terraform {
  required_providers {
    doppler = {
      source = "DopplerHQ/doppler"
    }
  }
}

# Define a variable so we can pass in our token
variable "doppler_token" {
  type        = string
  description = "A token to authenticate with Doppler"
}

# Configure the Doppler provider with the token
provider "doppler" {
  doppler_token = var.doppler_token
}

# Create and manage your project
resource "doppler_project" "rocket" {
  name        = "rocket"
  description = "To the moon and back."
}

# Create and manage your environments
resource "doppler_environment" "rocket_dev" {
  project = doppler_project.rocket.name
  name    = "Development"
  slug    = "dev"
}

resource "doppler_environment" "rocket_stage" {
  project = doppler_project.rocket.name
  name    = "Staging"
  slug    = "stage"
}

resource "doppler_environment" "rocket_ci" {
  project = doppler_project.rocket.name
  name    = "CI"
  slug    = "ci"
}

resource "doppler_environment" "rocket_prod" {
  project = doppler_project.rocket.name
  name    = "Production"
  slug    = "prod"
}

# Create and manage any branch configs you may want
resource "doppler_config" "rocket_ci_github" {
  project     = doppler_project.rocket.name
  environment = doppler_environment.rocket_ci.slug
  name        = "${doppler_environment.rocket_ci.slug}_github"
}

resource "doppler_config" "rocket_ci_circleci" {
  project     = doppler_project.rocket.name
  environment = doppler_environment.rocket_ci.slug
  name        = "${doppler_environment.rocket_ci.slug}_circleci"
}

We define a new Doppler project named rocket and set up four Environments in it (dev, stage, ci, and prod). We then specify two Branch Configs to manage under the ci environment.

Similar to previous examples, run terraform init and then terraform apply to test the configuration.

Managing Service Tokens

In this example, we'll use the doppler_service_token resource to manage a service token in our Doppler workplace:

# Install the Doppler provider
terraform {
  required_providers {
    doppler = {
      source = "DopplerHQ/doppler"
    }
  }
}

# Define a variable so we can pass in our token
variable "doppler_token" {
  type        = string
  description = "A token to authenticate with Doppler"
}

# Configure the Doppler provider with the token
provider "doppler" {
  doppler_token = var.doppler_token
}

# Create and manage your project
resource "doppler_project" "rocket" {
  name        = "rocket"
  description = "To the moon and back."
}

# Create and manage your environments
resource "doppler_environment" "rocket_ci" {
  project = doppler_project.rocket.name
  name    = "CI"
  slug    = "ci"
}

# Create and manage your service token
resource "doppler_service_token" "rocket_ci_token" {
  project = doppler_environment.rocket_ci.project
  config  = doppler_environment.rocket_ci.slug
  name    = "Rocket CI Token"
  access  = "read"
}

# Service token key available as `doppler_service_token.rocket_ci_token.key

We define a new Doppler project named rocket and set up a single ci environment. We then specify a service token for the rocket project under the ci environment. In this case, we're referencing the environment slug because this is the name for the root config in that environment. If you had Branch Configs under that environment, then you would want to reference a doppler_config resource for that config.

Similar to previous examples, run terraform init and then terraform apply to test the configuration.

Managing Integrations and Syncs

In this example, we'll use the doppler_integration_aws_secrets_manager and doppler_secrets_sync_aws_secrets_manager resources to setup a secrets sync from Doppler to AWS Secrets Manager.

resource "aws_iam_role" "doppler_secrets_manager" {
  name = "doppler_secrets_manager"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = "sts:AssumeRole"
        Principal = {
          AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator"
        },
        Condition = {
          StringEquals = {
            "sts:ExternalId" = "<YOUR_WORKPLACE_SLUG>"
          }
        }
      },
    ]
  })

  inline_policy {
    name = "doppler_secret_manager"

    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action = [

            "secretsmanager:GetSecretValue",
            "secretsmanager:DescribeSecret",
            "secretsmanager:PutSecretValue",
            "secretsmanager:CreateSecret",
            "secretsmanager:DeleteSecret",
            "secretsmanager:TagResource",
            "secretsmanager:UpdateSecret"
          ]
          Effect   = "Allow"
          Resource = "*"
          # Limit Doppler to only access certain secret names
        },
      ]
    })
  }
}

resource "doppler_integration_aws_secrets_manager" "prod" {
  name            = "Production"
  assume_role_arn = aws_iam_role.doppler_secrets_manager.arn
}

resource "doppler_secrets_sync_aws_secrets_manager" "backend_prod" {
  integration = doppler_integration_aws_secrets_manager.prod.id
  project     = "backend"
  config      = "prd"

  region = "us-east-1"
  path   = "/backend/"
}

We start by defining a new AWS IAM role which Doppler will assume to perform sync operations. Note that you'll need to provide your workplace slug (available in all Doppler dashboard URLs as https://dashboard.doppler.com/workplace/:slug/).

Then, we create a doppler_integration_aws_secrets_manager resource which uses the new AWS role. Finally, we create a doppler_secrets_sync_aws_secrets_manager resource to configure the sync between our backend.prd doppler project and the /backend/doppler secret in AWS Secrets Manager.

Similar to previous examples, run terraform init and then terraform apply to test the configuration.

Other integrations can be connected using similar resources. Check out our Terraform provider docs for all of the available options.

Things to Watch Out For

Terraform Password Generation

Terraform has a built-in resource called random_password that can be used to seed passwords. If you use this to seed passwords that end up getting used in YAML configuration files, you may want to remove the # from the override_special list. Otherwise, you can end up with passwords with a # character in them which YAML will interpret as a comment when it gets parsed. In a worst case scenario, you may end up with a password like #1sjh13l91, which is stored in Doppler fine, but when fetched from Doppler and used in a YAML config file will end up loading as null.

πŸ‘

Awesome Work!

Now you know to use the Doppler Terraform provider to securely read and manage secrets for your infrastructure.