In this lab we’ll be learning about HashiCorp Terraform, an Infrastructure as Code (IaC) language that enables us to build and maintain infrastructure via code. We’ll start by setting up our development environment and then build a static website hosted on an AWS S3 bucket.
This lab was created by Tyler Petty for Cybr, and you can view the full code and repository here for reference.
Pre-requisites
Completing this lab will require that you have a few tools installed:
- Terraform – installation instructions (on MacOS I recommend using Homebrew)
- AWS CLI – installation instructions
- Visual Studio Code (optional) – installation instructions
What is Infrastructure as Code (IaC)?
Infrastructure as Code (IaC) enables creating and managing infrastructure like virtual machines, networks, and more using code. So, for example, instead of going into the AWS console and manually creating dozens of resources, you can write code to do this.
By using IaC, your infrastructure now goes through the same software development lifecycle as any application code should. This means you can version control your infrastructure, automate deployments and updates, identify and remediate security issues prior to deployment, and more.
HashiCorp’s Terraform is arguably the most popular IaC tool, is open-source, and supports thousands of industry solutions like AWS, Azure, Kubernetes, Proxmox, Okta, and more. Several other IaC solutions do exist however including AWS CloudFormation, Pulumi, and OpenTofu.
Terraform Providers
To build infrastructure on AWS or other solutions, Terraform relies on Providers. These are essentially plugins that translate Terraform code into API calls for a service. So all you have to worry about is writing Terraform code and the provider will handle the rest.
Let’s take a look at the Providers available here and the differences between the tiers (Offical, Partner, and Community). The key thing to remember is that anyone can create a provider so it’s important that you trust the provider you’re using.
Looking at the AWS provider, we can see a few key things.
- Official – This is the official AWS provider maintained by HashiCorp. It’s the most trusted and most up-to-date provider.
- Version – This is the version of the provider. It’s important to keep your provider up-to- date to ensure you have the latest features and bug fixes.
- Source Code – This is the link to the source code of the provider. This is useful if you want to see how the provider works or if you want to contribute to the provider.
- Provider – Provides sample code for using the provider in your Terraform code.
- Documentation – Provides documentation on the various AWS resources that can be created.
Configuring credentials
Once you’ve started the lab, you will receive AWS credentials. Use those credentials to configure your AWS CLI:
aws configure --profile lab
You can leave off the profile name if you’d rather. Up to you. If you use a profile name, you will need to add it in your terraform main.tf
file so that Terraform knows which credentials to use.
Building Our First AWS Resource
1. Create a new directory for your Terraform code
We’ll need a place to store our Terraform code so let’s create a new directory for it.
mkdir awsWorkshop
2. Create a new Terraform file
Navigate to the directory you just created and create a new file called main.tf . The command below will open Visual Studio Code with the new file.
code main.tf
Code language: CSS (css)
Pro tip: If you don’t have the code shortcut set up, you can open Visual Studio Code, hit CMD + SHIFT + P
, search for Shell Command: Install 'code' command in PATH
, and hit enter.
3. Codify an AWS S3 Bucket
AWS S3 is a simple resource that enables storing large amounts of data in a cost-effective manner. By default, S3 buckets are private but can be made public if needed. We’ll start by creating a private bucket and then move on to making it public and hosting a static website.
Copy the below code into your main.tf file. Let’s reference the documentation for the AWS S3 bucket resource here.
provider "aws" {
region = "us-east-1"
# profile = "lab"
}
resource "aws_s3_bucket" "bucket" {
bucket = "<adduniquebucketnamehere>"
}
Code language: PHP (php)
(Bucket names must be lower case!)
You will need to uncomment the # profile = "lab"
line by removing the #
if you used a profile name when you configured your CLI. For example, if you did --profile lab
, you will need to set the profile value equal to "lab"
. This is what tells Terraform where to find your AWS credentials.
By default, Terraform will look for the AWS credentials in the ~/.aws/credentials
file. If you have multiple profiles, you can specify the profile in the provider block. Otherwise, don’t include the profile line.
4. Deploying the Terraform Code
Let’s work through a few Terraform commands to deploy our code.
Initialize
terraform init
– Initializes the directory with the necessary plugins and modules.
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.57.0...
- Installed hashicorp/aws v5.57.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record
the provider
selections it made above. Include this file in your version
control repository
so that Terraform can guarantee to make the same selections by
default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform
plan" to see
any changes that are required for your infrastructure. All
Terraform commands
should now work.
If you ever set or change modules or backend configuration for
Terraform,
rerun this command to reinitialize your working directory. If
you forget, other
commands will detect it and remind you to do so if necessary.
Code language: PHP (php)
This creates some hidden files and directories within your project directory.
.terraform
.terraform.lock.hcl
Code language: CSS (css)
The .terraform
directory contains the plugins and modules that Terraform needs to run. The .terraform.lock.hcl
file contains the versions of the plugins and modules that were installed.
Validate
terraform validate
– Checks the syntax of the code and ensures that the code is valid. It will only check for Terraform related issues and may not account for AWS specific issues.
terraform validate
Success! The configuration is valid.
If you have an issue with your code, you’ll see an error message here. For example, I’ve added a fake argument to the aws_s3_bucket
resource.
resource "aws_s3_bucket" "bucket" {
bucket = "tylers-bucket-2394829348239480983"
not_a_real_argument = "this is not a real argument"
}
Code language: JavaScript (javascript)
terraform validate
will return the following error:
terraform validate
╷
│ Error: Unsupported argument
│
│ on main.tf line 8, in resource "aws_s3_bucket" "bucket":
│ 8: not_a_real_argument = "this is not a real argument"
│
│ An argument named "not_a_real_argument" is not expected here.
Code language: JavaScript (javascript)
Format
terraform fmt
– Formats the code to ensure it’s properly aligned and consistent. If you set up the HashiCorp Terraform plugin for Visual Studio Code, it will automatically format your code when you save the file therefore you don’t need to run this command.
It will turn this:
resource "aws_s3_bucket" "bucket" {
bucket = "tylers-bucket-2394829348239480983"
not_a_real_argument = "this is not a real argument"
}
Code language: JavaScript (javascript)
Into this:
resource "aws_s3_bucket" "bucket" {
bucket = "tylers-bucket-2394829348239480983"
not_a_real_argument = "this is not a real argument"
}
Code language: JavaScript (javascript)
Plan
terraform plan
– Shows you what Terraform will do when you run terraform apply
. It will show you the resources that will be created, updated, or destroyed.
terraform plan
Terraform used the selected providers to generate the following
execution plan. Resource actions are indicated with the
following
symbols:
+ create
Terraform will perform the following actions:
# aws_s3_bucket.bucket will be created
+ resource "aws_s3_bucket" "bucket" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "tylers-bucket-
2394829348239480983"
+ bucket_domain_name = (known after apply)
+ bucket_prefix = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ tags_all = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Code language: PHP (php)
Apply
terraform apply
– Applies the changes to your infrastructure. This is where Terraform will create the resources you’ve defined in your code.
terraform apply
Terraform used the selected providers to generate the following
execution plan. Resource actions are indicated with the
following
symbols:
+ create
Terraform will perform the following actions:
# aws_s3_bucket.bucket will be created
+ resource "aws_s3_bucket" "bucket" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "tylers-bucket-
2394829348239480983"
+ bucket_domain_name = (known after apply)
+ bucket_prefix = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ tags_all = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_s3_bucket.bucket: Creating...
aws_s3_bucket.bucket: Creation complete after 2s [id=tylers-
bucket-2394829348239480983]
Code language: PHP (php)
If you get this error:
Error: creating S3 Bucket (addUniqueBucketNameHere-cybr): operation error S3: CreateBucket, https response error StatusCode: 400, RequestID: SQ7K46G1YVVWB68T, HostID: cSnHt4/0Ju0d4sFIRrngGPn94WuovN91sPeOFib8rUmFWdnDqcmvYHlkhPcsp1IKFn5v6JUgcu0=, api error InvalidBucketName: The specified bucket is not valid.
│
│ with aws_s3_bucket.bucket,
│ on main.tf line 6, in resource "aws_s3_bucket" "bucket":
│ 6: resource "aws_s3_bucket" "bucket" {
│
╵
Code language: JavaScript (javascript)
It’s because the bucket name you chose is invalid. Try a different name.
5. Validating Deployment
Once we get confirmation from Terraform that the bucket has been created, we can leverage the AWS CLI to see this.
aws s3 ls
2024-07-07 09:08:50 tylers-bucket-2394829348239480983
Code language: CSS (css)
Creating an AWS S3 Static Website
Now that we have an S3 bucket, let’s turn it into a static website. The first thing we need is an HTML file to host. Copy the code below into a file called index.html in the same directory as your main.tf file. Repalce “Your Name” with your name.
1. Creating the website file
<!DOCTYPE html>
<html>
<head>
<title>"Your Name" S3 Website</title>
</head>
<body>
<h1>"Your Name" S3 Website</h1>
</body>
</html>
Code language: HTML, XML (xml)
2. Enabling S3 public access
We need to update our S3 bucket policy to allow anyone to access it. This is controlled via an S3 bucket resource policy.
Update your main.tf file with the below code.
# allow public read access to all objects in the bucket
resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject",
Effect = "Allow",
Principal = "*",
Action = "s3:GetObject",
Resource = "${aws_s3_bucket.bucket.arn}/*",
},
],
})
}
Code language: PHP (php)
Now, make the bucket objects public.
# make the bucket publicly accessible
resource "aws_s3_bucket_public_access_block" "public_access_block" {
bucket = aws_s3_bucket.bucket.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
Code language: PHP (php)
3. Enabling the website configuration
Next, we need to upload our HTML file and enable the website configuration on the S3 bucket. This will allow us to host the static website.
# upload html file to the bucket
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.bucket.id
key = "index.html" # the file name in the bucket
source = "index.html" # the file to upload
content_type = "text/html" # set the content type so the browser knows how to render the file
source_hash = filemd5("index.html") # enables automatic updates when the file changes
}
# configure the bucket as a website
resource "aws_s3_bucket_website_configuration" "website_config"
{
bucket = aws_s3_bucket.bucket.id
index_document {
suffix = "index.html"
}
}
Code language: PHP (php)
Finally, we can add an output resource to display the website URL.
# output the website URL
output "website_url" {
value = aws_s3_bucket_website_configuration.website_config.website_endpo
int
}
Code language: PHP (php)
4. Deploying the website
Run terraform apply
to deploy the changes.
terraform apply
[...SNIP...]
aws_s3_bucket.bucket: Creating...
aws_s3_bucket.bucket: Creation complete after 1s [id=tylers- bucket-2394829348239480983]
aws_s3_bucket_policy.bucket_policy: Creating...
aws_s3_bucket_website_configuration.website_config: Creating...
aws_s3_bucket_public_access_block.public_access_block: Creating...
aws_s3_object.index: Creating...
aws_s3_bucket_public_access_block.public_access_block: Creation complete after 1s [id=tylers-bucket-2394829348239480983]
aws_s3_bucket_website_configuration.website_config: Creation complete after 1s [id=tylers-bucket-2394829348239480983]
aws_s3_object.index: Creation complete after 1s [id=index.html]
aws_s3_bucket_policy.bucket_policy: Creation complete after 1s[id=tylers-bucket-2394829348239480983]
Apply complete! Resources: 5 added, 0 changed, 0 destroyed. Outputs: website_url = "tylers-bucket-2394829348239480983.s3-website-us- east-1.amazonaws.com"
Code language: JavaScript (javascript)
5. Validating the website
Navigate to the website URL outputted by Terraform to see your website.
Destroying the Infrastructure
When you’re done with your infrastructure, you can destroy it using terraform destroy . It’s important to do this to avoid unnecessary costs. For an S3 bucket, you pay for the storage and data transfer costs. See the pricing here for more information.
Destroy
terraform destroy
– Destroys the infrastructure you’ve created.
terraform destroy
aws_s3_bucket.bucket: Refreshing state... [id=tylers-bucket-
2394829348239480983]
[...SNIP...]
Destroy complete! Resources: 5 destroyed.
Conclusion
Congratulations! You’ve just used Infrastructure as Code (IaC) with Terraform to create, and later destroy, an Amazon S3 static website.
This was awesome! I’m looking to learn more and build more things!!
You came to the right place! Happy learning!
This was fun. Looking forward to more IaC and maybe some CI/CD 😉
Excellent lab!
Glad you liked it! Thanks for the feedback!