Terraform Notes
Published: 2023-03-11 08:09:51


Install the binary on Windows and set Path
https://developer.hashicorp.com/terraform/downloads

Find the example codes for provider (e.g. AWS)
https://registry.terraform.io/providers/hashicorp/aws/latest/docs

Steps:

`init`
will parse the script and download the necessary plugins
When Terraform initializes a new Terraform directory, it creates a lock file named _.terraform.lock.hcl_ and the _.terraform _directory.

`plan`
Will have a preview of what resources to be created (by comparing configuartion and local state file)

`apply`
makes the changes defined by your Terraform configuration to create, update, or destroy resources.

Example codes:
_first_ec2.tf_
```
provider "aws" {
  region     = "us-west-2"
  access_key = "PUT-YOUR-ACCESS-KEY-HERE"
  secret_key = "PUT-YOUR-SECRET-KEY-HERE"
}

resource "aws_instance" "myec2" {
   ami = "ami-082b5a644766e0e6f"
   instance_type = "t2.micro"
}
```
Commands:

```
terraform init
terraform plan
terraform apply
```


**Quick steps for configuring provider AWS for Terraform**

- Create a free-tier account in AWS
- Log in using root user 
Go to IAM > create a user > attach policy as Administrator
Use following instructions to create access key and secert pair
https://docs.aws.amazon.com/powershell/latest/userguide/pstools-appendix-sign-up.html

- Then add this element in your main.tf

```
provider "aws" {
  region     = "us-west-2"
  access_key = "my-access-key"
  secret_key = "my-secret-key"
}
```
https://docs.aws.amazon.com/powershell/latest/userguide/pstools-appendix-sign-up.html

============
**BASIC**

**Variable**
Allowing you to specify value other than hardcoded in the main.tf. E.g. `terraform apply -var "instance_name=YetAnotherName"`

```
 resource "aws_instance" "app_server" {
   ami           = "ami-08d70e59c07c61a3a"
   instance_type = "t2.micro"

   tags = {
-    Name = "ExampleAppServerInstance"
+    Name = var.instance_name
   }
 }
```
https://developer.hashicorp.com/terraform/tutorials/aws-get-started/aws-variables


Outputs

```
output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.app_server.id
}

output "instance_public_ip" {
  description = "Public IP address of the EC2 instance"
  value       = aws_instance.app_server.public_ip
}
```
Then you can see in the log after running `apply`

```
Changes to Outputs:
  + instance_id        = "i-0bf954919ed765de1"
  + instance_public_ip = "54.186.202.254"
```
https://developer.hashicorp.com/terraform/tutorials/aws-get-started/aws-outputs

---------------

**Version of project and Terraform**
https://developer.hashicorp.com/terraform/tutorials/configuration-language/versions#inspect-the-terraform-state-file
The state file is generated/updated after running a plan/apply. It is a snapshot of the actual remote cloud resource.
  "version": 4,
  "serial": 8,
  "terraform_version": "0.15.0",

Note: when you update your project's **_main.tf_** and execute `terraform apply`, the serial number will bump up by 1 or 2.

The **_terraform.tf_** file (similar to **_Package.json_**) contains the required version constrains of Terraform Core and plug-ins (=providers). There is no convention that **_terraform.tf_** must be the one. Sometimes you will see in the tutorial that **versions.tf_** is used. So treat all *.tf as *.js in a node.js project.

This configuration sets required_version to ~> 0.12.29. The ~> symbol allows the patch version to be greater than 29 but requires the major and minor versions (0.12) to match the version that the configuration specifies. 

**Versions of providers in lock files**
- constraints
- side effects of version upgrade

**_terraform.lock.hcl_** (HashiCorp configuration language) is a middle product of terraform.lock.hcl is a middle product of init. It is similar to **_Package.json.lock_**. It is similar to Package-lock.json in npm.

Terraform automatically creates or updates the dependency lock file each time you run the terraform init command.

_Re-initialize your configuration with the -upgrade flag. This tells Terraform to download the new version of the provider, and update the version and signature in the lock file._

https://developer.hashicorp.com/terraform/tutorials/configuration-language/provider-versioning

-------------------

**Providers**
https://registry.terraform.io/browse/providers

------------------

**Define Infrastructure with Terraform Resources**
Full run of a typical EC2 with security group + user data script to initiate a web application:
https://developer.hashicorp.com/terraform/tutorials/configuration-language/resource 

2 key learnings from the above are:

- Know how to use docs of the plugin

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfh639b9u2cx6h0o3kqu.png)


- Output information of each 'apply'

```
output "domain-name" {
  value = aws_instance.web.public_dns
}

output "application-url" {
  value = "${aws_instance.web.public_dns}/index.php"
}

```

------------

The previous 2 tutorials and this following one are basically talking about the same:
https://developer.hashicorp.com/terraform/tutorials/cli/init

The above one is more exploring the process of how Terraform gets the providers and modules, into local file system.



The main.tf contains reference to 1 local module and 1 remote module. Module is more like function for quick implementation to do some task, relying on other providers. 

- **Providers**, which are plugins for Terraform that extend it with support for interacting with various external systems.
- **Modules**, which allow splitting out groups of Terraform configuration constructs (written in the Terraform language) into reusable abstractions.

```
module "nginx-pet" {
  source = "./nginx"

  container_name = "hello-${random_pet.dog.id}"
  nginx_port = 8001
}

module "hello" {
  source  = "joatmon08/hello/random"
  version = "3.0.1"

  hello = random_pet.dog.id

	secret_key = "secret"
}
```
-----------
**Terraform Cloud (doing execution and storing states in remote)**
After registration, use token (if you configure remote in main.tf you will be asked to use login with token) and creating an organisation:

All the `apply` will  be executed remotely (as github workflow)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sjvy4bd9gefc1jvxudlt.png)
https://developer.hashicorp.com/terraform/tutorials/aws-get-started/aws-remote


**Import**
Command import is to syncrhonise from an existing infrastructure into terraform state. The aim is to bring that infrastructure under terraform's control.
https://developer.hashicorp.com/terraform/tutorials/state/state-import

Note: The tutorial uses a hardcoded host value docker.sock in docker provider.
The solution is here: you will need to find out what a working docker.sock is in your own machine, and replace the docker.sock in docker provider.

----------------------------
**Commands for state management**

terraform.tfstate contains the resource state of last sync. If you run `terraform plan`, Terraform will calculate the difference of configuration and state, to figure out what to `apply`.

The relationship of 3 entities is:
Configuration (via `apply`)-> Physical resource (after `apply` or via `refresh`)-> Local state
(tutorial: https://developer.hashicorp.com/terraform/tutorials/state/state-cli)

**_replace_**
Terraform usually only updates your infrastructure if it does not match your configuration. You can use the -replace flag for terraform plan and terraform apply operations to safely recreate resources in your environment even if you have not edited the configuration, which can be useful in cases of system malfunction.

`terraform plan -replace="aws_instance.example"`

**_show_**
Get a human-friendly output of the resources contained in your state.

`terraform show`

**_state list_**
Run `terraform state list` to get the list of resource names and local identifiers in your state file. This command is useful for more complex configurations where you need to find a specific resource without parsing state with `terraform show`.

```
 terraform state list
data.aws_ami.ubuntu
aws_instance.example
aws_security_group.sg_8080
```

**_state mv_**
The `terraform state mv` command moves resources from one state file to another. You can also rename resources with `mv`. Moving resources is useful when you want to combine modules or resources from other states, but do not want to destroy and recreate the infrastructure.

Note: The move command will update the resource in state, but not in your configuration file. You have to add the corresponding resource in the configuration as well, sperately.

`terraform state mv -state-out=../terraform.tfstate aws_instance.example_new aws_instance.example_new`

**_state rm_**
The terraform state rm subcommand removes specific resources from your state file. This does not remove the resource from your configuration or destroy the infrastructure itself.

`terraform state rm aws_security_group.sg_8080`

**_terraform refresh_**
Updates the state file when physical resources change outside of the Terraform workflow.
The _-refresh-only_ flag was introduced in Terraform 0.15.4, and is preferred over the _terraform refres_h subcommand.
In previous versions of Terraform, the only way to refresh your state file was by using the terraform refresh subcommand. However, this was less safe than the -refresh-only plan and apply mode since it would automatically overwrite your state file without giving you the option to review the modifications first. In this case, that would mean automatically dropping all of your resources from your state file.

Use AWS cli to modify the physcial infrastructure outside terraform flow. E.g. delete an EC2 instance
` aws ec2 terminate-instances --instance-ids $(terraform output -raw instance_id)`

And then use `terraform refresh` to sync from physcial infrastructure into local state file

And then use `terraform plan` to check the difference between **configuration** and the **state**

**_terraform destroy_**

Note:
The unsync situation is defined as Resource Drift. The same command set (refresh and import) are covered in this tutorial:
https://developer.hashicorp.com/terraform/tutorials/state/resource-drift

----------
Trouble-shoot (logging, support ticket)

To switch on logging:
https://www.phillipsj.net/posts/how-to-configure-logging-for-terraform/

PowerShell
```
> $env:TF_LOG="TRACE"
> $env:TF_LOG_PATH="terraform.txt"
```
Bash
```
$ export TF_LOG="TRACE"
$ export TF_LOG_PATH="terraform.txt"
```
Tutorial: https://developer.hashicorp.com/terraform/tutorials/configuration-language/troubleshooting-workflow#enable-terraform-logging

---------------------------

**Modules**

Tutorial:
https://developer.hashicorp.com/terraform/tutorials/modules/module-create
This tutorial contains a **consuming project** and a **local module** within it. The module encapsulates the actions to create a website hosting-pueposed s3 bucket which allows public access. The project uses the module by passing some inputs such as bucket name as the website name.

In summary, for a module:
- Create scaffolder contains tf files and LICENSE + README.md
- Define proper input vars and outputs
- A consumer terraform to use the module to do stuff

The commands used in this tutorial:

`terraform get`
It is a light command to install modules.
In this case, Terraform will install the required 2 modules from registry, and install 1 local module by directly referring to its folder within your project.
```
Downloading registry.terraform.io/terraform-aws-modules/ec2-instance/aws 4.3.0 for ec2_instances...
- ec2_instances in .terraform\modules\ec2_instances
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 3.18.1 for vpc...
- vpc in .terraform\modules\vpc
- website_s3_bucket in modules\aws-s3-static-website-bucket
```

`terraform output`
The terraform output command is used to extract the value of an output variable from the state file.
https://developer.hashicorp.com/terraform/cli/commands/output
In this case we run `terraform output` to get the generated bucket website domain name so the developer knows easily what url to test with . E.g. _https://<YOUR BUCKET NAME>.s3-us-west-2.amazonaws.com/index.html_

**_Variables and Outputs_**
Tutorial
https://developer.hashicorp.com/terraform/tutorials/modules/module-use
The principal is to:

(Inputs)
Define variables in a standalone file variables.tf. E.g.
```
variable "vpc_name" {
  description = "Name of VPC"
  type        = string
  default     = "example-vpc"
}
```
Refer to the variables and assign them to the inputs of modules within main.tf:

```
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  ...
  name = var.vpc_name
```

(Outputs)
Refer to the outputs of modules as outputs within main.tf:

```
output "ec2_instance_public_ips" {
  description = "Public IP addresses of EC2 instances"
  value       = module.ec2_instances[*].public_ip
}
```

----------------------
**Collection Operation**

Tutorial:
https://developer.hashicorp.com/terraform/tutorials/configuration-language/troubleshooting-workflow#correct-a-for_each-error

In this case, the configuration defines a map collection of _aws_instance_:

```
resource "aws_instance" "web_app" {
   for_each               = local.security_groups
   ami                    = data.aws_ami.ubuntu.id
   instance_type          = "t2.micro"
   vpc_security_group_ids = [each.value]
   tags = {
    Name = "${var.name}-learn-${each.key}"
   }
 }
}
```
And the outputs refer to the collection of _aws_instance_

```
 output "instance_id" {
   description = "ID of the EC2 instance"
    value       = [for instance in aws_instance.web_app: instance.id]
 }

 output "instance_public_ip" {
   description = "Public IP address of the EC2 instance"
    value       = [for instance in aws_instance.web_app: instance.public_ip]
 }

output "instance_name" {
   description = "Tags of the EC2 instance"
   value        = [for instance in aws_instance.web_app: instance.tags.Name]
}
```

The outputs print:

```
Outputs:

instance_id = [
  "i-0a679d5acef2b8e01",
  "i-02bd8b86bfa93ae96",
]
instance_name = [
  "terraform-learn-sg_8080",
  "terraform-learn-sg_ping",
]
instance_public_ip = [
  "13.58.116.31",
  "3.131.82.166",
]
```