Deploy Python App with Terraform
Deploy a Python app with Terraform basics — AWS Cloud & DevOps with Python tutorial, lesson 24. Hands-on steps, troubleshooting, and what to study next.
Focus: deploy a python app with terraform basics
Tired of clicking through the AWS Console to launch an EC2 instance, attach a security group, and pray your Python app comes up? Manual infrastructure setup is slow, error-prone, and impossible to reproduce — and it doesn't scale past one environment. This lesson shows you how to deploy a Python app with Terraform basics, turning your fragile click-ops into declarative, version-controlled infrastructure that you can spin up and tear down in seconds.
The Problem This Lesson Solves
Every DevOps engineer eventually hits the same wall: the manual deployment pipeline. You log into AWS, click EC2, choose an instance type, configure a security group, and SSH in only to realize you forgot to open port 8000. Now you're debugging at 2 AM, and there's no record of why that security group exists or who created that AMI.
Beyond the frustration, manual setups have three major flaws:
- No repeatability — You can't reliably recreate the same environment in staging, production, or a disaster recovery region.
- No version control — Your infrastructure config lives in your head or a wiki, not in git.
- No accountability — When something breaks, there's no audit trail to tell you what changed and when.
The pain point: Without infrastructure as code, deploying a Python app becomes a fragile, hand-crafted ritual instead of a deterministic, testable process.
Core Concept / Mental Model
Think of Terraform as a blueprint for your cloud infrastructure — instead of assembling a server piece by piece, you write a declarative file that describes the end state you want, and Terraform figures out the steps to get there.
Key definitions to internalize:
- Infrastructure as Code (IaC) — Managing infrastructure (servers, networks, load balancers) through machine-readable definition files, not manual processes.
- Declarative vs. imperative — You declare what you want (e.g., "an EC2 instance with this AMI and this security group"), not how to build it step-by-step. Terraform handles the how.
- Resource — A single infrastructure object Terraform manages, like an
aws_instanceoraws_security_group. - Provider — A plugin that lets Terraform talk to a specific cloud API (e.g.,
aws). - State file — A local or remote file (
terraform.tfstate) that tracks the current state of your deployed resources. It's Terraform's source of truth — treat it like a precious artifact.
The mental model in action:
Imagine you want to deploy a Flask app to AWS. Without Terraform, you manually:
- Pick an AMI
- Choose an instance type
- Configure a security group
- SSH in and install Python, pip, and your app
- Open port 8000
With Terraform, you write a single main.tf that declares all of that, and Terraform does the heavy lifting — no clicking, no guesswork.
How It Works Step by Step
Terraform follows a predictable five-phase workflow. Master this and you can deploy any infrastructure, not just a Python app.
-
Write — Define your desired infrastructure in
.tffiles. You'll use theterraformblock to pin the provider version and theresourceblocks to define AWS objects. -
Initialize — Run
terraform initto download the AWS provider plugin and set up your working directory. -
Plan — Run
terraform planto create an execution plan. Terraform compares your config to the current state and shows what it will add, change, or destroy. This is your safety net. -
Apply — Run
terraform applyto execute the plan. Terraform creates the resources in the correct order, respecting dependencies (like starting an instance after its security group exists). -
Destroy — Run
terraform destroywhen you're done to tear everything down — no orphaned resources, no surprise AWS bills.
Why this matters for your Python app: Terraform's dependency graph means you don't have to manually sequence resource creation. It also gives you idempotent deployments — running apply twice produces the same final state, not duplicate resources.
Hands-On Walkthrough
Let's deploy a minimal Flask app on an EC2 instance. You'll need:
- Terraform installed (v1.5+ recommended)
- An AWS account with credentials configured (via
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY, oraws configure)
Step 1: Create your project structure
mkdir flask-terraform && cd flask-terraform
Step 2: Write the Terraform configuration
Create a file named main.tf:
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.0"
}
provider "aws" {
region = "us-east-1"
}
resource "aws_security_group" "flask_sg" {
name = "flask-sg"
description = "Allow inbound SSH and Flask traffic"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 8000
to_port = 8000
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "flask_app" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2 (check for your region)
instance_type = "t2.micro"
security_groups = [aws_security_group.flask_sg.name]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y python3 python3-pip
pip3 install flask
echo 'from flask import Flask; app = Flask(__name__); @app.route("/") def home(): return "Hello, Terraform!"' > app.py
nohup python3 app.py --host=0.0.0.0 --port=8000 &
EOF
tags = {
Name = "FlaskApp"
}
}
output "public_ip" {
value = aws_instance.flask_app.public_ip
}
What's happening here:
- The
terraformblock pins the AWS provider version — reproducibility. - The
aws_security_groupblock opens ports 22 (SSH) and 8000 (Flask). - The
aws_instanceblock defines the AMI, instance type, and attaches our security group. user_dataruns a shell script during first boot to install Python, Flask, and start the app.- The
outputblock prints the public IP after creation — your app's address.
Step 3: Initialize and deploy
terraform init
terraform plan
terraform apply -auto-approve
Expected output (simplified):
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "54.123.45.67"
Step 4: Verify your app
curl http://<public_ip>:8000
You should see Hello, Terraform!.
Step 5: Tear it down
terraform destroy -auto-approve
This kills the EC2 instance and security group — no lingering costs.
Pro tip: Never store
terraform.tfstatein git as-is. Use a backend like S3 with DynamoDB locking for team deployments — your teammates will thank you.
Compare Options / When to Choose What
Terraform isn't your only IaC tool. Here's how it stacks up against the alternatives:
| Tool | Language / Style | Learning Curve | State Management | Best For |
|---|---|---|---|---|
| Terraform | HCL, declarative | Moderate | Local/S3/remote | Multi-cloud, broad AWS coverage, fast adoption |
| AWS CloudFormation | YAML/JSON, declarative | Steep (verbose) | AWS-native | Teams fully locked into AWS, StackSets, AWS-native drift detection |
| Pulumi | Python/TS/Go, imperative | Moderate if you know Python | State backends like S3 | Developers who want to code infrastructure in a real language |
| Ansible | YAML, imperative (task-based) | Easy for sysadmins | No native state | Configuration management, mixed config+provisioning |
When to choose Terraform:
- You need to manage resources across multiple cloud providers (AWS + GCP, for example).
- You want a mature ecosystem with thousands of modules and a huge community.
- You prefer declarative infrastructure with a powerful plan/dry-run feature.
Variations to explore:
- Terraform modules — Package reusable config (e.g., a VPC module) and share across environments.
- Remote state backends — Store state in S3 with DynamoDB locking for team collaboration.
Troubleshooting & Edge Cases
Even experts hit snags. Here are the usual suspects when deploying a Python app with Terraform:
1. terraform plan shows an error: "No valid credential sources found"
Cause: AWS credentials aren't configured.
Fix: Run aws configure, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.
2. EC2 instance launches, but curl times out
Cause: The security group doesn't allow traffic on port 8000, or the app failed to start.
Fix: Double-check the ingress block for from_port = 8000. SSH into the instance and check logs:
ssh -i your-key.pem ec2-user@<public_ip>
sudo cat /var/log/cloud-init-output.log | tail -20
If Flask isn't installed, rerun the user_data script manually to debug.
3. user_data script isn't running
Cause: The script didn't start with #!/bin/bash or had a syntax error.
Fix: Verify heredoc formatting and permissions. Test the script locally before embedding it.
4. State file conflicts in a team
Cause: Multiple people ran terraform apply on the same state file.
Fix: Use a remote backend (S3 + DynamoDB) and enable state locking.
5. Terraform destroys a resource you wanted to keep
Cause: You removed a resource block from your config and ran apply.
Fix: Use terraform state rm to detach a resource from state instead of deleting it, or use prevent_destroy = true lifecycle setting.
Pro tip: Always run
terraform planbeforeapplyin production. It's your free dry run.
What You Learned & What's Next
You now understand the core idea behind deploying a Python app with Terraform basics: you wrote a declarative config, initialized Terraform, planned your changes, applied them, and verified your Flask app responded on port 8000. You also saw how to destroy everything cleanly — no orphaned resources. You can apply this same workflow to any AWS service, from S3 buckets to Lambda functions.
You've hit a critical milestone in the AWS Cloud & DevOps with Python track. Next up, you'll tackle AWS CloudFormation — AWS's native IaC tool — where you'll compare its YAML template approach to Terraform's HCL. You'll learn to write a CloudFormation stack by hand, understand its drift detection, and decide when to choose CloudFormation over Terraform (or vice versa).
Your Terraform skills are your ticket to reproducible, version-controlled cloud infrastructure. Keep building!
Practice recap
Now try it yourself: modify the main.tf to expose port 5000 instead of 8000, change the instance type to t3.micro, and redeploy. Then, explore adding an S3 bucket resource to your config and run terraform plan to see the new resource appear in the plan.
Common mistakes
- Forgetting to run
terraform initbeforeplan— you'll get a 'provider not found' error. - Using a hardcoded AMI that doesn't exist in your region — instance creation fails.
- Leaving the state file in git — it can contain sensitive data and cause team conflicts.
- Ignoring the
user_datascript — it must be executable and start with#!/bin/bashto run on boot.
Variations
- Use Terraform modules to encapsulate reusable infrastructure, like a VPC or EC2 module.
- Adopt a remote state backend with S3 and DynamoDB locking for team collaboration.
- Consider Pulumi, which lets you write infrastructure in Python instead of HCL.
Real-world use cases
- Spin up a staging environment with the exact same EC2 config as production using Terraform modules.
- Deploy a Flask microservice to AWS for a demo or MVP, then tear it down with
terraform destroy. - Automate a multi-region deployment of a Python app with Terraform workspaces for dev vs. prod.
Key takeaways
- Terraform is declarative — you define the end state, Terraform handles the steps.
- Always run
terraform initbeforeplanandapplyto download providers. - Use
terraform planas a safety net to review exact changes before applying. - Security groups must explicitly allow ports your app uses, like 8000 for Flask.
- The
user_datascript is your best friend for app bootstrapping on EC2. - Always
terraform destroyto avoid surprise cloud bills.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.