commit 88b020837b66448fc37deb8ef515be0368099db0 Author: Lukasz Raczylo Date: Wed Aug 19 12:46:34 2026 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..362af79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Terraform +.terraform/ +*.tfstate +*.tfstate.* +*.tfplan +crash.log +crash.*.log + +# Variable files may contain secrets; commit only examples +*.tfvars +*.tfvars.json +!*.tfvars.example + +# Local overrides +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc + +# Superseded RTF originals +*.rtf + +# macOS +.DS_Store diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..50d790d --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.9.0" + hashes = [ + "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", + "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", + "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", + "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", + "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", + "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", + "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", + "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", + "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", + "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", + "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", + "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dd97e8 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# hello-world-tf + +Minimal AWS network and storage baseline managed with Terraform. Creates a VPC with public and private subnets, an internet gateway, an optional NAT gateway, and a private S3 bucket. No compute resources. + +## What it creates + +- **VPC** — `10.0.0.0/16` by default, DNS support and hostnames enabled +- **2 public subnets** (`10.0.1.0/24`, `10.0.2.0/24`) — auto-assign public IPs, routed to the internet gateway +- **2 private subnets** (`10.0.10.0/24`, `10.0.20.0/24`) — own route table, no internet egress unless the NAT gateway is enabled +- **Internet gateway** + public route table +- **NAT gateway** (optional, off by default) — single NAT in the first public subnet with an Elastic IP; adds a default route for the private subnets +- **S3 bucket** — `--app-data-`, versioning on by default, all public access blocked + +All resources are tagged with `Project`, `Environment`, `ManagedBy`, and `CreatedBy` via provider `default_tags`, plus anything passed in `additional_tags`. + +## Requirements + +| Name | Version | +|------|---------| +| Terraform | >= 1.6.0 | +| AWS provider | ~> 5.0 | +| Random provider | ~> 3.6 | + +AWS credentials must be available in the environment (profile, SSO, or environment variables). + +## Usage + +```sh +terraform init +terraform plan +terraform apply +``` + +State is local. Configure a remote backend before using this beyond experiments. + +## Inputs + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `project_name` | string | `aether-test` | Resource name prefix; lowercase letters, numbers, hyphens only | +| `environment` | string | `dev` | One of `dev`, `staging`, `prod` | +| `aws_region` | string | `eu-west-2` | AWS region | +| `vpc_cidr` | string | `10.0.0.0/16` | VPC CIDR block | +| `public_subnet_cidrs` | list(string) | `["10.0.1.0/24", "10.0.2.0/24"]` | Public subnet CIDRs | +| `private_subnet_cidrs` | list(string) | `["10.0.10.0/24", "10.0.20.0/24"]` | Private subnet CIDRs | +| `enable_s3_versioning` | bool | `true` | Toggle S3 bucket versioning | +| `create_nat_gateway` | bool | `false` | Create NAT gateway for private subnet egress | +| `additional_tags` | map(string) | `{}` | Extra tags merged into all resources | + +## Outputs + +VPC ID and CIDR, subnet IDs and CIDRs, internet gateway ID, public and private route table IDs, NAT gateway ID (null when disabled), S3 bucket name/ARN/region, AWS account ID and region, and a `resource_summary` object. + +## Cost notes + +Everything here is free or near-free except the NAT gateway: enabling `create_nat_gateway` costs roughly $0.05/hour plus data processing charges in eu-west-2. The single-NAT layout is a deliberate simplification for dev use; production would run one NAT per availability zone. diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..9178f17 --- /dev/null +++ b/main.tf @@ -0,0 +1,217 @@ +######################################## +# Terraform Configuration +######################################## +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + } +} + +######################################## +# Provider Configuration +######################################## +provider "aws" { + region = var.aws_region + + default_tags { + tags = merge( + { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + CreatedBy = "Aether" + }, + var.additional_tags + ) + } +} + +######################################## +# VPC +######################################## +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "${var.project_name}-${var.environment}-vpc" + } +} + +######################################## +# Public Subnets +######################################## +resource "aws_subnet" "public" { + count = length(var.public_subnet_cidrs) + + vpc_id = aws_vpc.main.id + cidr_block = var.public_subnet_cidrs[count.index] + availability_zone = data.aws_availability_zones.available.names[count.index] + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-${var.environment}-public-subnet-${count.index + 1}" + Type = "public" + } +} + +######################################## +# Private Subnets +######################################## +resource "aws_subnet" "private" { + count = length(var.private_subnet_cidrs) + + vpc_id = aws_vpc.main.id + cidr_block = var.private_subnet_cidrs[count.index] + availability_zone = data.aws_availability_zones.available.names[count.index] + + tags = { + Name = "${var.project_name}-${var.environment}-private-subnet-${count.index + 1}" + Type = "private" + } +} + +######################################## +# Internet Gateway +######################################## +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project_name}-${var.environment}-igw" + } +} + +######################################## +# Public Route Table +######################################## +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = { + Name = "${var.project_name}-${var.environment}-public-rt" + } +} + +resource "aws_route_table_association" "public" { + count = length(aws_subnet.public) + + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +######################################## +# NAT Gateway (optional) +######################################## +resource "aws_eip" "nat" { + count = var.create_nat_gateway ? 1 : 0 + + domain = "vpc" + + tags = { + Name = "${var.project_name}-${var.environment}-nat-eip" + } + + depends_on = [aws_internet_gateway.main] +} + +resource "aws_nat_gateway" "main" { + count = var.create_nat_gateway ? 1 : 0 + + allocation_id = aws_eip.nat[0].id + subnet_id = aws_subnet.public[0].id + + tags = { + Name = "${var.project_name}-${var.environment}-nat" + } + + depends_on = [aws_internet_gateway.main] +} + +######################################## +# Private Route Table +######################################## +resource "aws_route_table" "private" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project_name}-${var.environment}-private-rt" + } +} + +resource "aws_route" "private_nat" { + count = var.create_nat_gateway ? 1 : 0 + + route_table_id = aws_route_table.private.id + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[0].id +} + +resource "aws_route_table_association" "private" { + count = length(aws_subnet.private) + + subnet_id = aws_subnet.private[count.index].id + route_table_id = aws_route_table.private.id +} + +######################################## +# S3 Bucket for Application Data +######################################## +resource "aws_s3_bucket" "app_data" { + bucket = "${var.project_name}-${var.environment}-app-data-${random_id.bucket_suffix.hex}" + + tags = { + Name = "${var.project_name}-${var.environment}-app-data" + Description = "Application data storage" + } +} + +resource "aws_s3_bucket_versioning" "app_data" { + bucket = aws_s3_bucket.app_data.id + + versioning_configuration { + status = var.enable_s3_versioning ? "Enabled" : "Disabled" + } +} + +resource "aws_s3_bucket_public_access_block" "app_data" { + bucket = aws_s3_bucket.app_data.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +######################################## +# Random ID for unique bucket naming +######################################## +resource "random_id" "bucket_suffix" { + byte_length = 4 +} + +######################################## +# Data Sources +######################################## +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..a257cb8 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,105 @@ +######################################## +# VPC Outputs +######################################## +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.main.id +} + +output "vpc_cidr" { + description = "CIDR block of the VPC" + value = aws_vpc.main.cidr_block +} + +######################################## +# Subnet Outputs +######################################## +output "public_subnet_ids" { + description = "IDs of public subnets" + value = aws_subnet.public[*].id +} + +output "private_subnet_ids" { + description = "IDs of private subnets" + value = aws_subnet.private[*].id +} + +output "public_subnet_cidrs" { + description = "CIDR blocks of public subnets" + value = aws_subnet.public[*].cidr_block +} + +output "private_subnet_cidrs" { + description = "CIDR blocks of private subnets" + value = aws_subnet.private[*].cidr_block +} + +######################################## +# Networking Outputs +######################################## +output "internet_gateway_id" { + description = "ID of the Internet Gateway" + value = aws_internet_gateway.main.id +} + +output "public_route_table_id" { + description = "ID of the public route table" + value = aws_route_table.public.id +} + +output "private_route_table_id" { + description = "ID of the private route table" + value = aws_route_table.private.id +} + +output "nat_gateway_id" { + description = "ID of the NAT Gateway (null if not created)" + value = var.create_nat_gateway ? aws_nat_gateway.main[0].id : null +} + +######################################## +# S3 Outputs +######################################## +output "s3_bucket_name" { + description = "Name of the S3 bucket" + value = aws_s3_bucket.app_data.id +} + +output "s3_bucket_arn" { + description = "ARN of the S3 bucket" + value = aws_s3_bucket.app_data.arn +} + +output "s3_bucket_region" { + description = "Region of the S3 bucket" + value = aws_s3_bucket.app_data.region +} + +######################################## +# AWS Account Information +######################################## +output "aws_account_id" { + description = "AWS Account ID" + value = data.aws_caller_identity.current.account_id +} + +output "aws_region" { + description = "AWS Region" + value = data.aws_region.current.name +} + +######################################## +# Resource Summary +######################################## +output "resource_summary" { + description = "Summary of created resources" + value = { + vpc_created = true + public_subnets_count = length(aws_subnet.public) + private_subnets_count = length(aws_subnet.private) + s3_bucket_created = true + s3_versioning_enabled = var.enable_s3_versioning + internet_gateway_created = true + nat_gateway_created = var.create_nat_gateway + } +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..f6e4937 --- /dev/null +++ b/variables.tf @@ -0,0 +1,83 @@ +######################################## +# Project Configuration +######################################## +variable "project_name" { + type = string + description = "Name of the project (used for resource naming)" + default = "aether-test" + + validation { + condition = can(regex("^[a-z0-9-]+$", var.project_name)) + error_message = "Project name must contain only lowercase letters, numbers, and hyphens." + } +} + +variable "environment" { + type = string + description = "Environment name (dev, staging, prod)" + default = "dev" + + validation { + condition = contains(["dev", "staging", "prod"], var.environment) + error_message = "Environment must be dev, staging, or prod." + } +} + +######################################## +# AWS Configuration +######################################## +variable "aws_region" { + type = string + description = "AWS region for resources" + default = "eu-west-2" +} + +######################################## +# Network Configuration +######################################## +variable "vpc_cidr" { + type = string + description = "CIDR block for the VPC" + default = "10.0.0.0/16" + + validation { + condition = can(cidrhost(var.vpc_cidr, 0)) + error_message = "Must be a valid IPv4 CIDR block." + } +} + +variable "public_subnet_cidrs" { + type = list(string) + description = "CIDR blocks for public subnets" + default = ["10.0.1.0/24", "10.0.2.0/24"] +} + +variable "private_subnet_cidrs" { + type = list(string) + description = "CIDR blocks for private subnets" + default = ["10.0.10.0/24", "10.0.20.0/24"] +} + +######################################## +# Feature Flags +######################################## +variable "enable_s3_versioning" { + type = bool + description = "Enable versioning for S3 buckets" + default = true +} + +variable "create_nat_gateway" { + type = bool + description = "Create NAT Gateway for private subnet internet access" + default = false +} + +######################################## +# Tags +######################################## +variable "additional_tags" { + type = map(string) + description = "Additional tags to apply to all resources" + default = {} +}