[toc]
### Terraform HCL
#### Write a tf file to create an EC2 instance in us-east-1 region
````yaml=
## Create an EC2 instance in us-east-1
provider "aws" {
access_key = "<Your_Access_Key>"
secret_key = "<Your_Secret_Key>"
token = "<Your_Security_Token>"
region = "us-east-1"
}
resource "aws_instance" "terraform_demo" {
ami = "ami-02f3f602d23f1659d"
instance_type = "t2.micro"
}
````
#### Add the "Name" tag to your EC2 instance
````yaml=
## Add a user defined tag to your instance
provider "aws" {
access_key = "<Your_Access_Key>"
secret_key = "<Your_Secret_Key>"
token = "<Your_Security_Token>"
region = "us-east-1"
}
resource "aws_instance" "terraform_demo" {
ami = "ami-02f3f602d23f1659d"
instance_type = "t2.micro"
tags = {
Name = "terraform-node"
}
}
Apply changes:
terraform plan
terraform apply
or
terraform apply --auto-approve
Validate changes:
Go to AWS console --> EC2 Dashboard --> instances
Your instance name should be changed to "terraform-node"
````
#### Attach an existing security group to the instance
````yaml=
provider "aws" {
access_key = "your accesskey"
secret_key = "yoursecretkey"
token = "yourtoken"
region = "us-east-1"
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = ["sg-0d3359a397ff3a1f1"]
tags = {
Name = "ec2-sg-web"
env = "dev"
}
}
````
#### Providing User data for EC2 instances [inline]
````yaml=
provider "aws" {
access_key = "your accesskey"
secret_key = "yoursecretkey"
token = "yourtoken"
region = "us-east-1"
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = ["sg-0d3359a397ff3a1f1"]
tags = {
Name = "ec2-userdata-inline"
env = "dev"
}
user_data = <<EOF
#!/bin/bash
yum update -y
yum install -y httpd.x86_64
systemctl start httpd.service
systemctl enable httpd.service
echo “Welcome to Simplilearn CMAT from $(hostname -f)” > /var/www/html/index.html
EOF
}
````
#### Providing User data for EC2 instances [via script file]
````yaml=
provider "aws" {
access_key = "your accesskey"
secret_key = "yoursecretkey"
token = "yourtoken"
region = "us-east-1"
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = ["sg-0d3359a397ff3a1f1"]
tags = {
Name = "ec2-userdata-script"
env = "dev"
}
user_data = "${file("myscript.sh")}"
}
````
:arrow_right: *For above userdata Make sure that you have myscript.sh file created in the same Terraform directory*
##### Sample user data script
```sh=
## vi mycript.sh
#!/bin/bash
yum update -y
yum install -y httpd.x86_64
systemctl start httpd.service
systemctl enable httpd.service
echo “Welcome to Simplilearn CMAT from $(hostname -f)” > /var/www/html/index.html
```
##### Validate user data
:::success
**You can check the user data of the instance as follows:**
Select the instance --> Click on **Actions** --> go to **instance settings** --> Click on **Edit user data**

You should now be able to see the user data of the instance:

:arrow_right: *If you want to edit the user data then you have to stop the instance first*
:::
#### Create an EC2 instance along with a new Security Group (Allow incoming ports 80/443/22 and all outgoing)
```hcl=
## Create an EC2 instance along with a new security group (80/443/22)
provider "aws" {
access_key = "your accesskey"
secret_key = "yoursecretkey"
token = "yourtoken"
region = "us-east-1"
}
resource "aws_security_group" "web-sg" {
name = "web-sg-tf"
description = "Allow HTTP, HTTPS and SSH traffic via Terraform"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web-sg.id]
tags = {
Name = "ec2-with-sg"
env = "dev"
}
user_data = "${file("myscript.sh")}"
}
```
##### Sample userdata script
```hcl=
## vi myscript.sh
#!/bin/bash
yum update -y
yum install -y httpd.x86_64
systemctl start httpd.service
systemctl enable httpd.service
echo “Welcome to Simplilearn CMAT from $(hostname -f)” > /var/www/html/index.html
```
##### Once done this is how your directory should look like:
```hcl=
sk@ec2-with-sg:$ tree
.
├── main.tf
├── myscript.sh
```
:::warning
**Apply changes**
```hcl=
terraform plan
terraform apply
or
terraform apply --auto-approve
```
**Validate changes**
- Go to AWS console --> EC2 Dashboard --> instances.
- Capture the public IP address of the newly created instance and access it on port 80 via a web browser.
:::
#### Create similar multiple resources using "count" function
```hcl=
## Create an EC2 instance along with a new security group (80/443/22)
provider "aws" {
access_key = "your accesskey"
secret_key = "yoursecretkey"
token = "yourtoken"
region = "us-east-1"
}
resource "aws_security_group" "web-sg" {
name = "web-sg-tf"
description = "Allow HTTP, HTTPS and SSH traffic via Terraform"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web-sg.id]
count = 3
tags = {
Name = "ec2-count-${count.index}"
env = "dev"
}
user_data = "${file("myscript.sh")}"
}
output "instance-public-ip_1" {
value = aws_instance.sl-tf-web[0].public_ip
}
output "instance-public-ip_2" {
value = aws_instance.sl-tf-web[1].public_ip
}
output "instance-public-ip_3" {
value = aws_instance.sl-tf-web[2].public_ip
}
output "instance-public-ip_4" {
value = aws_instance.sl-tf-web[3].public_ip
}
```
:arrow_right: *Apply usual terraform workflow (init/plan/apply) and validate the changes*
#### Create a proper folder structure by segragating the files:
##### provider.tf
```hcl=
provider "aws" {
access_key = "your accesskey"
secret_key = "yoursecretkey"
token = "yourtoken"
region = "us-east-1"
}
```
##### resources.tf
```hcl=
resource "aws_security_group" "web-sg" {
name = "web-sg-tf"
description = "Allow HTTP, HTTPS and SSH traffic via Terraform"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web-sg.id]
tags = {
Name = "mywebnode1"
env = "dev"
}
user_data = "${file("myscript.sh")}"
}
```
##### output.tf
```hcl=
output "instance-public-ip_4" {
value = aws_instance.sl-tf-web.public_ip
}
```
Your directory should look like the following now:
```hcl=
sk@segretate:$ tree
.
├── myscript.sh
├── output.tf
├── provider.tf
├── resources.tf
```
:arrow_right: *Apply usual terraform workflow (init/plan/apply) and validate the changes*
#### Alternate provider definition using aws-cli
##### Steps to perform
:::warning
- Create a new IAM user
- Create new security credentials (access key and secret access key) for this newly created IAM user
- Copy the credentials in a text editor
- Install AWSCLI
- Configure AWSCLI with the security credentials of the user you created in earlier steps
:::
:arrow_right: ***Please make sure not to use credentials from Simplilearn LMS***
##### Install AWSCLI
:::warning
Use the following link to install the cli on your system depending in which OS you are on:
https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
:::
##### Configure AWSCLI
```hcl=
$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-2
Default output format [None]: json
```
:arrow_right: *Please note that you need to have your access key and secret access key handy which you acquired in [this](https://hackmd.io/@skm23/cmat/https%3A%2F%2Fhackmd.io%2F34qN0C8nQVeW4djwHtPdDA%3Fview#How-to-get-access_key-and-secret_access_key-in-your-own-AWS-accounts) step.*
##### Update your provider definition to use local awscli profile
```hcl=
provider "aws" {
profile = "sladmin"
region = "us-east-1"
}
```
##### List existing AWSCLI profiles on current system:
```hcl=
$ aws configure list-profiles ## Works on MacOS and Windows
default
sladmin
sldev
For Linux, if above does not work, use the following:
$ aws configure list
```
### Variables in Terraform
#### Working with a Single variable
##### Define varibles in varibles.tf
```hcl=
variable "instance_name" {
description = "Instance name to be displayed on aws console"
type = string
default = "nodeviatf"
}
```
##### Call the variables in terraform block
```hcl=
resource "aws_security_group" "web-sg" {
name = "web-sg-tf"
description = "Allow HTTP, HTTPS and SSH traffic via Terraform"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web-sg.id]
tags = {
Name = var.instance_name
env = "dev"
}
}
```
:arrow_right: Apply the changes and verify in AWS Console
##### Override the default variables on the command line:
```yaml=
Syntax:
terraform apply -var "variable_name=variable=value"
Example:
terraform apply -var "instance_name=taggedoncli"
```
#### Terraform tfvars file
##### Create a tfvars file and initialize the variables
```hcl=
## prodvars.tfvars
instance_type = "t2.micro"
instance_name = "prodinstance"
```
```hcl=
## devvars.tfvars
instance_type = "t2.micro"
instance_name = "devinstance"
```
##### Run the following command to initialize the variables from tfvars file
```yaml=
terraform apply -var-file="prodvars.tfvars"
```
#### auto.tfvars files
:::info
If we use extension ".auto.tfvars" for the variables files, then they would be loaded automatically and -var-file flag on the command line would not be needed.
:::
:::warning
Rename one of the "tfvars" files created above to "auto.tfvars". For example:
```hcl=
mv devvars.tfvars devvars.auto.tfvars
```
Once done, run terraform apply again. You should see devvars variables getting initialized automatically without the need for -var-file flag with terraform CLI.
:::
### Terraform States
#### Local states
````yaml=
labsuser@controller:~/terra-docs/ec2-with-sg$ terraform state
Subcommands:
list List resources in the state
mv Move an item in the state
pull Pull current state and output to stdout
push Update remote state from a local state file
replace-provider Replace provider in the state
rm Remove instances from the state
show Show a resource in the state
````
#### Understanding terraform State files (Local)
##### before terraform init
````
labsuser@controller:~/terra-docs/terra-state$ ls -al
total 28
drwxrwxr-x 3 labsuser labsuser 4096 Mar 26 14:17 .
drwxrwxr-x 6 labsuser labsuser 4096 Mar 26 14:02 ..
-rw-rw-r-- 1 labsuser labsuser 708 Mar 26 14:03 main.tf
````
##### terraform init/plan
````
labsuser@controller:~/terra-docs/terra-state$ ls -al
total 28
drwxrwxr-x 3 labsuser labsuser 4096 Mar 26 14:17 .
drwxrwxr-x 6 labsuser labsuser 4096 Mar 26 14:02 ..
drwxr-xr-x 3 labsuser labsuser 4096 Mar 26 14:04 .terraform
-rw-r--r-- 1 labsuser labsuser 1377 Mar 26 14:04 .terraform.lock.hcl
-rw-rw-r-- 1 labsuser labsuser 708 Mar 26 14:03 main.tf
````
##### terraform apply (only once)
````
labsuser@controller:~/terra-docs/terra-state$ ls -al
total 28
drwxrwxr-x 3 labsuser labsuser 4096 Mar 26 14:17 .
drwxrwxr-x 6 labsuser labsuser 4096 Mar 26 14:02 ..
drwxr-xr-x 3 labsuser labsuser 4096 Mar 26 14:04 .terraform
-rw-r--r-- 1 labsuser labsuser 1377 Mar 26 14:04 .terraform.lock.hcl
-rw-rw-r-- 1 labsuser labsuser 708 Mar 26 14:03 main.tf
-rw-rw-r-- 1 labsuser labsuser 4437 Mar 26 14:17 terraform.tfstate
````
##### terraform apply (twice)
````
labsuser@controller:~/terra-docs/terra-state$ ls -al
total 36
drwxrwxr-x 3 labsuser labsuser 4096 Mar 26 14:22 .
drwxrwxr-x 6 labsuser labsuser 4096 Mar 26 14:02 ..
drwxr-xr-x 3 labsuser labsuser 4096 Mar 26 14:04 .terraform
-rw-r--r-- 1 labsuser labsuser 1377 Mar 26 14:04 .terraform.lock.hcl
-rw-rw-r-- 1 labsuser labsuser 708 Mar 26 14:20 main.tf
-rw-rw-r-- 1 labsuser labsuser 4439 Mar 26 14:22 terraform.tfstate
-rw-rw-r-- 1 labsuser labsuser 4437 Mar 26 14:22 terraform.tfstate.backup
````
##### Activity - Remove a resource from Terraform state
````yaml=!
Create an instance and security group using previously used tf files.
check terraform state:
terraform state list
Remove Security from terraform state:
terraform state rm <sg-id>
verify terraform state again:
terraform state list
Run terraform destroy:
terraform destroy -auto-approve
Validate:
Check on AWS console. EC2 instance should be terminated already however Security group should still be present and not destroyed as it was no longer managed as part of "terraform state".
````
#### Using Amazon S3 as backend for terraform "remote state"
##### Create a backend definition (backend.tf)
```hcl=
terraform {
backend "s3" {
bucket = "tfstate-sk"
key = "dev/tfstate"
region = "us-east-1"
profile = "sladmin"
}
}
```
:arrow_right: *Profile name should match with what you have configured when configuring AWSCLI. For example if you have not mentioned any profile name then it should be "default". You can check your profile name in "~/.aws/config".*
##### Sample backend file (for simplilearn labs)
````yaml=
terraform {
backend "s3" {
bucket = "<s3-bucket-name>"
key = "<bucket-prefix>"
region = "us-east-1"
access_key = "<Your-access-key>"
secret_key = "<Your-secret-key>"
token = "<Your-temp-token>"
}
}
````
##### provider.tf
```hcl=
provider "aws" {
profile = "sladmin"
region = "us-east-1"
}
```
:arrow_right: *Profile name should match with what you have configured when configuring AWSCLI. For example if you have not mentioned any profile name then it should be "default". You can check your profile name in "~/.aws/config".*
##### resource.tf
```hcl=
resource "aws_instance" "sl-tf-web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t2.micro"
tags = {
Name = "webnode-v1"
env = "dev"
}
}
```
:arrow_right: *Apply usual terraform workflow (init/plan/apply) and validate the changes*
*You Should be able to see a new file with your terraform state in the S3 bucket/prefix you mentioned in the backend definition*
#### Terraform remote state on Terraform Cloud
##### Complete the following activity from Hashicorp official tutorials
:::warning
https://developer.hashicorp.com/terraform/tutorials/aws-get-started/aws-remote
:::
##### Additional practice for terraform cloud
:::warning
https://developer.hashicorp.com/terraform/tutorials/cloud
:::
### Working with Terraform provisioners
#### Working with Local Provisioner (Local-exec)
##### Provider.tf
```hcl=
provider "aws" {
profile = "sladmin"
region = "us-east-1"
}
```
##### resources.tf
```hcl=
resource "aws_instance" "web" {
ami = "ami-0f34c5ae932e6f0e4"
instance_type = "t3.micro"
tags = {
Name = "terraform-node"
env = "dev"
}
provisioner "local-exec" {
command = "touch localexec.txt"
}
provisioner "local-exec" {
command = "echo ${self.private_ip} >> instance_ips.txt"
}
provisioner "local-exec" {
command = "echo ${self.public_ip} >> instance_ips.txt"
}
}
```
##### Folder structure before terraform apply
```yaml=
sk@local-exec:$ tree
.
├── provider.tf
├── resource.tf
```
:arrow_right: *Apply usual terraform workflow (init/plan/apply) and validate the changes*
##### Folder structure after terraform apply
```yaml=
sk@local-exec:$ tree
.
├── instance_ips.txt
├── localexecdemo.txt
├── provider.tf
├── resource.tf
└── terraform.tfstate
```
#### Working with Remote Provisioner (remote-exec)
##### Steps to be performed
:::info
- Create the EC2 instance
- Make sure the Security group has incoming ports (80/443/22) open
- make sure the public key is copied on the node (asymmetric keypair)
- copy a script file from controller to the node (nginx.sh)
- SSH into the instance
- run the nginx.sh script
- validate
- SSH into the instance and check nginx service
- check if nginx is accessiebl via the public-IP in a brower
#### Files needed
- provider.tf
- resources.tf
- nginx.sh
- asymmetric keypair
- outputs (optional)
:::
:::warning
1. Create provider.tf file and add your access-key and secret-access-key for authentication to AWS account
2. Create create-ec2.tf file which will contain the resource definition
3. Create nginx.sh script file which will be copied to newly created ec2 instance and run to install and configure nginx on the newly created EC2 node.
4. Create outputs.tf to get public ip and public DNS of the ec2 instance to access nginx server.
5. Create sg.tf to create a security group which allows ports 22,80 and 443 incoming.
6. Create asymmetric key pair (locally) using ssh-keygen which will be used by Terraform to ssh into the instance.
7. Run terraform apply and Validate the changes
:::
##### Create a project folder and cd into it
````yaml=
mkdir provisioners-tf
cd provisioners-tf
````
##### providers.tf
````yaml=
## vi providers.tf
provider "aws" {
access_key = "<Your_Access_Key>"
secret_key = "<Your_Secret_Key>"
token = "<Your_Security_Token>"
region = "us-east-1"
}
````
##### create-ec2.tf
````yaml=
## vi create-ec2.tf
## Working with "remote-exec" and "file" provisioners
resource "aws_key_pair" "sk-key" {
key_name = "sk-key"
public_key = file("sk-key.pub")
}
resource "aws_instance" "mynginx" {
ami = "ami-02f3f602d23f1659d"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web-sg.id]
key_name = aws_key_pair.sk-key.key_name
tags = {
Name = "tf-nginx"
}
provisioner "file" {
source = "nginx.sh"
destination = "/tmp/nginx.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/nginx.sh",
"sudo /tmp/nginx.sh",
]
}
connection {
host = self.public_ip
type = "ssh"
user = "ec2-user"
private_key = file("sk-key")
}
}
````
:mag: *Make sure to update the correct ***ami-id*** as per the AWS region and your account*
##### sg.tf
```hcl=
resource "aws_security_group" "web-sg" {
name = "web-sg-tf"
description = "Allow HTTP, HTTPS and SSH traffic via Terraform"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 3389
to_port = 3389
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
##### nginx.sh
````yaml=
## vi nginx.sh
#!/bin/bash
# install nginx
sudo yum update
sudo yum install -y nginx
# make sure nginx is started
systemctl start nginx
````
##### outputs.tf
````yaml=
## vi outputs.tf
## Outputs to be displayed after "Terraform run"
output "public_ip" {
value = aws_instance.mynginx.public_ip
}
output "public_dns" {
value = aws_instance.mynginx.public_dns
}
````
##### Create "remote-exec" keypair
````yaml=
root@terraform:~/provisioners-tf# ssh-keygen -t rsa -C a@b
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): sk-key
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in sk-key
Your public key has been saved in sk-key.pub
The key fingerprint is:
SHA256:2EXO2eDb8cWxpzZyCcCHrWW2xn3wS56GKx5xjlh5hxI a@b
The key's randomart image is:
+---[RSA 3072]----+
| .+o . |
| =o=* ..o|
| *E+o ++|
| o ..o*+o=+|
| . S .*o+@oo|
| o B=.* |
| . o .o |
| ... |
| ... |
+----[SHA256]-----+
root@terraform:~/provisioners-tf#
````
##### Final Directory / Module structure
Once all the steps are complete, your directory should look like the following:
````yaml=
root@terraform:~/provisioners-tf# tree
.
├── create-ec2.tf
├── nginx.sh
├── outputs.tf
├── provider.tf
├── sg.tf
├── sk-key
└── sk-key.pub
0 directories, 7 files
root@terraform:~/provisioners-tf#
````
##### Run terraform apply and validate the changes
````yaml=!
Apply Terraform:
terraform apply -auto-approve
Validate:
Grab the Public IP (or Public DNS) of the instance from Terraform outputs and access it in a web browser such as chrome or firefox. You should be able to see the default nginx page.
````
:mag: If nginx is not accessible on the web browser, make sure you are accessing it on http (80) and not on https (443)
### References
:::info
- https://developer.hashicorp.com/terraform/language/meta-arguments/count
- [Variables in Terraform](https://developer.hashicorp.com/terraform/language/values/variables)
- https://developer.hashicorp.com/terraform/tutorials/configuration-language/dependencies
- https://developer.hashicorp.com/terraform/tutorials/certification-associate-tutorials-003
- https://catalog.us-east-1.prod.workshops.aws/workshops/afee4679-89af-408b-8108-44f5b1065cc7/en-US
- https://hashicorp-terraform.awsworkshop.io/
:::