├── .gitignore ├── README.md ├── exercises ├── 01-installing-terraform │ ├── instructions.md │ └── solution │ │ └── solution.md ├── 02-basic-config-file │ ├── instructions.md │ └── solution │ │ ├── main.tf │ │ └── solution.md ├── 03-basic-workflow │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── imgs │ │ └── ec2-instance.png │ │ ├── main.tf │ │ └── solution.md ├── 04-provisioner │ ├── install_apache.sh │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── imgs │ │ └── aws-user-data.png │ │ ├── install_apache.sh │ │ ├── main.tf │ │ └── solution.md ├── 05-input-variables │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── main.tf │ │ ├── solution.md │ │ └── variables.tf ├── 06-local-values │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── locals.tf │ │ ├── main.tf │ │ └── solution.md ├── 07-output-values │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── solution.md ├── 08-data-source │ ├── instructions.md │ └── solution │ │ ├── main.tf │ │ └── solution.md ├── 09-loop │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── main.tf │ │ └── solution.md ├── 10-built-in-functions │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── main.tf │ │ └── solution.md ├── 11-child-module │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── main.tf │ │ ├── modules │ │ └── ec2 │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ └── solution.md ├── 12-workspaces │ ├── instructions.md │ ├── main.tf │ ├── solution │ │ ├── locals.tf │ │ ├── main.tf │ │ ├── solution.md │ │ └── variables.tf │ └── variables.tf ├── 13-resource-drift │ ├── instructions.md │ ├── main.tf │ └── solution │ │ ├── imgs │ │ ├── ec2-instance.png │ │ └── manage-tags.png │ │ ├── main.tf │ │ └── solution.md ├── 14-validate-format-cmds │ ├── instructions.md │ ├── main.tf │ ├── outputs.tf │ ├── solution │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── solution.md │ │ └── variables.tf │ └── variables.tf ├── 15-import-state-cmds │ ├── instructions.md │ ├── main.tf │ └── solution │ │ └── solution.md └── 16-internet-accessible-webserver │ ├── instructions.md │ └── solution │ ├── imgs │ └── apache-default-page.png │ ├── main.tf │ ├── outputs.tf │ ├── sg.tf │ ├── solution.md │ └── variables.tf ├── prerequisites └── instructions.md └── slides.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .terraform.lock.hcl 3 | terraform.tfstate 4 | terraform.tfstate.backup 5 | .terraform.tfstate.lock.info 6 | .terraform 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certified Terraform Associate (CTA) Crash Course 2 | 3 | HashiCorp Terraform is the leading open source automation tool for infrastructure as code (IaC) for building and deploying infrastructure to prominent cloud providers like AWS and Google Cloud. It is extremely valuable to DevOps engineers looking to standardize and simplify the way they define and manage their infrastructure resources. The Terraform Associate certification aims to validate your knowledge of the tool in the form of a multiple-choice exam. As a certified practitioner, you’ll demonstrate your proficiency in using Terraform for production environments including concepts, workflows, and syntax. 4 | 5 | Join expert and [Certified Terraform Associate](https://www.credly.com/badges/d571af1f-3557-4170-977f-84c0dd4d1c7a?source=linked_in_profile) Benjamin Muschko to dive into all the topics covered in the [exam curriculum](https://www.hashicorp.com/certification/terraform-associate), so you’ll be fully prepared to pass the test. You’ll also benefit from Ben’s personal experience with preparing for all aspects of the exam. 6 | 7 | ## Prerequisites 8 | 9 | All exercises in this repository practice a self-contained portion of the [CTA curriculum](https://www.hashicorp.com/certification/terraform-associate). Please make sure to follow the [instructions](./prerequisites/instructions.md) for setting up your environment before joining the training. 10 | 11 | ## Exercises 12 | 13 | All [exercises](./exercises) are numbered and live in dedicated directories starting with the name `exercise-`. You'll find instructions for each exercise in each folder. Solutions are available in the `solution` folder. Try to solve each exercise yourself before having a look at the solution. 14 | 15 | ## Additional Resources 16 | 17 | * 📚 [LeanPub: HashiCorp Terraform Certified Associate Preparation Guide](https://leanpub.com/terraform-certified/) 18 | * 📚 [Packt: HashiCorp Infrastructure Automation Certification Guide](https://www.amazon.com/HashiCorp-Infrastructure-Automation-Certification-Guide/dp/1800565976) 19 | * 🎞️ [YouTube: HashiCorp Terraform Associate Certification Course - Pass the Exam!](https://www.youtube.com/watch?v=V4waklkBC38) 20 | * 🎞️ [A Cloud Guru: HashiCorp Certified Terraform Associate](https://acloudguru.com/course/hashicorp-certified-terraform-associate) 21 | * 🎞️ [Pluralsight: HashiCorp Certified: Terraform Associate Learning Path](https://www.pluralsight.com/paths/hashicorp-certified-terraform-associate) 22 | * 🧪 [Pearson: HashiCorp Terraform Associate](https://learning.oreilly.com/certifications/9780138190408/) 23 | * 🧪 [Udemy: HashiCorp Certified: Terraform Associate Practice Exam](https://www.udemy.com/course/terraform-associate-practice-exam) 24 | * 🧪 [Whizlabs: HashiCorp Certified Terraform Associate Certification](https://www.whizlabs.com/hashicorp-certified-terraform-associate/) 25 | * 🧪 [BrainCert: Hashicorp Certified Terraform Associate Practice Exams](https://prepcatalyst.braincert.com/lms/course/27706-Hashicorp-Certified-Terraform-Associate-Practice-Exams) 26 | -------------------------------------------------------------------------------- /exercises/01-installing-terraform/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 1 2 | 3 | In this exercise, you will install the Terraform binary on your machine. Afterward, you will run commands to verify that the installation is functional. 4 | 5 | 1. Install the latest Terraform binary on your machine. Choose the most fitting [installation method](https://learn.hashicorp.com/tutorials/terraform/install-cli) based on operating system and personal preference. 6 | 2. Run the command `terraform version` to determine the version of the executable. 7 | 3. Run the command `terraform -help` and explore the output. 8 | 4. [Create an AWS account](https://aws.amazon.com/) if you are planning to interact with the cloud provider for the exercises in this course. In your shell, export the AWS credentials with the relevant environment variables. Ignore this step if you are already set up with AWS. 9 | 5. Install an IDE or editor of your choice plus the corresponding Terraform plugin/extension. 10 | -------------------------------------------------------------------------------- /exercises/01-installing-terraform/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Follow the instructions on the Terraform page to install the binary. Verify that Terraform has been installed properly. As shown in the output below, Terraform 1.3.1 has been installed. 4 | 5 | ``` 6 | $ terraform version 7 | Terraform v1.3.1 8 | on darwin_amd64 9 | ``` 10 | 11 | The `-help` and `-h` command line options render the usage pattern of the executable. You will get the same output if you just run the `terraform` command. 12 | 13 | ``` 14 | $ terraform -help 15 | Usage: terraform [global options] [args] 16 | 17 | The available commands for execution are listed below. 18 | The primary workflow commands are given first, followed by 19 | less common or more advanced commands. 20 | 21 | Main commands: 22 | init Prepare your working directory for other commands 23 | validate Check whether the configuration is valid 24 | plan Show changes required by the current configuration 25 | apply Create or update infrastructure 26 | destroy Destroy previously-created infrastructure 27 | 28 | All other commands: 29 | console Try Terraform expressions at an interactive command prompt 30 | fmt Reformat your configuration in the standard style 31 | force-unlock Release a stuck lock on the current workspace 32 | get Install or upgrade remote Terraform modules 33 | graph Generate a Graphviz graph of the steps in an operation 34 | import Associate existing infrastructure with a Terraform resource 35 | login Obtain and save credentials for a remote host 36 | logout Remove locally-stored credentials for a remote host 37 | output Show output values from your root module 38 | providers Show the providers required for this configuration 39 | refresh Update the state to match remote systems 40 | show Show the current state or a saved plan 41 | state Advanced state management 42 | taint Mark a resource instance as not fully functional 43 | test Experimental support for module integration testing 44 | untaint Remove the 'tainted' state from a resource instance 45 | version Show the current Terraform version 46 | workspace Workspace management 47 | 48 | Global options (use these before the subcommand, if any): 49 | -chdir=DIR Switch to a different working directory before executing the 50 | given subcommand. 51 | -help Show this help output, or the help for a specified subcommand. 52 | -version An alias for the "version" subcommand. 53 | ``` 54 | 55 | Congratulations! You are ready to use the Terraform CLI in the next exercises. 56 | -------------------------------------------------------------------------------- /exercises/02-basic-config-file/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 2 2 | 3 | In this exercise, you will implement a simple Terraform configuration file. 4 | 5 | 1. Create a new Terraform configuration file named `main.tf`. 6 | 2. Register the AWS provider named `aws` with the version `4.16.0`. For more information, see the [version on the Terraform registry](https://registry.terraform.io/providers/hashicorp/aws/4.16.0). 7 | 3. Define a provider named `aws` in the region `us-west-2`. 8 | 4. Define a resource of type `aws_instance` named `app_server`. Assign the AMI `ami-077ee47512dc6f3ca` and the instance type `t2.nano`. For more information on available AMI images, see the [EC2 AMI Locator](https://cloud-images.ubuntu.com/locator/ec2/). 9 | 5. In your current shell, export the environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` that set the AWS credentials. 10 | 6. Execute the commands `terraform init` and `terraform validate` commands. You should see a success message from the validation operation. 11 | -------------------------------------------------------------------------------- /exercises/02-basic-config-file/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/02-basic-config-file/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Create the file named `main.tf`. 4 | 5 | ``` 6 | $ touch main.tf 7 | ``` 8 | 9 | Edit the file and register the AWS provider. 10 | 11 | ```terraform 12 | terraform { 13 | required_providers { 14 | aws = { 15 | source = "hashicorp/aws" 16 | version = "4.16.0" 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | Define the provider in the region `us-west-2`. Make sure to use the same name as the registered provider. 23 | 24 | ```terraform 25 | provider "aws" { 26 | region = "us-west-2" 27 | } 28 | ``` 29 | 30 | Add the EC2 instance to the configuration with the relevant attribute values. 31 | 32 | ```terraform 33 | resource "aws_instance" "app_server" { 34 | ami = "ami-077ee47512dc6f3ca" 35 | instance_type = "t2.nano" 36 | } 37 | ``` 38 | 39 | For testing purposes, set the environment variables for the current shell that define the AWS credentials. 40 | 41 | ``` 42 | $ export AWS_ACCESS_KEY_ID= 43 | $ export AWS_SECRET_ACCESS_KEY= 44 | ``` 45 | 46 | Make sure the configuration file can be validated successfully. Run the `init` command to initialize the working directory first. 47 | 48 | ``` 49 | $ terraform init 50 | ... 51 | $ terraform validate 52 | Success! The configuration is valid. 53 | ``` -------------------------------------------------------------------------------- /exercises/03-basic-workflow/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 3 2 | 3 | In this exercise, you will practice a basic workflow with the help of the existing configuration in the file named [`main.tf`](./main.tf). 4 | 5 | 1. Run the Terraform command for initializing the working directory. 6 | 2. Run the Terraform command for validating the configuration file. 7 | 3. Run the Terraform commands for planning and applying the changes. 8 | 4. Open the [AWS dashboard](https://aws.amazon.com/) and find the [provisioned EC2 instance](https://us-west-2.console.aws.amazon.com/ec2/home?region=us-west-2#Instances:instanceState=running). 9 | 5. Run the Terraform command for deleting the infrastructure. 10 | -------------------------------------------------------------------------------- /exercises/03-basic-workflow/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/03-basic-workflow/solution/imgs/ec2-instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuschko/cta-crash-course/8c9f1e2cc773af70587fe253a31fde54135cfd70/exercises/03-basic-workflow/solution/imgs/ec2-instance.png -------------------------------------------------------------------------------- /exercises/03-basic-workflow/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } 18 | -------------------------------------------------------------------------------- /exercises/03-basic-workflow/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Initialize the working directory using the `init` command. 4 | 5 | ``` 6 | $ terraform init 7 | 8 | Initializing the backend... 9 | 10 | Initializing provider plugins... 11 | - Finding hashicorp/aws versions matching "4.16.0"... 12 | - Installing hashicorp/aws v4.16.0... 13 | - Installed hashicorp/aws v4.16.0 (signed by HashiCorp) 14 | 15 | Terraform has created a lock file .terraform.lock.hcl to record the provider 16 | selections it made above. Include this file in your version control repository 17 | so that Terraform can guarantee to make the same selections by default when 18 | you run "terraform init" in the future. 19 | 20 | Terraform has been successfully initialized! 21 | 22 | You may now begin working with Terraform. Try running "terraform plan" to see 23 | any changes that are required for your infrastructure. All Terraform commands 24 | should now work. 25 | 26 | If you ever set or change modules or backend configuration for Terraform, 27 | rerun this command to reinitialize your working directory. If you forget, other 28 | commands will detect it and remind you to do so if necessary. 29 | ``` 30 | 31 | Validating the configuration file should return with no issues. 32 | 33 | ``` 34 | $ terraform validate 35 | Success! The configuration is valid. 36 | ``` 37 | 38 | Execute the `plan` command to see the changes that can be applied. 39 | 40 | ``` 41 | $ terraform plan 42 | 43 | Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the 44 | following symbols: 45 | + create 46 | 47 | Terraform will perform the following actions: 48 | 49 | # aws_instance.app_server will be created 50 | + resource "aws_instance" "app_server" { 51 | ... 52 | ``` 53 | 54 | The `apply` command will make the changes on AWS. 55 | 56 | ``` 57 | $ terraform apply 58 | 59 | Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the 60 | following symbols: 61 | + create 62 | 63 | Terraform will perform the following actions: 64 | 65 | # aws_instance.app_server will be created 66 | + resource "aws_instance" "app_server" { 67 | ... 68 | ``` 69 | 70 | You should be able to find the provisioned EC2 instance in the AWS dashboard. 71 | 72 | ![ec2-instance](./imgs/ec2-instance.png) 73 | 74 | Delete the existing EC2 instance with the `destroy` command. 75 | 76 | ``` 77 | $ terraform destroy 78 | ... 79 | 80 | Plan: 0 to add, 0 to change, 1 to destroy. 81 | 82 | Do you really want to destroy all resources? 83 | Terraform will destroy all your managed infrastructure, as shown above. 84 | There is no undo. Only 'yes' will be accepted to confirm. 85 | 86 | Enter a value: yes 87 | 88 | aws_instance.app_server: Destroying... [id=i-0e11c52ef4a55367f] 89 | aws_instance.app_server: Still destroying... [id=i-0e11c52ef4a55367f, 10s elapsed] 90 | aws_instance.app_server: Still destroying... [id=i-0e11c52ef4a55367f, 20s elapsed] 91 | aws_instance.app_server: Destruction complete after 30s 92 | 93 | Destroy complete! Resources: 1 destroyed. 94 | ``` -------------------------------------------------------------------------------- /exercises/04-provisioner/install_apache.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | sudo apt-get update 3 | sudo apt-get install -y apache2 4 | sudo systemctl start apache2 5 | sudo systemctl enable apache2 6 | echo "

Deployed via Terraform

" | sudo tee /var/www/html/index.html -------------------------------------------------------------------------------- /exercises/04-provisioner/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 4 2 | 3 | In this exercise, you will define a remote provisioner to execute logic after the creation of an EC2 instance. 4 | 5 | 1. Inspect the contents of the existing configuration in the file named [`main.tf`](./main.tf) and [`install_apache.sh`](./install_apache.sh). 6 | 2. Define a remote provisioner via `user_data` for the EC2 instance definition `app_server` that uses the shell script. You can reference the file using the [`file` function](https://developer.hashicorp.com/terraform/language/functions/file). 7 | 3. Perform the Terraform command that provisions the resource and executes the user data. 8 | 4. Find information about the user data in the AWS console for the instance. 9 | 5. Run the Terraform command for deleting the infrastructure. 10 | -------------------------------------------------------------------------------- /exercises/04-provisioner/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/04-provisioner/solution/imgs/aws-user-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuschko/cta-crash-course/8c9f1e2cc773af70587fe253a31fde54135cfd70/exercises/04-provisioner/solution/imgs/aws-user-data.png -------------------------------------------------------------------------------- /exercises/04-provisioner/solution/install_apache.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | sudo apt-get update 3 | sudo apt-get install -y apache2 4 | sudo systemctl start apache2 5 | sudo systemctl enable apache2 6 | echo "

Deployed via Terraform

" | sudo tee /var/www/html/index.html -------------------------------------------------------------------------------- /exercises/04-provisioner/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | user_data = file("install_apache.sh") 18 | } -------------------------------------------------------------------------------- /exercises/04-provisioner/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Point to the shell script using the `user_data` attribute. To parse the contents of the file, use the [`file` function](https://www.terraform.io/language/functions/file). 4 | 5 | ```terraform 6 | resource "aws_instance" "app_server" { 7 | ami = "ami-077ee47512dc6f3ca" 8 | instance_type = "t2.nano" 9 | user_data = file("install_apache.sh") 10 | } 11 | ``` 12 | 13 | Execute the `apply` command to provision the EC2 instance. The shell script will be executed automatically. 14 | 15 | ``` 16 | $ terraform apply 17 | ``` 18 | 19 | You can find information about the executed shell script commands in the system log of the EC2 instance. Select the UI options _Actions > Instance settings > Edit user data_. 20 | 21 | ![AWS user_data](./imgs/aws-user-data.png) 22 | 23 | Delete the existing EC2 instance with the `destroy` command. 24 | 25 | ``` 26 | $ terraform destroy 27 | ``` -------------------------------------------------------------------------------- /exercises/05-input-variables/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 5 2 | 3 | In this exercise, you will replace some hard-coded values in a Terraform configuration file with input variables. Then you will provide values for the variables from the command line. 4 | 5 | 1. Inspect the existing configuration in the file named [`main.tf`](./main.tf). 6 | 2. Create a new configuration file named `variables.tf` that we'll use to define variables. 7 | 3. In the configuration file for variables, define a variable for instance type. The variable value should be assigned to the `instance_type` attribute in the resource named `app_server`. Define the default value `t2.nano`, provide a description, and ensure that the end user cannot pass a `null` value. 8 | 4. Furthermore, define a variable for the AMI and assign it to the `ami` argument of the resource named `app_server`. The default value should be `ami-077ee47512dc6f3ca`. 9 | 5. Lastly, define a variable for the tags used by the resource named `app_server`. Only provide the data type and description for the variable, but no default values. 10 | 6. Execute the `terraform plan` command and with and without providing values for the variables with the `-var` CLI option. 11 | 7. Instead of providing the variable values using the `-var` CLI option, define a variable file that will be consumed automatically without having to provide an explicit CLI option. -------------------------------------------------------------------------------- /exercises/05-input-variables/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/05-input-variables/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = var.ami_id 16 | instance_type = var.instance_type 17 | tags = var.tags 18 | } -------------------------------------------------------------------------------- /exercises/05-input-variables/solution/solution.md: -------------------------------------------------------------------------------- 1 | Create the file named `variables.tf`. 2 | 3 | ``` 4 | $ touch variables.tf 5 | ``` 6 | 7 | Define the variable for the EC2 instance type. 8 | 9 | ```terraform 10 | variable "instance_type" { 11 | type = string 12 | description = "The instance type to use for the EC2 instance." 13 | default = "t2.nano" 14 | nullable = false 15 | } 16 | ``` 17 | 18 | Define the variable for providing a specific AMI identifier. 19 | 20 | ```terraform 21 | variable "ami_id" { 22 | type = string 23 | description = "The AMI identifier to use for EC2 instance." 24 | default = "ami-077ee47512dc6f3ca" 25 | nullable = false 26 | } 27 | ``` 28 | 29 | The `tags` variable expects key-value pairs. Therefore, the data type needs to be a `map`. 30 | 31 | ```terraform 32 | variable "tags" { 33 | type = map(string) 34 | description = "The tags assigned to the EC2 instance." 35 | } 36 | ``` 37 | 38 | Consume the variables in the resource definition in the file `main.tf`. 39 | 40 | ```terraform 41 | resource "aws_instance" "app_server" { 42 | ami = var.ami_id 43 | instance_type = var.instance_type 44 | tags = var.tags 45 | } 46 | ``` 47 | 48 | The `tags` variable does not provide default values. Therefore, the `plan` command will ask you for the values on the command line if you don't define them with the `-var` option. 49 | 50 | ``` 51 | $ terraform plan 52 | var.tags 53 | The tags assigned to the EC2 instance. 54 | 55 | Enter a value: 56 | ``` 57 | 58 | The following command shows the use of the `-var` option for the `tags` variable. 59 | 60 | ``` 61 | $ terraform plan -var='tags={Environment = "Test", Service = "Example"}' 62 | ``` 63 | 64 | To automatically consume variables from a file, store them in `terraform.tfvars`. 65 | 66 | ```terraform 67 | tags = { 68 | Environment = "Test", 69 | Service = "Example" 70 | } 71 | ``` 72 | 73 | You will not have to define a `-var` CLI option anymore. 74 | 75 | ``` 76 | $ terraform plan 77 | ``` 78 | -------------------------------------------------------------------------------- /exercises/05-input-variables/solution/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | description = "The instance type to use for the EC2 instance." 4 | default = "t2.nano" 5 | nullable = false 6 | } 7 | 8 | variable "ami_id" { 9 | type = string 10 | description = "The AMI identifier to use for EC2 instance." 11 | default = "ami-077ee47512dc6f3ca" 12 | nullable = false 13 | } 14 | 15 | variable "tags" { 16 | type = map(string) 17 | description = "The tags assigned to the EC2 instance." 18 | } -------------------------------------------------------------------------------- /exercises/06-local-values/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 6 2 | 3 | In this exercise, you will define local values in the configuration as a means to provide default values. 4 | 5 | 1. Inspect the existing configuration in the file named [`main.tf`](./main.tf). 6 | 2. Create a new configuration file named `locals.tf` that we'll use to define local values. 7 | 3. In the file `locals.tf`, define an map named `timeouts` with two key-value pairs. The map defines the key `create` with the value `60m` and the key `delete` with the value `2h`. 8 | 4. Use the local values in the [`aws_db_instance`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance) resources with the label name `mysql` and `postgres`. 9 | 5. Execute the `plan` command. 10 | -------------------------------------------------------------------------------- /exercises/06-local-values/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_db_instance" "mysql" { 15 | allocated_storage = 10 16 | engine = "mysql" 17 | engine_version = "5.7" 18 | instance_class = "db.t3.micro" 19 | db_name = "mydb" 20 | username = "foo" 21 | password = "foobarbaz" 22 | parameter_group_name = "default.mysql5.7" 23 | skip_final_snapshot = true 24 | } 25 | 26 | resource "aws_db_instance" "postgres" { 27 | allocated_storage = 256 28 | engine = "postgres" 29 | engine_version = "9.5.4" 30 | instance_class = "db.r3.large" 31 | db_name = "data-dump" 32 | username = "user1" 33 | password = "passwd" 34 | parameter_group_name = "mydb" 35 | } -------------------------------------------------------------------------------- /exercises/06-local-values/solution/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | timeouts = { 3 | create = "60m" 4 | delete = "2h" 5 | } 6 | } -------------------------------------------------------------------------------- /exercises/06-local-values/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_db_instance" "mysql" { 15 | allocated_storage = 10 16 | engine = "mysql" 17 | engine_version = "5.7" 18 | instance_class = "db.t3.micro" 19 | db_name = "mydb" 20 | username = "foo" 21 | password = "foobarbaz" 22 | parameter_group_name = "default.mysql5.7" 23 | skip_final_snapshot = true 24 | timeouts { 25 | create = local.timeouts.create 26 | delete = local.timeouts.delete 27 | } 28 | } 29 | 30 | resource "aws_db_instance" "postgres" { 31 | allocated_storage = 256 32 | engine = "postgres" 33 | engine_version = "9.5.4" 34 | instance_class = "db.r3.large" 35 | db_name = "data-dump" 36 | username = "user1" 37 | password = "passwd" 38 | parameter_group_name = "mydb" 39 | timeouts { 40 | create = local.timeouts.create 41 | delete = local.timeouts.delete 42 | } 43 | } -------------------------------------------------------------------------------- /exercises/06-local-values/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Create the file named `locals.tf`. 4 | 5 | ``` 6 | $ touch locals.tf 7 | ``` 8 | 9 | Define the local values. 10 | 11 | ```terraform 12 | locals { 13 | timeouts = { 14 | create = "60m" 15 | delete = "2h" 16 | } 17 | } 18 | ``` 19 | 20 | Consume the local values in the resource definition in the file `main.tf`. 21 | 22 | ```terraform 23 | resource "aws_db_instance" "mysql" { 24 | allocated_storage = 10 25 | engine = "mysql" 26 | engine_version = "5.7" 27 | instance_class = "db.t3.micro" 28 | db_name = "mydb" 29 | username = "foo" 30 | password = "foobarbaz" 31 | parameter_group_name = "default.mysql5.7" 32 | skip_final_snapshot = true 33 | timeouts { 34 | create = local.timeouts.create 35 | delete = local.timeouts.delete 36 | } 37 | } 38 | 39 | resource "aws_db_instance" "postgres" { 40 | allocated_storage = 256 41 | engine = "postgres" 42 | engine_version = "9.5.4" 43 | instance_class = "db.r3.large" 44 | db_name = "data-dump" 45 | username = "user1" 46 | password = "passwd" 47 | parameter_group_name = "mydb" 48 | timeouts { 49 | create = local.timeouts.create 50 | delete = local.timeouts.delete 51 | } 52 | } 53 | ``` 54 | 55 | Execute the `plan` command. You should see the timeout values in the output of the command. 56 | 57 | ``` 58 | $ terraform plan 59 | ``` 60 | -------------------------------------------------------------------------------- /exercises/07-output-values/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 7 2 | 3 | In this exercise, you will define an output value for the [public IP address of an EC2 instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#public_ip). 4 | 5 | 1. Inspect the existing configuration in the file named [`main.tf`](./main.tf). 6 | 2. Create a new configuration file named `outputs.tf` that we'll use to define output values. 7 | 3. In the configuration file for output values, define an output for the public IP address of the `aws_instance` instance named `app_server`. Provide a description for the output value. 8 | 4. Execute the `terraform apply` command and find the output value in the console output. 9 | 5. Execute the `terraform output` command and find the definition in the console output. Would you expect the output to contain the value? 10 | -------------------------------------------------------------------------------- /exercises/07-output-values/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/07-output-values/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/07-output-values/solution/outputs.tf: -------------------------------------------------------------------------------- 1 | output "app_server_public_ip" { 2 | value = aws_instance.app_server.public_ip 3 | description = "The public IP address of the EC2 server instance." 4 | } 5 | -------------------------------------------------------------------------------- /exercises/07-output-values/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Create the file named `outputs.tf`. 4 | 5 | ``` 6 | $ touch outputs.tf 7 | ``` 8 | 9 | Define the output value. 10 | 11 | ```terraform 12 | output "app_server_public_ip" { 13 | value = aws_instance.app_server.public_ip 14 | description = "The public IP address of the EC2 server instance." 15 | } 16 | ``` 17 | 18 | The `plan` command will tell you about the added output value. 19 | 20 | ``` 21 | $ terraform plan 22 | ... 23 | 24 | Changes to Outputs: 25 | + app_server_public_ip = (known after apply) 26 | ``` 27 | 28 | The `output` command does not have the produced value in the state database yet. You will need to run the `apply` command first. 29 | 30 | ``` 31 | $ terraform output 32 | ╷ 33 | │ Warning: No outputs found 34 | │ 35 | │ The state file either has no outputs defined, or all the defined outputs are empty. Please define an output in 36 | │ your configuration with the `output` keyword and run `terraform refresh` for it to become available. If you are 37 | │ using interpolation, please verify the interpolated value is not empty. You can use the `terraform console` 38 | │ command to assist. 39 | ╵ 40 | ``` 41 | 42 | The `apply` command will render the output value as it as been assigned at this point. 43 | 44 | ``` 45 | $ terraform apply 46 | ... 47 | 48 | Outputs: 49 | 50 | app_server_public_ip = "34.211.146.45" 51 | ``` 52 | 53 | You can also retrieve the output value using the `output` command. 54 | 55 | ``` 56 | $ terraform output 57 | app_server_public_ip = "34.211.146.45" 58 | ``` -------------------------------------------------------------------------------- /exercises/08-data-source/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 8 2 | 3 | In this exercise, you will define a data source for consuming a response from a HTTP call. The response should be parsed and used as an output value. 4 | 5 | 1. Create a new configuration file named `main.tf`. 6 | 2. Define a data source named `weather` using the [hashicorp/http](https://registry.terraform.io/providers/hashicorp/http/latest) provider with version 3.4.0. 7 | 3. Assign the URL `https://api.weather.gov/points/39.73,-104.99` and declare the `application/geo+json` request header. 8 | 4. Define the output value named `city` that consumes the response body of the data source. Parse the value of the city attribute from the response body using the built-in function [jsondecode](https://www.terraform.io/language/functions/jsondecode). 9 | 5. Execute the `terraform plan` command and find the output value in the console output. 10 | -------------------------------------------------------------------------------- /exercises/08-data-source/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | http = { 4 | source = "hashicorp/http" 5 | version = "3.2.1" 6 | } 7 | } 8 | } 9 | 10 | data "http" "weather" { 11 | url = "https://api.weather.gov/points/39.73,-104.99" 12 | 13 | request_headers = { 14 | "Accept" = "application/geo+json" 15 | } 16 | } 17 | 18 | output "city" { 19 | value = jsondecode(data.http.weather.response_body).properties.relativeLocation.properties.city 20 | } 21 | -------------------------------------------------------------------------------- /exercises/08-data-source/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Create the file named `main.tf`. 4 | 5 | ``` 6 | $ touch main.tf 7 | ``` 8 | 9 | Add the provider with the version 3.4.0. 10 | 11 | ```terraform 12 | terraform { 13 | required_providers { 14 | http = { 15 | source = "hashicorp/http" 16 | version = "3.4.0" 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | Define the data source. Assign the URL and the request header. 23 | 24 | ```terraform 25 | data "http" "weather" { 26 | url = "https://api.weather.gov/points/39.73,-104.99" 27 | 28 | request_headers = { 29 | Accept = "application/geo+json" 30 | } 31 | } 32 | ``` 33 | 34 | Define an output value that consumes the response body provided by the data source. You can navigate the JSON structure by path. Make sure the decode the JSON content using the `jsondecode` function. 35 | 36 | ```terraform 37 | output "city" { 38 | value = jsondecode(data.http.weather.response_body).properties.relativeLocation.properties.city 39 | } 40 | ``` 41 | 42 | Executing the `plan` command renders the output. You should see the city `Glendale`. 43 | 44 | ``` 45 | $ terraform plan 46 | data.http.weather: Reading... 47 | data.http.weather: Read complete after 1s [id=https://api.weather.gov/points/39.73,-104.99] 48 | 49 | Changes to Outputs: 50 | + city = "Glendale" 51 | 52 | You can apply this plan to save these new output values to the Terraform state, without changing any 53 | real infrastructure. 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /exercises/09-loop/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 9 2 | 3 | In this exercise, you will define an input variable used for defining a loop using the `count` argument. 4 | 5 | 1. Inspect the existing configuration in the file named [`main.tf`](./main.tf). 6 | 2. Create an input variable named `instance_count` with the data type `number`. Define the number 3 as the default value for the input variable. 7 | 3. Specify that instances of the `aws_instance` should be created using the `count` argument. Add a tag named `Name` for each instance that uses the current count index in its assigned value. 8 | 4. Execute the `plan` command by providing no value for the `instance_count` input variable, and 5 to define a value explicitly. 9 | -------------------------------------------------------------------------------- /exercises/09-loop/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/09-loop/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | variable "instance_count" { 15 | type = number 16 | description = "The number of EC2 instances to be managed." 17 | default = 3 18 | } 19 | 20 | resource "aws_instance" "app_server" { 21 | count = var.instance_count 22 | ami = "ami-077ee47512dc6f3ca" 23 | instance_type = "t2.nano" 24 | tags = { 25 | Name = "instance ${count.index}" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /exercises/09-loop/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Start by defining the input variable named `instance_count`. 4 | 5 | ```terraform 6 | variable "instance_count" { 7 | type = number 8 | description = "The number of EC2 instances to be managed." 9 | default = 3 10 | } 11 | ``` 12 | 13 | To use the count syntax, simply assign the variable named `instance_count` to the `count` attribute. 14 | 15 | ```terraform 16 | resource "aws_instance" "app_server" { 17 | count = var.instance_count 18 | ami = "ami-077ee47512dc6f3ca" 19 | instance_type = "t2.nano" 20 | tags = { 21 | Name = "instance ${count.index}" 22 | } 23 | } 24 | ``` 25 | 26 | The `plan` command uses the default value 3 if the value of the input variable has not been provided. You can control the number of instances by setting a variable value other than 3. 27 | 28 | ``` 29 | $ terraform plan 30 | ... 31 | Plan: 3 to add, 0 to change, 0 to destroy. 32 | 33 | $ terraform plan -var='instance_count=5' 34 | ... 35 | Plan: 5 to add, 0 to change, 0 to destroy. 36 | ``` 37 | -------------------------------------------------------------------------------- /exercises/10-built-in-functions/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 10 2 | 3 | In this exercise, you will create public and private subnets from a CIDR block without hard-coding the values. Instead you will use the [`cidrsubnet`](https://developer.hashicorp.com/terraform/language/functions/cidrsubnet) function to create network spaces programmatically. 4 | 5 | 1. Inspect the existing configuration in the file named [`main.tf`](./main.tf). 6 | 2. Create a local variable named `private_subnets`. Use the `cidrsubnet` function to compute 3 new subnet addresses within given IP network address prefix. Use the input variable `cidr` as a starting point, add 8 as the number of additional bits, and start with 1 as the whole number that can be represented as a binary integer. 7 | 2. Create a local variable named `public_subnets`. Use the `cidrsubnet` function to compute 3 new subnet addresses within given IP network address prefix. Use the input variable `cidr` as a starting point, add 8 as the number of additional bits, and start with 4 as the whole number that can be represented as a binary integer. 8 | 4. Execute the `plan` command. What CIDRs do you expect to be generated? 9 | -------------------------------------------------------------------------------- /exercises/10-built-in-functions/main.tf: -------------------------------------------------------------------------------- 1 | variable "cidr" { 2 | type = string 3 | default = "10.0.0.0/16" 4 | description = "A network address prefix in CIDR notation" 5 | } 6 | 7 | output "public_subnets" { 8 | value = local.public_subnets 9 | description = "Computed public subnet CIDR blocks" 10 | } 11 | 12 | output "private_subnets" { 13 | value = local.private_subnets 14 | description = "Computed private subnet CIDR blocks" 15 | } -------------------------------------------------------------------------------- /exercises/10-built-in-functions/solution/main.tf: -------------------------------------------------------------------------------- 1 | variable "cidr" { 2 | type = string 3 | default = "10.0.0.0/16" 4 | description = "A network address prefix in CIDR notation" 5 | } 6 | 7 | locals { 8 | private_subnets = [ 9 | cidrsubnet(var.cidr, 8, 1), 10 | cidrsubnet(var.cidr, 8, 2), 11 | cidrsubnet(var.cidr, 8, 3) 12 | ] 13 | 14 | public_subnets = [ 15 | cidrsubnet(var.cidr, 8, 4), 16 | cidrsubnet(var.cidr, 8, 5), 17 | cidrsubnet(var.cidr, 8, 6) 18 | ] 19 | } 20 | 21 | output "public_subnets" { 22 | value = local.public_subnets 23 | description = "Computed public subnet CIDR blocks" 24 | } 25 | 26 | output "private_subnets" { 27 | value = local.private_subnets 28 | description = "Computed private subnet CIDR blocks" 29 | } -------------------------------------------------------------------------------- /exercises/10-built-in-functions/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Define a local variable named `private_subnets` and `public_subnets`. Assign a list of strings. Each item in the list needs to use the `cidrsubnet` function. Feed in the value of the `cidr` input variable as `prefix` attribute for the function. The `newbits` attribute needs to use the value 8. Assign a counting integer to the `netnum` attribute starting with the number 0. 4 | 5 | ```terraform 6 | locals { 7 | private_subnets = [ 8 | cidrsubnet(var.cidr, 8, 1), 9 | cidrsubnet(var.cidr, 8, 2), 10 | cidrsubnet(var.cidr, 8, 3) 11 | ] 12 | 13 | public_subnets = [ 14 | cidrsubnet(var.cidr, 8, 4), 15 | cidrsubnet(var.cidr, 8, 5), 16 | cidrsubnet(var.cidr, 8, 6) 17 | ] 18 | } 19 | ``` 20 | 21 | The resulting execution of the `plan` command will generate 3 private subnets and 3 public subnets. 22 | 23 | ``` 24 | $ terraform plan 25 | 26 | Changes to Outputs: 27 | + private_subnets = [ 28 | + "10.0.1.0/24", 29 | + "10.0.2.0/24", 30 | + "10.0.3.0/24", 31 | ] 32 | + public_subnets = [ 33 | + "10.0.4.0/24", 34 | + "10.0.5.0/24", 35 | + "10.0.6.0/24", 36 | ] 37 | ``` -------------------------------------------------------------------------------- /exercises/11-child-module/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 11 2 | 3 | In this exercise, you extract existing configuration into a local module while following best practices. The root module will then consume the child module. 4 | 5 | 1. Inspect the existing configuration in the file named [`main.tf`](./main.tf). 6 | 2. Extract the creation of the `aws_instance` into a child module named `ec2` in the subdirectory `modules`. 7 | 3. Expose input variables for the AMI and tags. Create the variables in the file `variables.tf`. 8 | 4. Capture the private IP of the EC2 instance in an output value named `private_ip`. 9 | 5. Consume the child module from the root module. Provide the same input values you saw in the initial state of the exercise. 10 | 6. Execute the `plan` command. 11 | -------------------------------------------------------------------------------- /exercises/11-child-module/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | tags = { 18 | Name = "App Server" 19 | } 20 | } -------------------------------------------------------------------------------- /exercises/11-child-module/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | module "app_server" { 15 | source = "./modules/ec2" 16 | ami_id = "ami-077ee47512dc6f3ca" 17 | tags = { Name: "App Server" } 18 | } -------------------------------------------------------------------------------- /exercises/11-child-module/solution/modules/ec2/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_instance" "app_server" { 2 | ami = var.ami_id 3 | instance_type = "t2.nano" 4 | tags = var.tags 5 | } -------------------------------------------------------------------------------- /exercises/11-child-module/solution/modules/ec2/outputs.tf: -------------------------------------------------------------------------------- 1 | output "app_server_private_ip" { 2 | value = aws_instance.app_server.private_ip 3 | description = "The private IP address of the EC2 server instance." 4 | } 5 | -------------------------------------------------------------------------------- /exercises/11-child-module/solution/modules/ec2/variables.tf: -------------------------------------------------------------------------------- 1 | variable "ami_id" { 2 | type = string 3 | description = "The AMI identifier to use for EC2 instance." 4 | default = "ami-077ee47512dc6f3ca" 5 | nullable = false 6 | } 7 | 8 | variable "tags" { 9 | type = map(string) 10 | description = "The tags assigned to the EC2 instance." 11 | } -------------------------------------------------------------------------------- /exercises/11-child-module/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Start by creating the subdirectory `modules/ec2`. 4 | 5 | ``` 6 | $ mkdir -p modules/ec2 7 | ``` 8 | 9 | Create the file `variables.tf` in the subdirectory `modules/ec2`. Add the following content. 10 | 11 | ```terraform 12 | variable "ami_id" { 13 | type = string 14 | description = "The AMI identifier to use for EC2 instance." 15 | default = "ami-077ee47512dc6f3ca" 16 | nullable = false 17 | } 18 | 19 | variable "tags" { 20 | type = map(string) 21 | description = "The tags assigned to the EC2 instance." 22 | } 23 | ``` 24 | 25 | Create the file `main.tf` in the subdirectory `modules/ec2`. Add the following content. 26 | 27 | ```terraform 28 | resource "aws_instance" "app_server" { 29 | ami = var.ami_id 30 | instance_type = "t2.nano" 31 | tags = var.tags 32 | } 33 | ``` 34 | 35 | Create the file `outputs.tf` in the subdirectory `modules/ec2`. Add the following content. 36 | 37 | 38 | ```terraform 39 | output "app_server_private_ip" { 40 | value = aws_instance.app_server.private_ip 41 | description = "The private IP address of the EC2 server instance." 42 | } 43 | ``` 44 | 45 | Modify the `main.tf` in the root module by replacing the `resource` definition with a `module` definition. The module's `source` attribute needs to point to the local path of the child module. Make sure to start with a dot character to indicate that you are referencing a local path. Assign the values to the input variables `ami_id` and `tags`. 46 | 47 | ```terraform 48 | module "app_server" { 49 | source = "./modules/ec2" 50 | ami_id = "ami-077ee47512dc6f3ca" 51 | tags = { Name: "App Server" } 52 | } 53 | ``` 54 | 55 | The `plan` commahd should succeed and plan a single EC2 instance. 56 | 57 | ``` 58 | $ terraform plan 59 | ... 60 | Plan: 1 to add, 0 to change, 0 to destroy. 61 | ``` -------------------------------------------------------------------------------- /exercises/12-workspaces/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 12 2 | 3 | In this exercise, you will create two new workspaces. Each workspace will use different input values. You will learn to switch between the workspaces to plan the changes. 4 | 5 | 1. Inspect the existing configuration files in the current directory. 6 | 2. Create two new workspaces, `dev` and `prod`. 7 | 3. In the file `locals.tf`, define a variable of type `map` named `instance_config`. The variable should group workspace configuration for each environment. You should end up with a map of maps. Assign the EC2 instance type `t2.nano` to the `dev` workspace, and `t2.micro` to the `prod` workspace. Remove the input variable `instance_type` and use the local variable in `main.tf`. 8 | 4. The tags assigned to the EC2 instance should contain an entry for the current environment using the key `Environment`. Add the configuration to the local variable. Continue to use the tags captured through the input variable `tags` and merge them with the value from the local variable in `main.tf`. 9 | 5. Execute the `terraform plan` command for the `dev` workspace. Inspect the result. 10 | 6. Execute the `terraform plan` command for the `prod` workspace. Inspect the result. 11 | 7. Delete both workspaces, `dev` and `prod`. 12 | -------------------------------------------------------------------------------- /exercises/12-workspaces/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = var.ami_id 16 | instance_type = var.instance_type 17 | tags = var.tags 18 | } -------------------------------------------------------------------------------- /exercises/12-workspaces/solution/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | instance_config = { 3 | dev = { 4 | instance_type = "t2.nano" 5 | env = "development" 6 | }, 7 | prod = { 8 | instance_type = "t2.micro" 9 | env = "production" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /exercises/12-workspaces/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = var.ami_id 16 | instance_type = local.instance_config[terraform.workspace].instance_type 17 | tags = merge(var.tags, { Environment=local.instance_config[terraform.workspace].env }) 18 | } -------------------------------------------------------------------------------- /exercises/12-workspaces/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Start by creating both workspaces. 4 | 5 | ``` 6 | $ terraform workspace new dev 7 | Created and switched to workspace "dev"! 8 | $ terraform workspace new prod 9 | Created and switched to workspace "prod"! 10 | ``` 11 | 12 | Create a new file `locals.tf` and add the workspace configuration there. You should end up with the following definition. 13 | 14 | ```terraform 15 | locals { 16 | instance_config = { 17 | dev = { 18 | instance_type = "t2.nano" 19 | env = "development" 20 | }, 21 | prod = { 22 | instance_type = "t2.micro" 23 | env = "production" 24 | } 25 | } 26 | } 27 | ``` 28 | To use the local variables, simply use the `local` prefix. The local variable is of type `map` so you can pick the relevant workspace configuration by passing in the currently-selected workspace using the `terraform.workspace` variable. The input variable `tags` should still ask the end user for a value. You can merge the provided value with the environment-specific value by using the built-in `merge` function. 29 | 30 | ```terraform 31 | resource "aws_instance" "app_server" { 32 | ami = var.ami_id 33 | instance_type = local.instance_config[terraform.workspace].instance_type 34 | tags = merge(var.tags, { Environment=local.instance_config[terraform.workspace].env }) 35 | } 36 | ``` 37 | 38 | Select the `dev` workspace and run the `plan` command. The configuration values for the development workspace should be applied. 39 | 40 | ``` 41 | $ terraform workspace select dev 42 | Switched to workspace "dev". 43 | $ terraform plan 44 | 45 | var.tags 46 | The tags assigned to the EC2 instance. 47 | 48 | Enter a value: {Service = "hello"} 49 | ... 50 | # aws_instance.app_server will be created 51 | + resource "aws_instance" "app_server" { 52 | + instance_type = "t2.nano" 53 | ... 54 | + tags = { 55 | + "Environment" = "development" 56 | + "Service" = "hello" 57 | } 58 | } 59 | ... 60 | } 61 | } 62 | ``` 63 | 64 | Switch to the `prod` workspace and run the `plan` command. The configuration values for the production workspace should be applied. 65 | 66 | ``` 67 | $ terraform workspace select prod 68 | Switched to workspace "prod". 69 | $ terraform plan 70 | 71 | var.tags 72 | The tags assigned to the EC2 instance. 73 | 74 | Enter a value: {Service = "hello"} 75 | ... 76 | # aws_instance.app_server will be created 77 | + resource "aws_instance" "app_server" { 78 | + instance_type = "t2.micro" 79 | ... 80 | + tags = { 81 | + "Environment" = "production" 82 | + "Service" = "hello" 83 | } 84 | } 85 | ... 86 | } 87 | } 88 | ``` 89 | 90 | Change to the `default` workspace before deleting the `dev` and `prod` workspaces. 91 | 92 | ``` 93 | $ terraform workspace select default 94 | Switched to workspace "default". 95 | $ terraform workspace delete dev 96 | Deleted workspace "dev"! 97 | $ terraform workspace delete prod 98 | Deleted workspace "prod"! 99 | ``` -------------------------------------------------------------------------------- /exercises/12-workspaces/solution/variables.tf: -------------------------------------------------------------------------------- 1 | variable "ami_id" { 2 | type = string 3 | description = "The AMI identifier to use for EC2 instance." 4 | default = "ami-077ee47512dc6f3ca" 5 | nullable = false 6 | } 7 | 8 | variable "tags" { 9 | type = map(string) 10 | description = "The tags assigned to the EC2 instance." 11 | } -------------------------------------------------------------------------------- /exercises/12-workspaces/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | description = "The instance type to use for the EC2 instance." 4 | default = "t2.nano" 5 | nullable = false 6 | } 7 | 8 | variable "ami_id" { 9 | type = string 10 | description = "The AMI identifier to use for EC2 instance." 11 | default = "ami-077ee47512dc6f3ca" 12 | nullable = false 13 | } 14 | 15 | variable "tags" { 16 | type = map(string) 17 | description = "The tags assigned to the EC2 instance." 18 | } -------------------------------------------------------------------------------- /exercises/13-resource-drift/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 13 2 | 3 | In this exercise, you will manage resource drift that can occur if a resource under Terraform's control has been changed manually in the cloud environment. 4 | 5 | 1. In the current directory, run the `init` and `apply` command to create the resources need to set up the scenario. 6 | 2. Open the AWS dashboard and find the created resource. 7 | 3. Add two tags to the resource manually from the AWS dashboard: `Service = Backend`, `Author = John Doe`. 8 | 4. Run the `plan` command. What do you see? 9 | 5. Reconcile the local state with the actual configuration in AWS. Ensure that the `apply` command reports with "no changes needed". 10 | -------------------------------------------------------------------------------- /exercises/13-resource-drift/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } 18 | -------------------------------------------------------------------------------- /exercises/13-resource-drift/solution/imgs/ec2-instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuschko/cta-crash-course/8c9f1e2cc773af70587fe253a31fde54135cfd70/exercises/13-resource-drift/solution/imgs/ec2-instance.png -------------------------------------------------------------------------------- /exercises/13-resource-drift/solution/imgs/manage-tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuschko/cta-crash-course/8c9f1e2cc773af70587fe253a31fde54135cfd70/exercises/13-resource-drift/solution/imgs/manage-tags.png -------------------------------------------------------------------------------- /exercises/13-resource-drift/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | tags = { 18 | "Service": "Backend", 19 | "Author": "John Doe" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /exercises/13-resource-drift/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Create the EC2 instance on AWS with the following commands: 4 | 5 | ``` 6 | $ terraform init 7 | $ terraform apply 8 | ``` 9 | 10 | You should be able to find the provisioned EC2 instance in the AWS dashboard. 11 | 12 | ![ec2-instance](./imgs/ec2-instance.png) 13 | 14 | Add the new tags under _Tags > Manage Tags_ for the EC2 instance. 15 | 16 | ![manage-tags](./imgs/manage-tags.png) 17 | 18 | Execute the `plan` command. The command output indicates the changes that happened in the cloud. The `plan` command automatically compared the local state with the current state in the cloud. 19 | 20 | ``` 21 | $ terraform plan 22 | aws_instance.app_server: Refreshing state... [id=i-0b7c0e5c308a5842f] 23 | 24 | Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: 25 | ~ update in-place 26 | 27 | Terraform will perform the following actions: 28 | 29 | # aws_instance.app_server will be updated in-place 30 | ~ resource "aws_instance" "app_server" { 31 | id = "i-0b7c0e5c308a5842f" 32 | ~ tags = { 33 | - "Author" = "John Doe" -> null 34 | - "Service" = "Backend" -> null 35 | } 36 | ~ tags_all = { 37 | - "Author" = "John Doe" 38 | - "Service" = "Backend" 39 | } -> (known after apply) 40 | # (28 unchanged attributes hidden) 41 | 42 | # (6 unchanged blocks hidden) 43 | } 44 | 45 | Plan: 0 to add, 1 to change, 0 to destroy. 46 | ``` 47 | 48 | If you were to run `apply` as-is, then the operation would remove the tags of the EC2 instance in AWS as the Terraform configuration files doesn't declare them. 49 | 50 | Add the tags to the EC2 resource definition in `main.tf` to reflect the actual state in the cloud. It could look as follows: 51 | 52 | ```terraform 53 | resource "aws_instance" "app_server" { 54 | ami = "ami-077ee47512dc6f3ca" 55 | instance_type = "t2.nano" 56 | tags = { 57 | "Service": "Backend", 58 | "Author": "John Doe" 59 | } 60 | } 61 | ``` 62 | 63 | The `plan` and `apply` commands will not require any changes to the infrastructure. 64 | 65 | ``` 66 | $ terraform plan 67 | aws_instance.app_server: Refreshing state... [id=i-0b7c0e5c308a5842f] 68 | 69 | No changes. Your infrastructure matches the configuration. 70 | 71 | $ terraform apply 72 | aws_instance.app_server: Refreshing state... [id=i-0b7c0e5c308a5842f] 73 | 74 | No changes. Your infrastructure matches the configuration. 75 | 76 | Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed. 77 | 78 | Apply complete! Resources: 0 added, 0 changed, 0 destroyed. 79 | ``` 80 | 81 | At this time, you'd usually commit and push the changes to version control. 82 | -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 14 2 | 3 | In this exercise, you will fix and format existing configuration files using the relevant Terraform commands. 4 | 5 | 1. Inspect the configuration files in the current directory. Is there anything that sticks out that doesn't follow best practices? 6 | 2. Execute the `validate` command. Fix any issues you see. 7 | 3. Execute the `fmt` command. What has been changed? -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" 11 | { 12 | region = "us-west-2" 13 | access_key = "abc" 14 | secret_key = "def" 15 | } 16 | 17 | resource "aws_instance" "app_server" { 18 | ami = var_ami_id 19 | instance_type = var.instance_type 20 | tags = [var.tags] 21 | } -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/outputs.tf: -------------------------------------------------------------------------------- 1 | output "app_server_public_ip" { 2 | val = aws_instance.app_server.public_ip 3 | description = "The public IP address of the EC2 server instance." 4 | } 5 | -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.17.1" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = var.ami_id 16 | instance_type = var.instance_type 17 | tags = var.tags 18 | } -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/solution/outputs.tf: -------------------------------------------------------------------------------- 1 | output "app_server_public_ip" { 2 | value = aws_instance.app_server.public_ip 3 | description = "The public IP address of the EC2 server instance." 4 | } 5 | -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | The `main.tf` configuration file directly defines AWS credentials in plain-text. Those variable values would be stored in the state file and therefore this configuration is not safe. You should remove the value assignment. You should use environment variables or a AWS credentials file instead. Remove the following lines from the configuration file. 4 | 5 | ```terraform 6 | access_key = "abc" 7 | secret_key = "def" 8 | ``` 9 | 10 | Execute the `validate` command. You will see the following errors. 11 | 12 | ``` 13 | $ terraform validate 14 | ╷ 15 | │ Error: Invalid block definition 16 | │ 17 | │ on main.tf line 10: 18 | │ 10: provider "aws" 19 | │ 11: { 20 | │ 21 | │ A block definition must have block content delimited by "{" and "}", starting on the same line 22 | │ as the block header. 23 | ╵ 24 | ╷ 25 | │ Error: Missing required argument 26 | │ 27 | │ on outputs.tf line 1, in output "app_server_public_ip": 28 | │ 1: output "app_server_public_ip" { 29 | │ 30 | │ The argument "value" is required, but no definition was found. 31 | ╵ 32 | ╷ 33 | │ Error: Unsupported argument 34 | │ 35 | │ on outputs.tf line 2, in output "app_server_public_ip": 36 | │ 2: val = aws_instance.app_server.public_ip 37 | │ 38 | │ An argument named "val" is not expected here. Did you mean "value"? 39 | ``` 40 | 41 | Fix the first error message by moving the opening curly brace of the AWS provider to the same line as the defintion. The resulting code will look as follows. 42 | 43 | ```terraform 44 | provider "aws" { 45 | region = "us-west-2" 46 | } 47 | ``` 48 | 49 | The next error message indicates the attribute named `value` has not been defined in the output value in the file `outputs.tf`. Change the code from `val` to `value`. 50 | 51 | ```terraform 52 | output "app_server_public_ip" { 53 | value = aws_instance.app_server.public_ip 54 | description = "The public IP address of the EC2 server instance." 55 | } 56 | ``` 57 | 58 | Another issue may see is the following: 59 | 60 | ``` 61 | $ terraform validate 62 | ╷ 63 | │ Error: Invalid reference 64 | │ 65 | │ on main.tf line 15, in resource "aws_instance" "app_server": 66 | │ 15: ami = var_ami_id 67 | │ 68 | │ A reference to a resource type must be followed by at least one attribute access, specifying 69 | │ the resource name. 70 | ``` 71 | 72 | This can be fixed by referencing the input variable named `ami_id` properly. Use `var.ami_id` instead of `var_ami_id`. 73 | 74 | ```terraform 75 | resource "aws_instance" "app_server" { 76 | ami = var.ami_id 77 | instance_type = var.instance_type 78 | tags = [var.tags] 79 | } 80 | ``` 81 | 82 | The last error you will encounter is the assignment to the `tags` attribute. We are trying to wrap a `map` defined by the `tags` input variables into a list. 83 | 84 | ``` 85 | $ terraform validate 86 | ╷ 87 | │ Error: Incorrect attribute value type 88 | │ 89 | │ on main.tf line 17, in resource "aws_instance" "app_server": 90 | │ 17: tags = [var.tags] 91 | │ ├──────────────── 92 | │ │ var.tags is a map of string, known only after apply 93 | │ 94 | │ Inappropriate value for attribute "tags": map of string required. 95 | ``` 96 | 97 | Fix this by removing the square brackets. 98 | 99 | ```terraform 100 | resource "aws_instance" "app_server" { 101 | ami = var.ami_id 102 | instance_type = var.instance_type 103 | tags = var.tags 104 | } 105 | ``` 106 | 107 | The `validate` command should find no more errors in the configuration files. 108 | 109 | ``` 110 | $ terraform validate 111 | Success! The configuration is valid. 112 | ``` 113 | 114 | The `fmt` command reformats the following files. 115 | 116 | ``` 117 | $ terraform fmt 118 | main.tf 119 | outputs.tf 120 | variables.tf 121 | ``` -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/solution/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | description = "The instance type to use for the EC2 instance." 4 | default = "t2.nano" 5 | nullable = false 6 | } 7 | 8 | variable "ami_id" { 9 | type = string 10 | description = "The AMI identifier to use for EC2 instance." 11 | default = "ami-077ee47512dc6f3ca" 12 | nullable = false 13 | } 14 | 15 | variable "tags" { 16 | type = map(string) 17 | description = "The tags assigned to the EC2 instance." 18 | } -------------------------------------------------------------------------------- /exercises/14-validate-format-cmds/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | description = "The instance type to use for the EC2 instance." 4 | default = "t2.nano" 5 | nullable = false 6 | } 7 | 8 | variable "ami_id" { 9 | type = string 10 | description = "The AMI identifier to use for EC2 instance." 11 | default = "ami-077ee47512dc6f3ca" 12 | nullable = false 13 | } 14 | 15 | variable "tags" { 16 | type = map(string) 17 | description = "The tags assigned to the EC2 instance." 18 | } -------------------------------------------------------------------------------- /exercises/15-import-state-cmds/instructions.md: -------------------------------------------------------------------------------- 1 | # Excercise 15 2 | 3 | In this exercise, you will use the `import` command to bring a EC2 server under Terraform's management. 4 | 5 | 1. Create the EC2 instance by running the relevant commands for the existing configuration in the current directory. 6 | 2. Identify the resource ID of the EC2 instance in the AWS dashboard. 7 | 3. Run the command `terraform state rm aws_instance.app_server` to remove the instance from the local state file and delete the configuration from the `main.tf` file. 8 | 4. Perform the relevant actions for bringing the instance under Terraform's management again. 9 | 5. Destroy the instance once you are done with the exercise. -------------------------------------------------------------------------------- /exercises/15-import-state-cmds/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "4.16.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "aws_instance" "app_server" { 15 | ami = "ami-077ee47512dc6f3ca" 16 | instance_type = "t2.nano" 17 | } -------------------------------------------------------------------------------- /exercises/15-import-state-cmds/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | Run the commands for deploying the EC2 instance on AWS. 4 | 5 | ``` 6 | $ terraform init 7 | $ terraform plan 8 | $ terraform apply 9 | ``` 10 | 11 | Open a browser and enter the URL [https://aws.amazon.com/](https://aws.amazon.com/). Log into your account. Navigate to _My Account > AWS Management Console_. Open the EC2 Dashboard. You should find the running EC2 instance you just created. Get the value for the column "Instance ID". For the purpose of explaining the import workflow, we'll use the instance ID `i-0651c2edd7dc884ac`. 12 | 13 | Remove the instance from the local state with the following command. 14 | 15 | ``` 16 | $ terraform state rm aws_instance.app_server 17 | Removed aws_instance.app_server 18 | Successfully removed 1 resource instance(s). 19 | ``` 20 | 21 | Delete the following section from the `main.tf` file. 22 | 23 | ```terraform 24 | resource "aws_instance" "app_server" { 25 | ami = "ami-077ee47512dc6f3ca" 26 | instance_type = "t2.nano" 27 | } 28 | ``` 29 | 30 | The state should render empty output now that you remove the EC2 instance. 31 | 32 | ``` 33 | $ terraform show 34 | ``` 35 | 36 | With the empty state as a starting point, we'll try to import the EC2 instance that still exists on AWS. As you can see from the output, Terraform will want you to add the configuration first. 37 | 38 | ``` 39 | $ terraform import aws_instance.app_server i-0651c2edd7dc884ac 40 | Error: resource address "aws_instance.app_server" does not exist in the configuration. 41 | 42 | Before importing this resource, please create its configuration in the root module. For example: 43 | 44 | resource "aws_instance" "app_server" { 45 | # (resource arguments) 46 | } 47 | ``` 48 | 49 | Add the following configuration to `main.tf`. Do not define any additional resource arguments yet. 50 | 51 | ```terraform 52 | resource "aws_instance" "app_server" { 53 | } 54 | ``` 55 | 56 | Run the `import` command again. Terraform will add the EC2 instance to the local state file. 57 | 58 | ``` 59 | $ terraform import aws_instance.app_server i-0651c2edd7dc884ac 60 | aws_instance.app_server: Importing from ID "i-0651c2edd7dc884ac"... 61 | aws_instance.app_server: Import prepared! 62 | Prepared aws_instance for import 63 | aws_instance.app_server: Refreshing state... [id=i-0651c2edd7dc884ac] 64 | 65 | Import successful! 66 | 67 | The resources that were imported are shown above. These resources are now in 68 | your Terraform state and will henceforth be managed by Terraform. 69 | ``` 70 | 71 | You can verify that the instance is available with the following command. 72 | 73 | ``` 74 | $ terraform state show aws_instance.app_server 75 | # aws_instance.app_server: 76 | resource "aws_instance" "app_server" { 77 | ... 78 | } 79 | ``` 80 | 81 | Running the `plan` command will tell you about the attributes that still need to be defined in the configuration file. 82 | 83 | ``` 84 | $ terraform plan 85 | ╷ 86 | │ Error: Missing required argument 87 | │ 88 | │ with aws_instance.app_server, 89 | │ on main.tf line 19, in resource "aws_instance" "app_server": 90 | │ 19: resource "aws_instance" "app_server" { 91 | │ 92 | │ "instance_type": one of `instance_type,launch_template` must be specified 93 | ╵ 94 | ╷ 95 | │ Error: Missing required argument 96 | │ 97 | │ with aws_instance.app_server, 98 | │ on main.tf line 19, in resource "aws_instance" "app_server": 99 | │ 19: resource "aws_instance" "app_server" { 100 | │ 101 | │ "ami": one of `ami,launch_template` must be specified 102 | ╵ 103 | ╷ 104 | │ Error: Missing required argument 105 | │ 106 | │ with aws_instance.app_server, 107 | │ on main.tf line 19, in resource "aws_instance" "app_server": 108 | │ 19: resource "aws_instance" "app_server" { 109 | │ 110 | │ "launch_template": one of `ami,instance_type,launch_template` must be specified 111 | ``` 112 | 113 | Define the attributes for the resource in `main.tf`. 114 | 115 | ```terraform 116 | resource "aws_instance" "app_server" { 117 | ami = "ami-077ee47512dc6f3ca" 118 | instance_type = "t2.nano" 119 | } 120 | ``` 121 | 122 | The `plan` command will be satisified with the current state. 123 | 124 | ``` 125 | $ terraform plan 126 | aws_instance.app_server: Refreshing state... [id=i-0651c2edd7dc884ac] 127 | 128 | No changes. Your infrastructure matches the configuration. 129 | 130 | Terraform has compared your real infrastructure against your configuration and found no 131 | differences, so no changes are needed. 132 | ``` 133 | 134 | The `apply` command will not need to make any additional changes. 135 | 136 | ``` 137 | $ terraform apply 138 | aws_instance.app_server: Refreshing state... [id=i-0651c2edd7dc884ac] 139 | 140 | No changes. Your infrastructure matches the configuration. 141 | 142 | Terraform has compared your real infrastructure against your configuration and found no 143 | differences, so no changes are needed. 144 | 145 | Apply complete! Resources: 0 added, 0 changed, 0 destroyed. 146 | ``` 147 | 148 | The `destroy` command will delete the instance. 149 | 150 | ``` 151 | terraform destroy 152 | aws_instance.app_server: Refreshing state... [id=i-0651c2edd7dc884ac] 153 | 154 | Terraform used the selected providers to generate the following execution plan. Resource actions 155 | are indicated with the following symbols: 156 | - destroy 157 | ... 158 | aws_instance.app_server: Destroying... [id=i-0651c2edd7dc884ac] 159 | aws_instance.app_server: Still destroying... [id=i-0651c2edd7dc884ac, 10s elapsed] 160 | aws_instance.app_server: Still destroying... [id=i-0651c2edd7dc884ac, 20s elapsed] 161 | aws_instance.app_server: Still destroying... [id=i-0651c2edd7dc884ac, 30s elapsed] 162 | aws_instance.app_server: Destruction complete after 40s 163 | 164 | Destroy complete! Resources: 1 destroyed. 165 | ``` 166 | -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/instructions.md: -------------------------------------------------------------------------------- 1 | # Exercise 16 2 | 3 | In this exercise, you will put everything you learned so far together. You'll create the infrastructure pieces that run the Apache web server on AWS. The web server should become accessible from the internet. 4 | 5 | 1. Define the AWS provider with the latest version in the 4.x range. 6 | 2. Create the following resources and tie them together properly. 7 | * A VPC using the [`aws_vpc`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) resource. Configure the CIDR to `178.0.0.0/16`. 8 | * A VPC Internet Gateway using the [`aws_internet_gateway`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) resource. 9 | * A VPC subnet resource using the [`aws_subnet`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) resource. Set the availability zone to `us-west-2a` and map the public IP on launch. 10 | * A VPC routing table using the [`aws_route_table`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) resource. Set the CIDR to `0.0.0.0/0`. 11 | * A resource to create an association between a route table and a subnet or a route table and an internet gateway or virtual private gateway using [`aws_route_table_association`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association). 12 | * A security group resource using [`aws_security_group`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group). Configure SSH and HTTP as ingress. Disallow any egress traffic. 13 | * An EC2 instance using the [`aws_instance`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) resource. Use the AMI `ami-0d70546e43a941d70`, the EC2 type `t2.micro`, and wire up the networking resources. Install the Apache web server on the EC2 instance. 14 | 3. Make the configuration changeable by the end user via input variables. Assign default values. 15 | 4. Define an output value that renders the public IP of the EC2 instance on the console. 16 | 5. Deploy the infrastructure using Terraform. 17 | 6. Open a browser and render the Apache web server default web page. 18 | 7. Destroy the infrastructure with Terraform. -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/solution/imgs/apache-default-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuschko/cta-crash-course/8c9f1e2cc773af70587fe253a31fde54135cfd70/exercises/16-internet-accessible-webserver/solution/imgs/apache-default-page.png -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/solution/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = var.region 12 | profile = var.profile_name 13 | } 14 | 15 | resource "aws_vpc" "app_vpc" { 16 | cidr_block = var.vpc_cidr 17 | 18 | tags = { 19 | Name = "app_vpc" 20 | } 21 | } 22 | 23 | resource "aws_internet_gateway" "igw" { 24 | vpc_id = aws_vpc.app_vpc.id 25 | 26 | tags = { 27 | Name = "vpc_igw" 28 | } 29 | } 30 | 31 | resource "aws_subnet" "public_subnet" { 32 | vpc_id = aws_vpc.app_vpc.id 33 | cidr_block = var.public_subnet_cidr 34 | map_public_ip_on_launch = true 35 | availability_zone = "us-west-2a" 36 | 37 | tags = { 38 | Name = "public_subnet" 39 | } 40 | } 41 | 42 | resource "aws_route_table" "public_rt" { 43 | vpc_id = aws_vpc.app_vpc.id 44 | 45 | route { 46 | cidr_block = "0.0.0.0/0" 47 | gateway_id = aws_internet_gateway.igw.id 48 | } 49 | 50 | tags = { 51 | Name = "public_rt" 52 | } 53 | } 54 | 55 | resource "aws_route_table_association" "public_rt_asso" { 56 | subnet_id = aws_subnet.public_subnet.id 57 | route_table_id = aws_route_table.public_rt.id 58 | } 59 | 60 | resource "aws_instance" "web" { 61 | ami = "ami-0d70546e43a941d70" 62 | instance_type = var.instance_type 63 | subnet_id = aws_subnet.public_subnet.id 64 | security_groups = [aws_security_group.sg.id] 65 | 66 | user_data = <<-EOF 67 | #!/bin/bash 68 | echo "*** Installing apache2" 69 | sudo apt update -y 70 | sudo apt install apache2 -y 71 | echo "*** Completed Installing apache2" 72 | EOF 73 | 74 | tags = { 75 | Name = "web_instance" 76 | } 77 | } -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/solution/outputs.tf: -------------------------------------------------------------------------------- 1 | output "web_instance_ip" { 2 | value = aws_instance.web.public_ip 3 | } -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/solution/sg.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "sg" { 2 | name = "allow_ssh_http" 3 | description = "Allow ssh http inbound traffic" 4 | vpc_id = aws_vpc.app_vpc.id 5 | 6 | ingress { 7 | description = "SSH from VPC" 8 | from_port = 22 9 | to_port = 22 10 | protocol = "tcp" 11 | cidr_blocks = ["0.0.0.0/0"] 12 | ipv6_cidr_blocks = ["::/0"] 13 | } 14 | 15 | ingress { 16 | description = "HTTP from VPC" 17 | from_port = 80 18 | to_port = 80 19 | protocol = "tcp" 20 | cidr_blocks = ["0.0.0.0/0"] 21 | ipv6_cidr_blocks = ["::/0"] 22 | } 23 | 24 | egress { 25 | from_port = 0 26 | to_port = 0 27 | protocol = "-1" 28 | cidr_blocks = ["0.0.0.0/0"] 29 | ipv6_cidr_blocks = ["::/0"] 30 | } 31 | 32 | tags = { 33 | Name = "allow_ssh_http" 34 | } 35 | } -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/solution/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | You can find the relevant Terraform configuration files in the [current directory](.). 4 | 5 | Deploy the infrastructure. The public IP address will be rendered in the console. 6 | 7 | ``` 8 | $ terraform apply 9 | ... 10 | Apply complete! Resources: 7 added, 0 changed, 0 destroyed. 11 | 12 | Outputs: 13 | 14 | web_instance_ip = "34.209.48.125" 15 | ``` 16 | 17 | You should be able to access the Apache web server after the a couple of seconds. In the output above, the public IP address is `34.209.48.125`. 18 | 19 | ![apache-default-page](./imgs/apache-default-page.png) 20 | 21 | Destroy all resources once you are done. 22 | 23 | ``` 24 | $ terraform destroy 25 | ``` -------------------------------------------------------------------------------- /exercises/16-internet-accessible-webserver/solution/variables.tf: -------------------------------------------------------------------------------- 1 | variable "profile_name" { 2 | type = string 3 | default = "default" 4 | } 5 | 6 | variable "region" { 7 | type = string 8 | default = "us-west-2" 9 | } 10 | 11 | variable "instance_type" { 12 | type = string 13 | default = "t2.micro" 14 | } 15 | 16 | variable "vpc_cidr" { 17 | type = string 18 | default = "178.0.0.0/16" 19 | } 20 | 21 | variable "public_subnet_cidr" { 22 | type = string 23 | default = "178.0.10.0/24" 24 | } -------------------------------------------------------------------------------- /prerequisites/instructions.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | Exercises will require the tooling listed below. Ensure that all of those tools have been installed before attending the training if you want to follow along. The training does not reserve time for setting up or verifying the installed tools or their respective versions. 4 | 5 | ## Technical Requirements 6 | 7 | * A computer with [Terraform](https://www.terraform.io/downloads) installed. Should you have to manage multiple Terraform versions on a single machine in parallel, the tool [tfswitch](https://tfswitch.warrensbox.com/) might come in handy. 8 | * A code editor to write and modify configuration files; instructor will use [VSCode](https://code.visualstudio.com/download) with the [Terraform extension](https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform) installed (other IDEs, like IDEA with the Terraform plugin installed, may work as well) 9 | * A [Git client](https://git-scm.com/downloads) to interact with a GitHub repository 10 | * An [AWS account](https://aws.amazon.com/) and credentials for creating or modifying resources during the exercises. _Note:_ Running Terraform commands for AWS to create resources may accrue a small cost. You can opt out of running the command during the exercise. 11 | 12 | ## Skills 13 | 14 | * An understanding of basic programming concepts (e.g., variables and functions) 15 | * The need for defining infrastructure as code 16 | * Familiarity with Unix command-line tools like wget, curl, or tar 17 | -------------------------------------------------------------------------------- /slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuschko/cta-crash-course/8c9f1e2cc773af70587fe253a31fde54135cfd70/slides.pdf --------------------------------------------------------------------------------