If you have ever clicked through the AWS Management Console to launch an EC2 instance, configure an S3 bucket, or set up a VPC, you know the process can be repetitive and error-prone. One wrong click, and you might launch an instance in the wrong region or forget to tag a resource. Infrastructure as Code, or IaC, solves this by letting you define your cloud resources in human-readable configuration files. Terraform, created by HashiCorp, is one of the most popular IaC tools available today. In the previous section, we covered how to set up Terraform on Ubuntu 24.04.

Step 1: Understand the Core Terraform Workflow and Project Structure
Before you write a single line of code, it helps to grasp the mental model behind Terraform. The tool operates on a declarative model: you describe the desired end state of your infrastructure, and Terraform figures out how to reach that state. This is different from imperative scripts where you list every action in order. The core Terraform workflow consists of four commands that you will use in almost every project, whether you are creating a single S3 bucket or a sprawling multi-region architecture.
These four commands are terraform init, terraform plan, terraform apply, and terraform destroy. Think of them as a cycle: initialize the project, preview the changes, apply the changes, and then tear everything down when you no longer need it. This predictable pattern is what makes Terraform so reliable for teams and individuals alike. According to HashiCorp’s 2023 State of Cloud Strategy Survey, about 78% of organizations using IaC reported fewer deployment failures compared to manual provisioning methods. That statistic underscores why mastering these terraform infrastructure steps matters for anyone working with cloud services.
Your project directory will contain files with a .tf extension. These files are written in HCL, or HashiCorp Configuration Language. The syntax is straightforward. A typical block looks like this:
block_type "label" "name" {
argument = "value"
}
There are three main block types you will encounter early on. The terraform block holds global settings like the required provider versions. The provider block tells Terraform which cloud platform to talk to, such as AWS, Azure, or GCP. The resource block defines the actual infrastructure component you want to create, like an S3 bucket or an EC2 instance. Understanding this structure is the foundation for every subsequent step in the terraform infrastructure steps workflow.
Step 2: Write Your First Terraform Configuration File
Now it is time to put theory into practice. Create a new directory for your project. You can call it terraform-project or anything descriptive. Inside that directory, create a file named main.tf or something similar like mns3.tf. The name does not matter as long as it ends with .tf. Open this file in your favorite text editor, whether that is nano, vim, or VS Code.
Start by defining the terraform block. This tells Terraform which version of the tool you expect and which providers you need. For AWS, the provider source is hashicorp/aws. Specifying a version like ~> 6.0 means Terraform will use any 6.x release but not jump to 7.0 automatically. This prevents unexpected breaking changes from disrupting your workflow.
Next, add a provider block for AWS. Choose a region that is geographically close to you or your users. For example, ap-southeast-1 is Singapore, which works well for audiences in Southeast Asia. The region setting here will apply to all resources in this configuration unless you override it at the resource level.
Now for the fun part: defining resources. Let us create two S3 buckets. One will store application logs, and the other will hold backups. Each resource block needs a unique label within your configuration so Terraform can reference it. For example, aws_s3_bucket.logs and aws_s3_bucket.backups. Inside each block, you set the bucket name and a few tags. Tags are metadata that help you organize and identify resources. A typical set includes Name, Environment, and Purpose.
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-shoeshop-2026"
tags = {
Name = "Application Logs"
Environment = "Development"
Purpose = "Logging"
}
}
Here is a critical detail: S3 bucket names must be globally unique across all AWS accounts. If you try to use a name that someone else already owns, Terraform will throw an error when you run apply. So pick a name that combines your project name, a purpose, and a year or random suffix. For example, my-app-logs-shoeshop-2026 is unlikely to conflict with another account.
Finally, add output blocks. Outputs let you extract information from your infrastructure after creation, such as the bucket ID or ARN. This is useful for passing values to other configurations or simply for verification.
output "logs_bucket_id" {
value = aws_s3_bucket.logs.id
}
Writing this configuration file is the second of the five terraform infrastructure steps. It translates your infrastructure requirements into a repeatable, version-controlled format. Once you save the file, you are ready to move to the next phase.
Step 3: Initialize the Project with terraform init
With your configuration file in place, open your terminal and navigate to the project directory. The first command you run will be terraform init. This command does not create any infrastructure yet. Instead, it prepares your working directory by downloading the necessary provider plugins. For the AWS provider, Terraform will fetch the plugin binary and store it in a hidden directory called .terraform. It also creates a lock file named .terraform.lock.hcl that records the exact version of the provider you downloaded.
Why is this step important? Provider plugins are essentially the bridge between Terraform and the cloud provider’s API. Without them, Terraform would not know how to talk to AWS. Running init also initializes the backend, which is where Terraform stores its state data. By default, the state is stored locally as a file named terraform.tfstate in your project directory.
You only need to run terraform init once per project, or whenever you add a new provider to your configuration. If you change the provider version constraint in your terraform block, you may need to re-run init to download the updated plugin. Think of this step as installing the tools before you start building. It is a one-time setup that makes all subsequent terraform infrastructure steps possible.
After running terraform init, you should see output confirming that the provider was downloaded and that Terraform has been initialized successfully. You can verify this by listing the contents of your directory with ls -la and spotting the .terraform folder and the lock file.
Step 4: Preview and Apply Your Infrastructure
Now comes the moment you have been waiting for. Run terraform plan. This command reads your configuration file, checks the current state (if any exists), and outputs a detailed summary of what actions Terraform will take. You will see a plus sign (+) next to resources that will be created, a tilde (~) for resources that will be modified, and a minus sign (–) for resources that will be destroyed. Some attribute values may show (known after apply), which means Terraform cannot know the value until AWS actually creates the resource. For example, the ARN of an S3 bucket is generated by AWS and not known at plan time.
Review the plan carefully. This is your safety net. Did you accidentally set the bucket name to something that already exists? Did you forget to add a tag? The plan output gives you a chance to catch errors before they affect your live environment. In a team setting, you might share this plan output with colleagues for code review before applying. This practice is one of the key reasons teams adopt IaC in the first place.
Once you are satisfied with the plan, run terraform apply. Terraform will show you the same plan again and ask for confirmation. Type yes and press Enter. The tool will then make API calls to AWS to create the resources defined in your configuration. You will see progress indicators as each resource is created. When the process finishes, you will see a success message along with any output values you defined, such as the bucket IDs.
You may also enjoy reading: 5 Ways China Earns $500M Per Hour from AI Exports.
Congratulations! You have just created AWS infrastructure entirely from the command line using code. This is the fourth of the five terraform infrastructure steps, and it is the most rewarding. To verify, log into the AWS Management Console, navigate to the S3 service, and you should see your two buckets listed with the tags you specified. You can also check the terraform.tfstate file that was created in your project directory. Open it with a text editor to see the JSON representation of your infrastructure, including the bucket ARN, region, and other metadata.
This state file is Terraform’s memory. It maps the resources in your configuration to the real-world objects in AWS. When you run terraform plan or apply again, Terraform reads this state file to understand what already exists and then compares it with your configuration to determine what changes are needed. Without the state file, Terraform would have no way of knowing that it created those buckets, and it would try to create them again, causing errors.
Step 5: Clean Up Resources with terraform destroy
Infrastructure costs money, even when you are not using it. If you created these S3 buckets for a tutorial or a test, you should remove them when you are done to avoid unexpected charges. The fifth and final step in the terraform infrastructure steps is to run terraform destroy.
This command works similarly to apply but in reverse. Terraform reads your state file, identifies all the resources it manages, and shows you a plan of what will be destroyed. Just like with apply, you must type yes to confirm. Once confirmed, Terraform will delete the resources in the correct order, respecting dependencies. For example, if you had an S3 bucket that contained objects, Terraform would first empty the bucket and then delete it (assuming you have the appropriate configuration for force destruction).
After the destroy command completes, your AWS account will be clean of those resources. The state file will also be updated to reflect that no resources exist. You can verify by checking the AWS Console again or by running terraform state list, which should return an empty list.
One common question is whether you can recover resources after running destroy. The answer is no, not through Terraform alone. The state file no longer references those resources, and the actual cloud resources are gone. That is why it is crucial to review the destroy plan carefully before confirming. In production environments, teams often restrict who can run destroy commands and require additional approvals.
Why State Management Matters in Real-World Projects
In a solo tutorial, storing the state file locally is fine. But in a team setting, you need a shared remote backend. If two people run apply from different machines with separate state files, they will overwrite each other’s changes. This leads to drift and confusion. Terraform supports remote backends like AWS S3 (with DynamoDB for locking) or Terraform Cloud. These backends store the state file centrally and lock it during operations so only one person can make changes at a time.
Understanding state is not just a technical detail; it is a core concept that affects how you collaborate. When you move from a single developer project to a team of five or ten, the way you manage state becomes as important as the configuration itself.
Expanding Beyond S3: Adding an EC2 Instance
Once you are comfortable with the five terraform infrastructure steps, you can expand your configuration to include other resources. For example, adding an EC2 instance is a natural next step. You would define a new resource block for aws_instance, specify an Amazon Machine Image (AMI) ID, an instance type like t2.micro, and a subnet ID. You would also likely need a security group resource to allow SSH or HTTP traffic. The same workflow applies: write the configuration, run init, plan, apply, and destroy.
Adding more resources introduces dependencies. For instance, an EC2 instance depends on a security group. Terraform automatically handles these dependencies by analyzing the references in your configuration. If you reference aws_security_group.my_sg.id inside your EC2 instance block, Terraform knows to create the security group first. This dependency resolution is one of Terraform’s most powerful features.
A Word on Best Practices
As you adopt Terraform for real projects, keep a few best practices in mind. First, always version your configuration files using Git. This gives you a history of changes and makes it easy to roll back if something goes wrong. Second, use variables instead of hardcoding values. Variables make your configuration reusable across different environments like development, staging, and production. Third, run terraform fmt to automatically format your code so it follows community standards. Finally, never edit the state file manually. If you need to make changes to state, use the terraform state subcommands, which are designed for that purpose.
These practices will save you from countless headaches as your infrastructure grows. They also make it easier for other team members to understand and contribute to your codebase.
By following these five terraform infrastructure steps — understanding the workflow, writing your configuration, initializing the project, applying changes, and cleaning up — you have built a solid foundation for managing cloud infrastructure with code. The same pattern applies whether you are creating a single S3 bucket or orchestrating a multi-service application on AWS. Each step builds on the previous one, forming a repeatable cycle that brings predictability and control to cloud operations.






