[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** ![](https://hackmd.io/_uploads/r1j7jB4o2.png) You should now be able to see the user data of the instance: ![](https://hackmd.io/_uploads/ByENiSVj2.png) :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/ :::