Terraform Level 1, Task 22: Managing CloudFormation Stacks with Terraform¶
Today's task was a fascinating dive into the world of "meta" Infrastructure as Code. My objective was to use Terraform to create and manage an AWS CloudFormation stack. This was a mind-bending concept at first: using one IaC tool to control another. The CloudFormation stack itself was simple—it just needed to create an S3 bucket with versioning enabled.
This was an incredibly insightful exercise. It taught me about AWS's native IaC service (CloudFormation) and how Terraform can act as a wrapper around it, allowing teams to manage their entire cloud footprint with a single tool, even if some parts are defined in different languages. This document is my very detailed, first-person guide to that entire process.
Table of Contents¶
The Task¶
My objective was to use Terraform to deploy an AWS CloudFormation stack. The specific requirements were:
1. Create a CloudFormation stack named nautilus-stack.
2. The stack's template must define a single resource: an S3 bucket named nautilus-bucket-431.
3. This S3 bucket must have versioning enabled.
4. All Terraform code had to be in a single main.tf file in the us-east-1 region.
My Step-by-Step Solution¶
The process involved writing a Terraform file that defined the aws_cloudformation_stack resource, which included an embedded JSON template for the stack itself.
Phase 1: Writing the Code¶
In the /home/bob/terraform directory, I created my main.tf file. I wrote the following declarative code.
# 1. Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
# 2. Define the CloudFormation Stack Resource
resource "aws_cloudformation_stack" "nautilus_stack_resource" {
name = "nautilus-stack"
# The 'template_body' argument contains the entire CloudFormation template.
# I'm using a 'heredoc' string (<<-EOT ... EOT) to write a multi-line JSON template.
template_body = <<-EOT
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "CloudFormation stack for an S3 bucket with versioning",
"Resources": {
"NautilusBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "nautilus-bucket-431",
"VersioningConfiguration": {
"Status": "Enabled"
}
}
}
}
}
EOT
tags = {
Name = "nautilus-stack"
}
}
Phase 2: The Terraform Workflow¶
From my terminal in the same directory, I executed the three core commands.
1. Initialize: terraform init.
2. Plan: terraform plan. The output showed that Terraform would create one aws_cloudformation_stack resource.
3. Apply: terraform apply. After I confirmed with yes, Terraform sent the template to the CloudFormation service, which then created the S3 bucket. The success message confirmed the task was done.
Why Did I Do This? (The "What & Why")¶
- AWS CloudFormation: This is AWS's native Infrastructure as Code service. Like Terraform, it allows you to define your cloud resources in a template file (written in JSON or YAML). A collection of resources created from a single template is called a stack.
- Terraform managing CloudFormation: This was the "meta" part. Why would I use one IaC tool to manage another? In a large organization, this is a common scenario.
- Legacy Infrastructure: A company might have hundreds of existing CloudFormation stacks but has decided to use Terraform for all new infrastructure. Terraform can manage these legacy stacks, providing a single, unified workflow.
- Unsupported Resources: Sometimes, a brand new AWS service is released and has CloudFormation support before the Terraform AWS provider is updated. Using the aws_cloudformation_stack resource allows a Terraform user to provision that new service immediately.
- S3 Bucket Versioning: This is a critical data protection feature for S3. When versioning is enabled on a bucket, S3 saves every version of an object.
- If I overwrite a file, S3 keeps the old version.
- If I delete a file, S3 doesn't actually delete it; it just places a "delete marker" on it, and I can still recover the previous version.
This is an essential safety net to protect against accidental deletions or data corruption.
Deep Dive: A Line-by-Line Explanation of My main.tf Script¶
This script shows how to embed a CloudFormation template directly inside a Terraform resource.
[Image of Terraform managing a CloudFormation stack]
# Standard provider configuration block.
provider "aws" {
region = "us-east-1"
}
# This is the resource block that defines my CloudFormation Stack.
# "aws_cloudformation_stack" is the Resource TYPE.
# "nautilus_stack_resource" is the local NAME I use to refer to this stack.
resource "aws_cloudformation_stack" "nautilus_stack_resource" {
# The 'name' argument sets the unique name for the stack within my AWS account and region.
name = "nautilus-stack"
# The 'template_body' argument takes the entire CloudFormation template as a string.
# The '<<-EOT' syntax is called a heredoc. It's a convenient way to write a
# multi-line string in Terraform without messy quotes and escape characters.
template_body = <<-EOT
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "CloudFormation stack for an S3 bucket with versioning",
# 'Resources' is the main section of a CloudFormation template where
# you define the AWS resources you want to create.
"Resources": {
# 'NautilusBucket' is the Logical ID for my resource within this stack.
"NautilusBucket": {
# 'Type' specifies the kind of AWS resource to create.
"Type": "AWS::S3::Bucket",
# 'Properties' defines the configuration for the S3 bucket.
"Properties": {
"BucketName": "nautilus-bucket-431",
# This 'VersioningConfiguration' block is how I enabled versioning.
"VersioningConfiguration": {
"Status": "Enabled"
}
}
}
}
}
EOT # The heredoc string ends here.
# Standard tagging for the CloudFormation stack itself.
tags = {
Name = "nautilus-stack"
}
}
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "CloudFormation stack for an S3 bucket with versioning",
# 'Resources' is the main section of a CloudFormation template where
# you define the AWS resources you want to create.
"Resources": {
# 'NautilusBucket' is the Logical ID for my resource within this stack.
"NautilusBucket": {
# 'Type' specifies the kind of AWS resource to create.
"Type": "AWS::S3::Bucket",
# 'Properties' defines the configuration for the S3 bucket.
"Properties": {
"BucketName": "nautilus-bucket-431",
# This 'VersioningConfiguration' block is how I enabled versioning.
"VersioningConfiguration": {
"Status": "Enabled"
}
}
}
}
}
EOT # The heredoc string ends here.
# Standard tagging for the CloudFormation stack itself.
tags = {
Name = "nautilus-stack"
}
}
Common Pitfalls¶
- Invalid Template Body: The string provided to template_body must be a valid JSON or YAML CloudFormation template. A single syntax error (like a missing comma in the JSON) would cause the terraform apply to fail.
- Stack Name Conflicts: CloudFormation stack names must be unique within an AWS account and region.
- Permissions: The IAM user or role running Terraform needs permissions for both Terraform's actions (cloudformation:CreateStack) and for all the actions that the CloudFormation template itself will perform (s3:CreateBucket, s3:PutBucketVersioning).
Exploring the Essential Terraform Commands¶
This task used the main workflow, but here's a more complete list of the essential commands I'm learning.
terraform init: Initializes the working directory, downloading provider plugins.terraform validate: Checks the syntax of your Terraform files.terraform fmt: Auto-formats your code to the standard style.terraform plan: A "dry run" that shows you what will be created, changed, or destroyed.terraform apply: Executes the plan and makes the changes to your cloud infrastructure.terraform show: Shows the current state of your managed infrastructure.terraform state list: Lists all the resources that Terraform is currently managing.terraform destroy: The opposite ofapply. It creates a plan to destroy all the infrastructure