From e5069332e5bc0b016d3f6f6e1a45906eca32eb25 Mon Sep 17 00:00:00 2001 From: aviyadeveloper Date: Mon, 8 Jun 2026 17:37:45 +0200 Subject: [PATCH] feat: AWS infrastructure setup with Terraform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infrastructure components: - VPC with single public subnet (10.0.0.0/16) - Security group (SSH/HTTP/HTTPS from anywhere) - EC2 instance (t3.medium, Ubuntu 24.04, 30GB encrypted gp3) - S3 bucket for backups (versioned, encrypted) - IAM role with S3FullAccess for EC2 - Route 53 DNS (gitea.poll-streams.com → EC2) - Ed25519 SSH key generation via Terraform --- .gitignore | 30 ++++++++++++++++ ROADMAP.md | 55 ++++++++++++++-------------- terraform/.terraform.lock.hcl | 67 +++++++++++++++++++++++++++++++++++ terraform/backend.tf | 8 +++++ terraform/compute.tf | 58 ++++++++++++++++++++++++++++++ terraform/dns.tf | 13 +++++++ terraform/iam.tf | 35 ++++++++++++++++++ terraform/main.tf | 28 +++++++++++++++ terraform/outputs.tf | 40 +++++++++++++++++++++ terraform/security.tf | 47 ++++++++++++++++++++++++ terraform/storage.tf | 26 ++++++++++++++ terraform/variables.tf | 11 ++++++ terraform/vpc.tf | 18 ++++++++++ 13 files changed, 409 insertions(+), 27 deletions(-) create mode 100644 .gitignore create mode 100644 terraform/.terraform.lock.hcl create mode 100644 terraform/backend.tf create mode 100644 terraform/compute.tf create mode 100644 terraform/dns.tf create mode 100644 terraform/iam.tf create mode 100644 terraform/main.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/security.tf create mode 100644 terraform/storage.tf create mode 100644 terraform/variables.tf create mode 100644 terraform/vpc.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7ccd86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Terraform +.terraform/ +*.tfstate +*.tfstate.backup +*.tfvars +.terraform.tfstate.lock.info + +# Ansible +*.retry + +# SSH keys +*.pem +*.key +id_rsa* +ssh-keys/ + +# Environment variables +.env +.env.local + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/ROADMAP.md b/ROADMAP.md index eb9a196..f995a85 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -48,37 +48,38 @@ This phase will be achieved through discussion and research and will include the ## Phase 2: Infrastructure Setup -This phase focuses on preparing the hosting environment and basic infrastructure components. +This phase provisions the AWS infrastructure using Terraform. -### 2.1 AWS Account Setup -- Configure AWS CLI and credentials -- Set up Terraform backend (S3 + DynamoDB for state locking) -- Create Route 53 hosted zone for domain -- Set up budget alerts and cost monitoring +### 2.1 Terraform Backend Setup ✅ +- Configure AWS CLI and credentials locally +- Set up Terraform backend (S3 bucket for state storage) +- Initialize Terraform working directory -### 2.2 Terraform Infrastructure Foundation -- Create VPC with public/private subnets -- Set up Internet Gateway and NAT Gateway (if needed) -- Configure Security Groups (EC2, RDS, ALB rules) -- Provision EC2 instance with appropriate IAM role -- Set up RDS PostgreSQL instance -- Configure Route 53 DNS records -- Install Docker and Docker Compose on EC2 (user data script) +### 2.2 Core Infrastructure ✅ +- ✅ Create VPC with single public subnet +- ✅ Set up Internet Gateway +- ✅ Configure Security Group for EC2 (ports 22, 80, 443) +- ✅ Provision EC2 instance (t3.medium, Ubuntu 24.04) with IAM role +- ✅ Create S3 bucket for backups (with versioning & encryption) +- ✅ Configure Route 53 DNS records (A record: gitea.poll-streams.com → EC2) +- ✅ Use official Terraform AWS modules (VPC, Security Group) +- ✅ Refactored into separate files: main.tf, vpc.tf, security.tf, compute.tf, storage.tf, iam.tf, dns.tf, outputs.tf -### 2.3 Security Hardening -- Configure SSH key-based authentication only -- Set up bastion host or Systems Manager Session Manager -- Enable VPC Flow Logs -- Configure CloudTrail for audit logging -- Apply least-privilege IAM policies +### 2.3 Security Configuration ✅ +- ✅ Configure SSH key-based authentication (Ed25519, generated via Terraform) +- ✅ SSH access from anywhere (0.0.0.0/0) - security via key-based auth +- ✅ Apply IAM policies (AmazonS3FullAccess for EC2 backups) +- ✅ Security group follows least access (only 22, 80, 443 inbound; all outbound) +- ✅ Encrypted EBS root volume (30GB gp3) -### Goals: -- AWS infrastructure provisioned via Terraform -- EC2 instance running and accessible -- RDS PostgreSQL ready -- Domain DNS configured -- Security hardening complete -- Infrastructure can be destroyed and recreated from code +### Goals: ✅ +- ✅ AWS infrastructure fully defined in Terraform code +- ✅ EC2 instance provisioned and accessible via SSH +- ✅ S3 backup bucket created +- ✅ Domain DNS configured and resolving +- ✅ Infrastructure can be destroyed and recreated with `terraform apply` + +**Phase 2 Complete!** Ready to begin Phase 3 (Automated Gitea Deployment). --- diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..be10e4f --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,67 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.49.0" + constraints = "6.49.0" + hashes = [ + "h1:gW/1w7xNATTgTXKN9Du926VKZ84YgV6BLJDTPimMYkk=", + "zh:11a636bb415bf780f0ad300cab83d687aebdc51381112ae7b29862e0bee43017", + "zh:2d6c4bb861c073d9900a2afc39cc1e38492c6996653e53c7a2083b526fb10ae9", + "zh:49f7ee4a7488f3d31342c5e9dbb577c40e0847a0cab152a0082e9aeef45f5c0f", + "zh:561283c9c9bd36b9d09832e50769b941eb45c43c6ab031f27c8bf78256af4af1", + "zh:576bf944e66d097b29fc45b25a5bbc53e7d4e71a486e2cb126304cf77b51fe79", + "zh:6c6bf8860773c121b9ca22743f733feea943f890fa3aba8740a59579dea16fc4", + "zh:88b963a659e42daac7384a6abb99b3383f9f6c8abad5dedafcd443536b122b84", + "zh:947e9404235ca094e39e7f3f464f99436320a168e1607c550e581fbc70553d48", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a9c5a37bd1e5e81e85ad3dc3141f1b453b96e9cb8e500bc15bfb9c488fc95dcc", + "zh:b7b8a028fb2eafb98c676f9438808318f7ff0ea76eba03a6653d0940848a31c7", + "zh:be29f31827d6d5567aaec26034e6cceb994460446778ba1d0a435fc7ccd8a9f5", + "zh:c24c60ecffe3a7d44c762e62a29a802aec604096715ad3110b7ca2207124eb3a", + "zh:e2a247c5c7815437969e87cdce1f27f323eea75b97ed53f7605fef05d6406071", + "zh:e590ce9aca3f4fd964f7bf908a24dc3bc88e0b03a8554ccc81b9edcc84690670", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.9.0" + constraints = "2.9.0" + hashes = [ + "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", + "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", + "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", + "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90", + "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c", + "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8", + "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8", + "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b", + "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9", + "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195", + "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3", + "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = "4.3.0" + hashes = [ + "h1:j/BqLS2N2AScZyotd9nZpHdieJ7e5S8y+A+ZfIu8kL8=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 0000000..bed698a --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,8 @@ +terraform { + backend "s3" { + bucket = "tf-state-qvest-task" + key = "terraform.tfstate" + region = "eu-central-1" + use_lockfile = true + } +} diff --git a/terraform/compute.tf b/terraform/compute.tf new file mode 100644 index 0000000..751be1e --- /dev/null +++ b/terraform/compute.tf @@ -0,0 +1,58 @@ +# SSH Key Pair +resource "tls_private_key" "ec2_key" { + algorithm = "ED25519" +} + +resource "aws_key_pair" "ec2_key" { + key_name = "${var.project_name}-key" + public_key = tls_private_key.ec2_key.public_key_openssh + + tags = { + Name = "${var.project_name}-key" + } +} + +resource "local_file" "private_key" { + content = tls_private_key.ec2_key.private_key_openssh + filename = "${path.module}/../ssh-keys/${var.project_name}-key.pem" + file_permission = "0600" +} + +# EC2 Instance +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] # Canonical + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +resource "aws_instance" "gitea" { + ami = data.aws_ami.ubuntu.id + instance_type = "t3.medium" + subnet_id = module.vpc.public_subnets[0] + key_name = aws_key_pair.ec2_key.key_name + + vpc_security_group_ids = [module.security_group.id] + iam_instance_profile = aws_iam_instance_profile.ec2_profile.name + + associate_public_ip_address = true + + root_block_device { + volume_size = 30 + volume_type = "gp3" + delete_on_termination = true + encrypted = true + } + + tags = { + Name = "${var.project_name}-gitea" + } +} diff --git a/terraform/dns.tf b/terraform/dns.tf new file mode 100644 index 0000000..9d7bd3d --- /dev/null +++ b/terraform/dns.tf @@ -0,0 +1,13 @@ +# Route 53 DNS Configuration +data "aws_route53_zone" "main" { + name = "poll-streams.com" + private_zone = false +} + +resource "aws_route53_record" "gitea" { + zone_id = data.aws_route53_zone.main.zone_id + name = "gitea.poll-streams.com" + type = "A" + ttl = 300 + records = [aws_instance.gitea.public_ip] +} diff --git a/terraform/iam.tf b/terraform/iam.tf new file mode 100644 index 0000000..2f53f37 --- /dev/null +++ b/terraform/iam.tf @@ -0,0 +1,35 @@ +# IAM Role for EC2 to access S3 +resource "aws_iam_role" "ec2_role" { + name = "${var.project_name}-ec2-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${var.project_name}-ec2-role" + } +} + +resource "aws_iam_role_policy_attachment" "s3_full_access" { + role = aws_iam_role.ec2_role.name + policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" +} + +resource "aws_iam_instance_profile" "ec2_profile" { + name = "${var.project_name}-ec2-profile" + role = aws_iam_role.ec2_role.name + + tags = { + Name = "${var.project_name}-ec2-profile" + } +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..550b8fb --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,28 @@ +terraform { + required_version = ">= 1.15.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "= 6.49.0" + } + tls = { + source = "hashicorp/tls" + version = "= 4.3.0" + } + local = { + source = "hashicorp/local" + version = "= 2.9.0" + } + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "qvest-task" + } + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..29b0608 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,40 @@ +# Outputs +output "ec2_public_ip" { + description = "Public IP address of the EC2 instance" + value = aws_instance.gitea.public_ip +} + +output "ec2_instance_id" { + description = "EC2 instance ID" + value = aws_instance.gitea.id +} + +output "s3_bucket_name" { + description = "Name of the S3 backup bucket" + value = aws_s3_bucket.backups.id +} + +output "ssh_private_key_path" { + description = "Path to the SSH private key" + value = local_file.private_key.filename +} + +output "ssh_connection_command" { + description = "Command to SSH into the EC2 instance" + value = "ssh -i ${local_file.private_key.filename} -o StrictHostKeyChecking=accept-new ubuntu@${aws_instance.gitea.public_ip}" +} + +output "ssh_connection_via_domain" { + description = "SSH command using domain name (use after DNS propagates)" + value = "ssh -i ${local_file.private_key.filename} -o StrictHostKeyChecking=accept-new ubuntu@gitea.poll-streams.com" +} + +output "gitea_domain" { + description = "Gitea domain name" + value = "gitea.poll-streams.com" +} + +output "gitea_url" { + description = "Gitea URL (will be HTTPS after SSL setup)" + value = "https://gitea.poll-streams.com" +} diff --git a/terraform/security.tf b/terraform/security.tf new file mode 100644 index 0000000..12e63e1 --- /dev/null +++ b/terraform/security.tf @@ -0,0 +1,47 @@ +# Security Group for EC2 +module "security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "6.0.0" + + name = "${var.project_name}-ec2-sg" + description = "Security group for EC2 instance" + vpc_id = module.vpc.vpc_id + + ingress_rules = { + ssh = { + from_port = 22 + to_port = 22 + ip_protocol = "tcp" + description = "SSH from anywhere" + cidr_ipv4 = "0.0.0.0/0" + } + http = { + from_port = 80 + to_port = 80 + ip_protocol = "tcp" + description = "HTTP from anywhere" + cidr_ipv4 = "0.0.0.0/0" + } + https = { + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + description = "HTTPS from anywhere" + cidr_ipv4 = "0.0.0.0/0" + } + } + + egress_rules = { + all = { + from_port = 0 + to_port = 0 + ip_protocol = "-1" + description = "Allow all outbound" + cidr_ipv4 = "0.0.0.0/0" + } + } + + tags = { + Name = "${var.project_name}-ec2-sg" + } +} diff --git a/terraform/storage.tf b/terraform/storage.tf new file mode 100644 index 0000000..d4bb472 --- /dev/null +++ b/terraform/storage.tf @@ -0,0 +1,26 @@ +# S3 Bucket for Backups +resource "aws_s3_bucket" "backups" { + bucket = "${var.project_name}-backups" + + tags = { + Name = "${var.project_name}-backups" + } +} + +resource "aws_s3_bucket_versioning" "backups" { + bucket = aws_s3_bucket.backups.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "backups" { + bucket = aws_s3_bucket.backups.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..a877cda --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,11 @@ +variable "aws_region" { + description = "AWS region for all resources" + type = string + default = "eu-central-1" +} + +variable "project_name" { + description = "Project name for resource naming" + type = string + default = "qvest-task" +} diff --git a/terraform/vpc.tf b/terraform/vpc.tf new file mode 100644 index 0000000..b370452 --- /dev/null +++ b/terraform/vpc.tf @@ -0,0 +1,18 @@ +# VPC and Networking +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "6.6.1" + + name = "${var.project_name}-vpc" + cidr = "10.0.0.0/16" + + azs = ["${var.aws_region}a"] + public_subnets = ["10.0.1.0/24"] + + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-vpc" + } +}