Usefull links:
- Terraform Cloud: https://www.terraform.io/cloud
- Terraform recipes: https://gist.github.com/nicolasdao/d90015ff90aae77c0a599621f5a8f432
- Providers list: https://registry.terraform.io/namespaces/hashicorp
- AWS provider config: https://registry.terraform.io/providers/hashicorp/aws/latest/docs
Rule of thumb: To figure out how to configure a resource, simply Google
terraform <resource name>
WARNING: The
workspace
concept is very confusing. For now, please be aware that it means different things whether you're managing your project locally or via Terraform Cloud. The difference between the two contexts is explained in the The Workspace conundrum section.
- Overview
- Install, setup & prerequisites
- Getting started
- The Workspace conundrum
- Commands
- Functions
- Dealing with drift
- Terraform Cloud
- Git workflows and dev lifecycles
- CDK - Cloud Developmemt Kit
- Troubleshooting
- FAQ
- How to debug?
- How to delete a resource?
- How to restrict a provider to a specific account ID?
- How to load a JSON file to access its properties?
- How to list the details of a provisioned resource?
- How to deploy a specific resources instead of the entire project stack?
- How to migrate a workspace to another?
- How to remove all resources from the state?
- Where are my Terraform Cloud credentials stored on my local machine?
- Annexes
- References
Terraform is an infrastructure-as-code tool that supports all the major Cloud providers. Its core concepts are:
- Infrastructure as cattle, not pets, i.e., provisioning an immutable infrastructure rathen than mutating it (1).
- Declarative approach over procedural, i.e., declare what you want (end result) via config files (
.tf
files) rather than coding how to get what you want. With the declarative approach, declaring 5 servers must be provisioned guarantees that 5 servers will run in total. With the procedural, there are risks that 5 additional servers are created, rasing the total number to more than 5. - Use the CLI tool locally, or use Terraform Cloud to store states and execute operations. The first one is good to get started, but the second one is the one you'd probably want to collaborate, govern and automate your infrastructure (CI/CD).
(1) Tools that would mutate the infrastructure: Chef, Puppet, Ansible.
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
brew update
brew upgrade hashicorp/tap/terraform
To test the install worked:
terraform -help
To install auto-complete:
- Make sure you already have a
.bashrc
(if you're using bash) or.zshrc
(if you're using Zsh). - Run this command:
terraform -install-autocomplete
This section helps to get started using the simplest approach, which is to use the Terraform CLI. In this context, the entire development is done on your local machine without using any remote or external systems. In reality, you will probably not do this and switch to using Terraform Cloud to improve collaboration, governance, security and automation. Terraform Cloud is covered in another section. We recommend you get familiar with the core concepts of this section first before jumping into Terraform Cloud.
This section does not cover Git workflows and dev lifecycles as those workflows depend on understanding the difference between Terraform CLI and Terraform Cloud (which is mainly the Workspace conundrum). This topic is covered later in the Git workflows and dev lifecycles section.
- A Terraform project describes the infrastructure in
.tf
files. They look similar to JSON files, but they're not. The language is referred to asHCL
(HashiCorp Configuration Language
). All.tf
files will be executed regardless of their name. - Each
.tf
file can define the following blocks:provider
: That's typically a cloud provider such as AWS, Google and so on (AWS example here).resource
: That's the actual infrastructure piece that you wish to provision (AWS S3 example here).- You could stop there and with those two concepts, you'd be able to deploy anything. However, as you project becomes more complex, the following other concepts become necessary:
variable
: A variable is a definition that helps passing arguments to your deployment. A variable does not define a value, but can define a default value when a value is not explicitely passed.locals
: A series of local variables (not related to the variable above) that can use some native functions which usually helps managing values with string operations (e.g., string concatenation, upper/lower case, ...).output
: Helps to modularize project by returning values so that other modules or projects can use them. Also use for debugging as outputs are printed in the terminal.data
: Data allows you to query provisioned Cloud resources to extract their details and use them in any module or project.
module
: Helps to encapsulate logic in reusable resources.
- The order in which files and their content is provisioned is inferred automatically by Terraform. You can define a resource in one file and refer to it in another without any problems.
- You can configure the entire infra in a single file, but that's not recommended. Instead, try to break it down in logical blocks that reflect your intent.
- The other file types are:
.tfvars
: This files stores the explicit value ofvariable
definitions. A typical use case would be adev.tfvars
andprod.tfvars
..tfstate
: This is where the magic happens. It stores the current infrastructure state. That's how Terraform knows something has changed. There is no constraints on how that file should be named, but the usual convention is to name itterraform.tfstate
.
- To manage environments such as dev and prod, Terraform introduces the concept of
Workspaces
(in Pulumi, this is equivalent to Stacks). The idea is to define specific configurations stored in.tfvars
files that can be associated with each workspace so that deploying a specific environment is only a matter of switching workspaces and then executing aterraform apply
. - By default, the state of your infra is stored on your local machine inside a
terraform.tfstate
file. This file is critical to run your project, but it cannot be put under source-control as it may contain sensitive information. To solve this challenge, Terraform can usebackends
other than the defaultlocal
backend (which stores theterraform.tfstate
file locally). A backend is responsible for storing the.tfstate
file. To sync your.tfstate
so that concurrent deployment don't create unexpected results, choose a backend such asS3
or Terraform Cloud). For more details, please refer to the backends section.
Please refer to the Commands section for a description of the main commands.
Once you have your files ready, the first command to use is:
terraform init
This command will create the default terraform.tfstate
. This command must be run again if you update the backend.
When you're ready to deploy your changes, run:
terraform apply
Don't worry, this command will review your changes first (equivelant to
terraform plan
). It will prompt you to confirm if the details are correct before applying them. If you do not want to be asked to review over and over again, useterraform apply -auto-approve
instead.
To list the details of a provisioned resource:
terraform state show aws_s3_bucket.www
If you want to filter a specific property:
terraform state show aws_s3_bucket.www | grep website_endpoint
Terraform uses the same login procedure as the specific cloud CLI (e.g., AWS CLI, gcloud CLI). To deploy to the right account, make sure you are logged in to it via your preferred CLI (if NPX is installed on your machine, simply run npx switch-profile
).
Create a main.tf
file as follow:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Configure the default AWS Provider
provider "aws" {
region = "ap-southeast-2"
}
data "aws_s3_bucket" "selected" {
bucket = "qwerty4321"
}
output "my_bucket" {
value = data.aws_s3_bucket.selected
}
Where
qwerty4321
should be replaced by a bucket's name that exists in your logged in AWS account (also, make sure theregion
for your S3 bucket matches the one defined above (i.e.,ap-southeast-2
), otherwise change it).
then run:
terraform init
then:
terraform plan
If this works, this command should output the bucket's details. This rest of this section covers how this file works.
A provider
is usually the first thing you define in your project. It looks like this:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Configure the default AWS Provider
provider "aws" {
region = "ap-southeast-2"
}
AWS regions available in the annexes under the AWS regions section.
To provision a resource like an S3 bucket, use the resource
block:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Configure the default AWS Provider
provider "aws" {
region = "ap-southeast-2"
}
# More S3 resource APIs at https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket
resource "aws_s3_bucket" "b" {
bucket = "my-tf-test-bucket-43917451"
acl = "private"
tags = {
Name = "My bucket"
Environment = "Dev"
}
}
Then run:
terraform init
Only if your project had not been initialized yet
and then:
terraform apply
To delete a resource, simply remove it from the .tf
file, then run terraform apply
.
As you can see, you have to define one provider per region. If you need to define multiple region, use alias
as follow:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Configure the default AWS Provider
provider "aws" {
region = "ap-southeast-2"
}
# Configure the US West AWS Provider
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_s3_bucket" "b" {
bucket = "my-tf-test-bucket-43917451"
acl = "private"
tags = {
Name = "My bucket"
Environment = "Dev"
}
}
resource "aws_s3_bucket" "b_west" {
provider = aws.west
bucket = "my-tf-test-bucket-43917451-west"
acl = "private"
tags = {
Name = "My bucket west"
Environment = "Dev"
}
}
Because you may have multiple account connected to your local machine, it helps to explicitely define which account is targetted to avoid disrupting an account in prod. Use the allowed_account_ids
property on the provider as follow:
provider "aws" {
allowed_account_ids = ["123455"]
region = "ap-southeast-2"
}
The output
block helps to:
- Debug by getting explicit data back after a
terraform plan
orterraform apply
. - Expose specific data if the project is meant to be a module. In the case, loading that module will expose those outputs.
In our simple case, we may want to get the details of the bucket we've just provisioned:
output "my_buckets" {
value = aws_s3_bucket.b.id
description: "My bucket ID"
}
As you can see, an output
requires a value
property and an optional description
property.
The above will output:
my_buckets = "my-tf-test-bucket-43917451"
To output the entire bucket data:
output "my_buckets" {
value = aws_s3_bucket.b
description: "My bucket data"
}
Now that we have provisioned something, we can explicitely query its metadata via the data
block.
data "aws_s3_bucket" "bb" {
bucket = aws_s3_bucket.b.bucket
}
output "my_buckets" {
value = data.aws_s3_bucket.bb
}
Ignoring critical files in the .gitignore
is important as those files may contain sensitive pieces of information. At a minimum, ignore those files and folder:
.terraform/
terraform.tfstate
terraform.tfstate.backup
terraform.tfstate.*.backup
The .terraform/
folder is ignored because it includes artefact binaries. The terraform.tfstate
may contain sensitive pieces of information. As for terraform.tfstate.backup
(1) and terraform.tfstate.*.backup
(2), .backup
is a defacto convention to backup the .tfstate
, so it must also be excluded.
IMPORTANT: Because the
terraform.tfstate
should not be version-controlled, collaboration between multiple engineers requires a backend other than the defaultlocal
. Please refer to the backends section for more details.
You may also want to ignore some .tfvars
files if they contain sensitive information:
.terraform/
terraform.tfstate
terraform.tfstate.backup
*.tfvars
(1) This file is automatically created when the
terraform refresh
command is executed. (2) These files are automatically created when theterraform state rm <RESOURCE NAME>
command is executed.
Before jumping into provisioning a static website on S3, let's learn how to manage strings with Terraform, as this is a common task in many automation processes.
With Terraform, you usually either use inline strings or load them from a file or remote location.
When a property requires a string which contains line breaks, use one of the following 2 options:
Option 1 - Maintain indentation:
someProp = <<WHATEVER_YOU_WANT
Some text that says:
Hello world
WHATEVER_YOU_WANT
With this option, the string passed to someProp
is:
Some text that says:
Hello world
Option 2 - No indentation:
someProp = <<-WHATEVER_YOU_WANT
Some text that says:
Hello world
WHATEVER_YOU_WANT
With this option, the string passed to someProp
is:
Some text that says:
Hello world
If your string is inside a local file, then use the file
function to access it as follow:
# Creates a public bucket to host static website.
# Doc: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#static-website-hosting
resource "aws_s3_bucket" "website_bucket" {
bucket = "cloudlesslabs.com"
acl = "public-read"
policy = file("public_website_policy.json")
website {
index_document = "index.html"
error_document = "error.html"
}
versioning {
enabled = true
}
}
Let's go back to provisioning a static website on AWS S3.
locals {
domain = "cloudlesslabs.com"
}
# Creates a public bucket to host static website.
# Doc: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#static-website-hosting
resource "aws_s3_bucket" "website_bucket" {
bucket = local.domain
acl = "public-read"
policy = <<-POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::${local.domain}/*"
}
]
}
POLICY
website {
index_document = "index.html"
error_document = "error.html"
}
versioning {
enabled = true
}
}
The sample above shows that using the local.domain
local variable, we can maintain that value in a single place.
Very often, you'll have to refer to the same object (string, number, ...) over and over again. This is error prone as well we tedious to manage when an update is required. To deal with this challenge, Terraform uses locals
and variable
blocks.
locals
is the easiest to use:
locals {
hello = "world"
}
output "msg" {
value = "Hello ${local.hello}"
}
Running terraform plan
outputs Hello world
.
Notice the string interpolation
$
in"Hello ${local.hello}"
. This is the same as in Javascript.
Use locals
when you need to manage data in a single place throughout your project and that data does not vary from one environment to another (if the data changes whether you're in dev or prod, use variable
instead of locals
). locals
can be defined in their own .tf
file and referred in others.
variable
can be used similarly to locals
if it defines a default
value, but its purpose is different. A variable
is a definition. Once defined, values can be injected in your project via the CLI using explicit values or .tfvars
files. For example:
Using variable
as a replacement for locals
:
variable "hello" {
default = "world"
}
output "msg" {
value = "Hello ${var.hello}"
}
Running terraform plan
outputs Hello world
. But running terraform plan -var="hello=Sam"
outputs Hello Sam
.
To pass multiple values use a command similar to this: terraform plan -var="hello=Sam" -var="var2=something else"
.
Using the following some.tfvars
:
hello = "Little File"
Running terraform plan -var-file="some.tfvars"
outputs: Hello Little File
.
Additionally, variables support the following 4 optional arguments:
type
,description
,validation
andsensitive
. More about those arguments at https://www.terraform.io/docs/language/values/variables.html#arguments.
Variables stored inside a terraform.tfvars
or any file ending with .auto.tfvars
is in the root folder are automatically applied to the terraform plan
or terraform apply
without requiring the -var-file
option.
This section focuses on improving maintainability and reusability.
The dynamic
block and the count
meta-argument are designed to primarily create multiple blocks of the same type, but with different values. This section will also show a design pattern to use it to emulate a conditional block, i.e., the ability to toggle a configuration on or off.
In cases where you need to provide the same resource multiple times (e.g., multiple EC2 instances):
resource "aws_instance" "server" {
count = 4 # create four similar EC2 instances
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "Server ${count.index}"
}
}
The dynamic
block allows to rewrite this:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
...
setting {
name = "Subnets"
namespace = "aws:ec2:vpc"
value = "subnet-06dcc5d225b48b816"
}
setting {
name = "VPCId"
namespace = "aws:ec2:vpc"
value = "vpc-00e555dc5f3a3cbc2"
}
}
as this:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
...
dynamic "setting" {
for_each = var.settings
content {
name = setting.value["name"]
namespace = setting.value["namespace"]
value = setting.value["value"]
}
}
}
Where settings
is defined in a .tfvars
file as follow:
settings = [
{
name = "Subnets"
namespace = "aws:ec2:vpc"
value = "subnet-00e555dc5f3a3cbc2"
},
{
name = "VPCId"
namespace = "aws:ec2:vpc"
value = "vpc-06dcc5d225b48b816"
},
]
As of July 2021, Terraform does not support native constructs to toggle a configuration on or off, i.e., conditional block. This section details a couple of design patterns which allows to emulate this feature.
locals {
my_function_name = "awesome-function"
}
resource "aws_lambda_function" "my_lambda_java" {
filename = "lambda.jar"
function_name = "${local.my_function_name}"
handler = "Handler"
runtime = "java8"
count = var.lambda_type == "java" ? 1 : 0
}
resource "aws_lambda_function" "my_lambda_javascript" {
filename = "lambda.jar"
function_name = "${local.my_function_name}"
handler = "index.handler"
runtime = "nodejs10.x"
count = var.lambda_type == "javascript" ? 1 : 0
}
WARNING: Because we're using
count
,aws_lambda_function.my_lambda_java
andaws_lambda_function.my_lambda_javascript
are now arrays and not single object. To access their properties, you must useaws_lambda_function.my_lambda_java[0]
instead ofaws_lambda_function.my_lambda_java
.
variable "maintenance_mode" {
type = bool
default = false
}
resource "aws_lb_listener" "this" {
load_balancer_arn = aws_lb.this.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
dynamic "default_action" {
for_each = var.maintenance_mode ? [] : [1]
content {
type = "forward"
target_group_arn = aws_lb_target_group.this.arn
}
}
dynamic "default_action" {
for_each = var.maintenance_mode ? [1] : []
content {
type = "fixed-response"
fixed_response {
content_type = "text/html"
message_body = file("${path.module}/pages/scheduled-maintenance.html")
status_code = "503"
}
}
}
}
IMPORTANT: Modules cannot define a
provider
block.provider
blocks can only be defined in the root project.
A local module is simply a sub-folder that contains:
- Some
.tf
files that describe the resources for that module. - Optionally a
variables.tf
(or whatever you wish to call this file) that contains the possible inputs for this module. You would only definevariable
blocks in that file. - Optionally an
outputs.tf
(or whatever you wish to call this file) that contains the possible outputs for this module. You would only defineoutput
blocks in that file.
To refer that module in the rest of your project, use the module
block:
module lambda_edge {
source = "./modules/lambda_edge"
name = "Super cool" # name would be a variable defined in your module.
}
Modules can also be deployed in isolation of the rest of the stack:
terraform apply -target="module.lambda_edge"
Terraform workspaces is a confusing concept as its implementation varies based on its execution context. This section focuses on managing workspaces locally with the CLI. To learn how to manage workspaces with Terraform Cloud, or how to migrate a workspace managed by the CLI to Terraform Cloud, please refer to the Terraform Cloud Overview section.
In practice:
- Create a new workspace:
terraform workspace new prod
- Use the workspace's name in your resources:
"The current workspace is ${terraform.workspace}"
- Deploy a workspace:
terraform workspace select prod && terraform apply -var-file=prod.tfvars
WARNING!!!:
terraform.workspace
is only usefull if you plan to manage your project locally via the CLI. IT WILL NOT WORK AS EXPECTED WITH TERRAFORM CLOUD. Indeed, with the in Terraform Cloud,terraform.workspace
will always returndefault
😠👿🤯.
Workspaces allow to manage multiple variations of the same infrastructure (e.g., dev, prod). You'll manage the same .tf
files, but there will be more than one .tfstate
(one for each workspace). Those new .tfstate
files are located under a new terraform.tfstate.d
folder. You will also typically replace the terraform.tfvars
with workspace specific .tfvars
(e.g., dev.tfvars
, prod.tfvars
). Deploying your changes to an environment will change from executing a single command:
terraform apply
to execute 2 commands:
terraform workspace select dev
terraform apply -var-file=dev.tfvars
As of July 2021, it is not possible to explicitely link dev.tfvars to the dev workspace. We have to remember to apply the right tfvars to the right workspace.
To access the current workspace name in your .tf
file:
output current_workspace {
value = "The current workspace is ${terraform.workspace}"
}
The default workspace is "default"
.
WARNING!!!:
terraform.workspace
is only usefull if you plan to manage your project locally via the CLI. IT WILL NOT WORK AS EXPECTED WITH TERRAFORM CLOUD. Indeed, with the in Terraform Cloud,terraform.workspace
will always returndefault
😠👿🤯.
Command | Description |
---|---|
terraform workspace list |
Lists all the workspaces. |
terraform workspace new prod |
Creates a new workspace. |
terraform workspace select dev |
Selects a workspace. |
terraform workspace show |
Shows the current workspace. |
terraform workspace delete prod |
Deletes a workspace. |
terraform state push terraform.tfstate |
Migrate the terraform.tfstate into the current selected workspace. |
This situation happens when the first terraform draft was done is the default workspace. Later on, when that draft seems stable and the idea of having multiple environments arises, you may want to isolate those environments in their own workspaces (e.g., dev, prod). You may not want to destroy the default workspace and reprovision a new dev or prod. This section shows how to migrate the default worspace into a new dev workspace.
- Create a new workspace:
terraform workspace new dev
- Confirm dev is the current workspace:
terraform workspace show
- Add the default state to dev:
terraform state push terraform.tfstate
- Clean the deafult state (that's not required, but recommended to avoid human mistakes):
terraform workspace select default
terraform state list | xargs -L 1 terraform state rm
- Delete the
terraform.tfstate
in your root folder (i.e., thedefault
workspace) if you know you won't need it anymore.
Usefull links:
- Terraform Cloud backend config: https://www.terraform.io/docs/language/settings/backends/remote.html#example-configurations
- S3 backend config: https://www.terraform.io/docs/language/settings/backends/s3.html#example-configuration
A backend is an optional configuration (default is local
). Terraform's backends are similar to Pulumi's backends. They primarily help storing an infrastructure's project's state. Contrary to Pulumi, some Terraform's backends can replace the CLI and run operations (though some could argue that Pulumi is capable of running operations without its CLI via its Automation API, but IMHO, that's a different use case). When a Terraform's backend can store the state as well as execute operations, it is referred to as an enhanced
backend, otherwise, it is referred to as standard
(can only store the state). Backends are listed below:
local
(default - enhanced): This instructs the CLI that the state is stored on the local file system and that it has to run the operations.remote
(1) (enhanced): The most common remote backends are Terraform Cloud and Terraform Enterprise. They can be configured to store state and execute operations or to store state only (in which case the CLI executes the operations). When they execute the operations, the output is streamed back to the CLI.S3
(1) (standard)Consul
(1) (standard)- ... (full list of standard backends available at https://www.terraform.io/docs/language/settings/backends/index.html)
(1) As you can see, this is a bit confusing as
S3
andConsul
store the state remotely, and have nothing to do with the actualremote
backend.
- Providers list: https://registry.terraform.io/namespaces/hashicorp
- AWS config: https://registry.terraform.io/providers/hashicorp/aws/latest/docs
The workspace concept is used to manage multiple environments (e.g., dev, uat, prod). Unfortunately, this concept is implemented differently whether you're using it in the context of Terraform CLI or Terraform Cloud. This makes upgrading from using the CLI to using Terraform Cloud very confusing.
YOU MUST REMEMBER THE FOLLOWING:
- Do not rely on the
terraform.workspace
in your logic if you plan to migrate to Terraform Cloud as this value is set todefault
regardless of the Terraform Cloud workspace. - Your branching strategy will most likely change depending on whether you're using Terraform CLI or Terraform Cloud. When managing a project via the Terraform CLI, you can have a single branch (e.g.,
master
) with multiple workspaces (e.g.,dev
andprod
), whereas with Terraform Cloud each workspace will most likely be assoaciated with a different branch. Let's consider adev
andprod
environment. With the CLI, you would typically maintain a single Git master branch with two different workspaces calleddev
andprod
. You would deploy to dev by switching to thedev
workspace, and do something similar to deploy to prod. However, with Terraform Cloud, you would instead maintain two Git branches (e.g.,master
for prod anddev
for dev). You would then connect themaster
anddev
branch to Terraform Cloud workspaces respectively calledmy-project-prod
andmy-project-dev
.
What is the difference between the Terraform CLI workspaces and the Terraform Cloud workspaces?
Terraform Cloud organizes each stack in workspaces that are not to be confused with the CLI workspaces. To clarify this idiotic conundrum, let's assume you started with a local project using your CLI using its default backend (i.e., local
). Let's call this project my-website
(this is important as this project name concept does not explicitely exist when you're coding locally, but it somewhat will when we're migrating to Terraform Cloud). Let's also imagine that you've configured two workpaces called dev
and prod
, as explained in the Workspaces section. With that setup, your project looks like a single directory whose state can be switched between dev and prod via the terraform workspace select <ENV>
command. Once migrated to Terraform Cloud, this setup will look like 2 separate directories called my-website-dev
and my-website-prod
. As far as Terraform Cloud is concerned, these are two separate projects (i.e., Terraform Cloud workspaces), whereas Terraform CLI sees a single project with 2 different workspaces (i.e., Terraform CLI workspaces).
WARNING!!!:
terraform.workspace
is only usefull if you plan to manage your project locally via the CLI. IT WILL NOT WORK AS EXPECTED WITH TERRAFORM CLOUD. Indeed, with the in Terraform Cloud,terraform.workspace
will always returndefault
😠👿🤯.
Command | Description |
---|---|
terraform init |
Creates an empty .tfstate file ready to be used. Must be executed when a new project is created or when a new backend is configured. |
terraform plan |
Preview the list of changes that could be applied. |
terraform apply |
Applies the new configuration. |
terraform apply -auto-approve |
Applies the new configuration with a confirmation prompt. |
terraform apply -target=aws_security_group.my_sg |
Applies a single resource or module. For more details about this command, please refer to the How to deploy a specific resources instead of the entire project stack? section. |
terraform refresh |
Sycn the terraform.tfstate with the real target infrastructure. For more details, please refer to the Dealing with drift section. |
terraform import aws_security_group.rogue_sg sg-123456 |
Import a non-tracked resource in Terraform. For more details, please refer to the Dealing with drift section. |
terraform state list |
Lists all the resources in the current workspace. |
terraform state rm <RESOURCE NAME> |
Removes a resource name from the state. You can list them with terraform state list . |
terraform workspace list |
Lists all the workspaces. |
terraform workspace new prod |
Creates a new workspace. |
terraform workspace select dev |
Selects a workspace. |
terraform workspace show |
Shows the current workspace. |
terraform workspace delete prod |
Deletes a workspace. |
terraform state push terraform.tfstate |
Migrate the terraform.tfstate into the current selected workspace. |
Official doc at https://www.terraform.io/docs/language/functions/index.html
Loads a local file as a string.
resource "aws_s3_bucket" "website_bucket" {
bucket = "cloudlesslabs.com"
acl = "public-read"
policy = file("public_website_policy.json")
website {
index_document = "index.html"
error_document = "error.html"
}
versioning {
enabled = true
}
}
Can be mixed with the jsondecode
to convert a JSON file:
locals {
testJson = jsondecode(file("public_website_policy.json"))
}
output "test" {
value = local.testJson.Version
}
Drift is what happens when your real infrastructure is in a different state than your terraform.tfstate. This happens quite often for a various reasons. Terraform offers a few techniques to deal and reason about your infrastructure drift:
- The
refresh
command: This command helps adjusting resources that were initially provisioned by Terraform, but were modified outside of it. - The
import
command: This command helps importing into Terraform a resource that was provisoned outside of it.
terraform refresh
is actually run before anyterraform plan
orterraform apply
.
This command helps adjusting resources that were initially provisioned by Terraform, but were modified outside of it. refresh
does not change your infrastructure, so it is perfectly fine to execute it. It may update the terraform.tfstate
though. Indeed, this command uses that file to query its real resources and check for discrepancies. If there are any, Terraform updates the terraform.tfstate
to match the real state. To avoid data loss, a terraform.tfstate.backup
is created. That file is a copy of the terraform.tfstate
before any changes are applied to it. This file is also very useful to discover what changed:
git diff --no-index terraform.tfstate terraform.tfstate.backup
The
--no-index
option is required because both the terraform.tfstate and the terraform.tfstate.backup are not supposed to be tracked by Git. Please refer to the Put your project under source control and ignore critical files section for more details about this topic.
This command helps importing into Terraform a resource that was provisoned outside of it. Imagine the case where a new security group was manually created in the AWS console.
- Create that security group in your Terraform project:
resource "aws_security_group" "rogue_sg" {
...
}
- Get the new SG ID from the AWS Console.
- Import the new SG in Terraform using the SG ID (e.g., 'sg-123456'):
terraform import aws_security_group.rogue_sg sg-123456
Terraform Cloud is the most common remote
backend categorized as an enhanced backend (as opposed to standard), which means it can store a Terraform state as well as run operations (the other enhanced backend is the default local
). The problem that Terraform Cloud tackles is collaboration and governance.
The issue with Terraform Cloud is its workspace implementation, which differs from the normal Terraform CLI workspaces. For more details about this topic, please refer to the The Workspace conundrum section.
IMPORTANT PREREQUISITE: Replace your
terraform.workspace
with a variable that explicitely reference your workspace (e.g.,var.env
). Indeed, in Terraform Cloud,terraform.workspace
is always set todefault
.
- Create an account or login at https://www.terraform.io/cloud.
- Create or select an organization.
- Connect your local CLI to your Terraform Cloud account (1):
- Execute this command:
terraform login
- Copy the API token from your browser and paste it in your CLI.
- Update the
terraform
block by addin a newbackend
block as follow (refer to (2) for theprefix
explanation):
terraform {
...
backend "remote" {
hostname = "app.terraform.io"
organization = "my-org"
workspaces {
# name = "my-app" # Use 'name' instead of 'prefix' if your only using the default workspace.
prefix = "my-app-"
}
}
}
- Run the following command to migrate your workspaces to Terraform Cloud (refer to (3) if you wish to not migrate the
default
workspace):
terraform init
- (Optional - Only needed if you were using
.tfvars
files). For each new Terraform Cloud Workspace, manually create all the variables (i.e., the ones you may have stored under files such asdev.tfvars
,prod.tfvars
):- Login to your Terraform Cloud account: https://www.terraform.io/cloud.
- Select the correct organization.
- Select the workspace.
- Click on the
Variables
tab. - Click the
Add variable
button under theTerraform Variables
section.
- For each new Terraform Cloud Workspace, manually create all the environment variables the stack requires (most likely the AWS or GCP credentials (e.g.,
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
)):- Login to your Terraform Cloud account.
- Select the correct organization.
- Select the workspace.
- Click on the
Variables
tab. - Click the
Add variable
button under theEnvironment Variables
section (IMPORTANT: For each sensitive variable, don't forget to tick thesensitive
tick box to ensure no sensitive information can leak).
- Test with
terraform plan
(4)
Now that all the states and variables have been migrated to Terrafform Cloud, the following local files and folders can be deleted:
terraform.tfstate
file (and its backups)terraform.tfstate.d
folder*.tfvars
(1) Those credentials will be stored under /Users//.terraform.d/credentials.tfrc.json. (2)
prefix
is used when your local project is configured with multiple CLI workspaces. Those workspaces will be exported to Terraform in their own Terraform Cloud workspace. For example, if the project defines two workspaces calleddev
andprod
, then the configuration above will create two Terraform Cloud workspaces called respectivelymy-app-dev
andmy-app-prod
. (3) If you've stopped using thedefault
workspace, and therefore wish to not migrate it to Terraform Cloud, then delete theterraform.tfstate
in the root. (4) There is no more needs to pass the-var-file="some.tfvars"
option (this would fail) as those variables should have been migrated in step #6.
This is done the same way as if the workspace was still maintained via the CLI:
terraform workspace select prod
It makes sense and at the same time it doesn't. As explained earlier, workspaces mean something different whether you are using the CLI or Terraform Cloud. So how could this work? It works because the backend
block has been configured with the prefix
property instead of name
in step #4 above. That's how the Terraform CLI knows how to map the CLI workspace with the Terraform Cloud workspace.
- Select a workspace:
terrafrom workspace select dev
- Run the usual
plan
orapply
:
terraform apply
As mentionned earlier, the Git workflows and dev lifecycles depend on what backend you're using. This section details the main two options:
- Create a new project.
- Put it under source-control and add a
.gitignore
similar to:
.terraform/
terraform.tfstate
terraform.tfstate.backup
terraform.tfstate.*.backup
Add
*.tfvars
if those files contain sensitive pieces of information.
- Create one workspace per environment:
terraform workspace new dev
- Login to your Terraform Cloud account: https://www.terraform.io/cloud
- Select or create an organization (e.g.,
my-org
). - Create a new project in your local machine, and set it up with the following backend:
terraform {
...
backend "remote" {
hostname = "app.terraform.io"
organization = "my-org"
workspaces {
prefix = "my-app-"
}
}
}
IMHO,
prefix
is better thanname
because you will most likely need workspaces to support multiple environments.
- Put it under source-control and add a
.gitignore
similar to:
.terraform/
terraform.tfstate
terraform.tfstate.backup
terraform.tfstate.*.backup
- Commit your project and push it to your VCS (e.g., github.com).
- Log back in to Terraform Cloud account: https://www.terraform.io/cloud and select your organization (e.g.,
my-org
). - Click on the
New workspace
button and select theVersion control workflow
option. - Connect to your VCS and select the repository you created in step #5.
- Name your workspace using the prefix used in step #3 and the environment name you wish to associated to your branch (e.g.,
my-app-prod
is prod is associated with master). - Click on the
Create workspace
button. - Optionally, configure that workspace to automatically apply changes after a new commit (or PR merged into that branch).
- Select your new workspace.
- In the right menu, click on the
Auto apply
to switch it fromOff
toOn
.
With this setup, each time you deploy to that branch, Terraform Cloud will try to apply your changes.
CDK is an attempt to compete with Pulumi and replace the native HCL, aka HashiCorp Configuration Language, with a programmatic language. As of June 2021, The Terraform's CDK only support Typeform and Python.
This happens when doing something similar to this:
output "dns_challenge" {
value = local.is_prod ? aws_acm_certificate.cert[0].domain_validation_options : "No DNS challenges in non-prod environment."
}
The ternary operator is not allowed to return different types.
The hack is to parse the returned values to the same type:
output "dns_challenge" {
value = local.is_prod ? jsonencode(aws_acm_certificate.cert[0].domain_validation_options) : jsonencode({})
}
or use null
output "dns_challenge" {
value = local.is_prod ? aws_acm_certificate.cert[0].domain_validation_options : null
}
export TF_LOG=trace && terraform apply
TF_LOG
values can be:
TRACE
: This is the most verbose. IMHO, that's the one you should be using when things go wrong.DEBUG
: This usually flags the unexpected errors.INFO
: This is basic info.WARN
: This warns about configurations or states that are not severe but could be harmful.ERROR
: This flags explicit errors in your configuration or system.
To disable, the debug mode, simply clear the TF_LOG
variable:
unset TF_LOG
Simply remove it from the .tf
file, then run terraform apply
.
Please refer to the Restricting a provider to a specific account ID section.
locals {
testJson = jsondecode(file("public_website_policy.json"))
}
output "test" {
value = local.testJson.Version
}
terraform state show aws_s3_bucket.www | grep website_endpoint
To deploy a single resource:
terraform apply -target=aws_security_group.my_sg
You can also specify more than one resource:
terraform apply -target=aws_security_group.my_sg -target=aws_security_group.my_2nd_sg
If deploying many SGs is too tedious, you can choose between:
- Deploying a resource that depends on those SGs, as the
-target
option respects dependencies:
terraform apply -target=aws_instance.my_server
- Encapsulate the SGs in a module, and deploy that module only, as the
-target
also supports modules:
terraform apply -target=module.my_security_groups
Please refer to the Migrating the default workspace into another one section.
terraform state list | xargs -L 1 terraform state rm
On Mac, they are stored under /Users/<YOUR USER>/.terraform.d/credentials.tfrc.json
.
Region Name | Region |
---|---|
US East (Ohio) | us-east-2 |
US East (N. Virginia) | us-east-1 |
US West (N. California) | us-west-1 |
US West (Oregon) | us-west-2 |
Africa (Cape Town) | af-south-1 |
Asia Pacific (Hong Kong) | ap-east-1 |
Asia Pacific (Mumbai) | ap-south-1 |
Asia Pacific (Osaka-Local) | ap-northeast-3 |
Asia Pacific (Seoul) | ap-northeast-2 |
Asia Pacific (Singapore) | ap-southeast-1 |
Asia Pacific (Sydney) | ap-southeast-2 |
Asia Pacific (Tokyo) | ap-northeast-1 |
Canada (Central) | ca-central-1 |
China (Beijing) | cn-north-1 |
China (Ningxia) | cn-northwest-1 |
EU (Frankfurt) | eu-central-1 |
EU (Ireland) | eu-west-1 |
EU (London) | eu-west-2 |
EU (Milan) | eu-south-1 |
EU (Paris) | eu-west-3 |
EU (Stockholm) | eu-north-1 |
Middle East (Bahrain) | me-south-1 |
South America (Sao Paulo) | sa-east-1 |
AWS GovCloud (US-East) | us-gov-east-1 |
AWS GovCloud (US-West) | us-gov-west-1 |
Region Name | Region | Availability zones |
---|---|---|
asia-east1 | Taiwan | a,b,c |
asia-east2 | Hong Kong | a,b,c |
asia-northeast1 | Tokyo | a,b,c |
asia-northeast2 | Osaka | a,b,c |
asia-northeast3 | Seoul | a,b,c |
asia-south1 | Mumbai | a,b,c |
asia-south2 | Delhi | a,b,c |
asia-southeast1 | Singapore | a,b,c |
asia-southeast2 | Jakarta | a,b,c |
australia-southeast1 | Sydney | a,b,c |
australia-southeast2 | Melbourne | a,b,c |
europe-central2 | Poland | a,b,c |
europe-north1 | Hamina, Finland, Europe | a,b,c |
europe-west1 | St. Ghislain, Belgium, Europe | a,b,c |
europe-west2 | London, England, Europe | a,b,c |
europe-west3 | Frankfurt, Germany Europe | a,b,c |
europe-west4 | Eemshaven, Netherlands, Europe | a,b,c |
europe-west6 | Zurich, Switzerland, Europe | a,b,c |
northamerica-northeast1 | Montréal, Québec, North America | a,b,c |
southamerica-east1 | Osasco, São Paulo, Brazil, South America | a,b,c |
us-central1 | Council Bluffs, Iowa, North America | a,b,c,f |
us-east1 | Moncks Corner, South Carolina, North America | b,c,d |
us-east4 | Ashburn, Virginia, North America | a,b,c |
us-west1 | The Dalles, Oregon, North America | a,b,c |
us-west2 | Los Angeles, California, North America | a,b,c |
us-west3 | Salt Lake City, Utah, North America | a,b,c |
us-west4 | Las Vegas, Nevada, North America | a,b,c |