How to force users to specify `-target=module.<environment>` when running `terraform apply`?

Viewed 1936

I have a terraform script that provisions a web service on AWS.

Now I want to reuse this script in different environments (production, stage, dev, ...).

Here's my folder structure:

.
├── main.tf
└── core_module
    ├── outputs.tf
    ├── my_service.tf
    └── variables.tf

main.tf contains something like the following:

module "prod-service" {
  source = "./core_module"
  env_specific_variable = "this is a production environment"
  ...
}

module "stage-service" {
  source = "./core_module"
  env_specific_variable = "this is a stage environment"
  ...
}

module "dev-service" {
  source = "./core_module"
  env_specific_variable = "this is a dev environment"
  ...
}

When I want to create the service on the production environment, I run

terraform apply -target=module.prod-service \
  -var 'access_key=<prod_access_key>' \
  -var 'secret_key=<prod_secret_key>' \
  -var 'region=<prod_region>

And when I want to create the service on the stage environment, I run

terraform apply -target=module.stage-service \
  -var 'access_key=<stage_access_key>' \
  -var 'secret_key=<stage_secret_key>' \
  -var 'region=<stage_region>

How can I force the users of this script to add the -target option so that he/she doesn't create all the environments in one single command?

Because different environments require different aws_access_key_id and aws_secret_access_key, running terraform apply to create all environments will create errors.

2 Answers

There are ways to force variables to be set or to be validated. I don't know of a way to enforce something in that target command line attribute. You'd have to use. variable instead and then change your main.tf to only deploy the environment specific version of the module based off some check if that variable.

However, it looks like you aren't using Terraform how it iis ntended. There is a concept called workspaces you should use to manage state of different environments instead of creating duplicate resources in one script for all the different environments.

To create a new workspace use something like

terraform workspace new development terraform workspace new staging

The create a variables file for that environment

development.tfvars

variable env_specific_variable {
  default = "this is a dev environment"
}

staging.tfvara

variable env_specific_variable {
  default = "this is a staging environment"
}

Then in main.tf you deploy just one instance of your module and use the workspace specific environment instead of a literal string.

main.tf

module "my-service" {
  source = "./core_module"
  env_specific_variable = var.env_specific_variable
  ...
}

To deploy development use

terraform workspace select development
terraform apply

To deploy staging use

terraform workspace select staging
terraform apply

Note I am typing this on mobile and haven't ran this. There might be typos or syntax issues depending on your TF version.

Resources: https://www.terraform.io/docs/state/workspaces.html https://www.terraform.io/docs/configuration/variables.html https://blog.gruntwork.io/how-to-manage-terraform-state-28f5697e68fa (good article on general, but specifically check out the section on isolating workspace)

This is not something that the -target argument is appropriate for. The Terraform documentation is explicit that the -target argument is for exceptional situations only, and gives some advice on what to do instead:

This targeting capability is provided for exceptional circumstances, such as recovering from mistakes or working around Terraform limitations. It is not recommended to use -target for routine operations, since this can lead to undetected configuration drift and confusion about how the true state of resources relates to configuration.

Instead of using -target as a means to operate on isolated portions of very large configurations, prefer instead to break large configurations into several smaller configurations that can each be independently applied. Data sources can be used to access information about resources created in other configurations, allowing a complex system architecture to be broken down into more manageable parts that can be updated independently.

In your case, I'd recommend to make each environment a separate Terraform configuration with its own separate Terraform state. Since you've factored out your environments into a shared module, each of them will contain only the module block to call it:

module "service" {
  source = "../../modules/core"
  env_specific_variable = "this is a production environment"
  ...
}

A common directory structure for this approach is:

environments/
  prod/
    main.tf
  stage/
    main.tf
  dev/
    main.tf
modules/
  core/
    variables.tf
    service.tf
    outputs.tf

To apply changes to a specific environment:

cd environments/prod
terraform init
terraform apply

By configuring Terraform like this, you avoid the need to force users to add extra options to their Terraform executions by using Terraform in the standard way. The environment configurations being separate has other benefits too, such as being able to use different provider versions in different environments in case you want to try rolling out a new version on your staging environment first before changing the production configuration.

Related