diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d321f26 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Environment +ENVIRONMENT=development +DEBUG=true + +# Application +APP_NAME=FastAPI Starter +APP_VERSION=1.0.0 + +# Celery +CELERY_BROKER_URL=amqp://guest:guest@localhost:5672// +CELERY_RESULT_BACKEND= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 888582c..5ed9675 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,9 @@ jobs: code-quality: name: Code Quality Checks runs-on: ubuntu-latest + env: + CELERY_BROKER_URL: memory:// + CELERY_RESULT_BACKEND: cache+memory:// steps: - name: Checkout code diff --git a/requirements.txt b/requirements.txt index 1878a3e..f80db32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ fastapi==0.104.1 uvicorn[standard]==0.24.0 python-multipart==0.0.6 celery==5.3.4 -kombu==5.3.4 \ No newline at end of file +kombu==5.3.4 +pydantic-settings==2.1.0 \ No newline at end of file diff --git a/src/celery_app.py b/src/celery_app.py index a01fbce..7b6cc9f 100644 --- a/src/celery_app.py +++ b/src/celery_app.py @@ -1,17 +1,14 @@ -import os +from celery import Celery # type: ignore[import-untyped] -from celery import Celery +from src.config import get_settings -# RabbitMQ connection URL -broker_url = os.getenv("CELERY_BROKER_URL") -# RPC backend for results (uses RabbitMQ RPC, no additional service needed) -result_backend = os.getenv("CELERY_RESULT_BACKEND") +settings = get_settings() # Create Celery instance celery_app = Celery( "fastapi_app", - broker=broker_url, - backend=result_backend, + broker=settings.CELERY_BROKER_URL, + backend=settings.CELERY_RESULT_BACKEND, include=["src.tasks"], ) diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..a0a74b6 --- /dev/null +++ b/src/config.py @@ -0,0 +1,57 @@ +from functools import lru_cache +from typing import Literal + +from pydantic import Field, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Centralna konfiguracja aplikacji z walidacją.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + # Environment + ENVIRONMENT: Literal["development", "staging", "production"] = Field( + default="development", description="Środowisko uruchomieniowe" + ) + DEBUG: bool = Field(default=False, description="Tryb debugowania") + + # Application + APP_NAME: str = Field(default="FastAPI Starter", description="Nazwa aplikacji") + APP_VERSION: str = Field(default="1.0.0", description="Wersja aplikacji") + + # Celery + CELERY_BROKER_URL: str = Field(..., description="URL brokera Celery (RabbitMQ/Redis)") + CELERY_RESULT_BACKEND: str = Field( + default="", description="Backend dla wyników Celery (opcjonalne)" + ) + + @field_validator("CELERY_BROKER_URL") + @classmethod + def validate_celery_broker(cls, v: str) -> str: + if not v: + raise ValueError("CELERY_BROKER_URL jest wymagane") + return v + + @property + def is_production(self) -> bool: + """Sprawdza czy aplikacja działa w produkcji.""" + return self.ENVIRONMENT == "production" + + @property + def is_development(self) -> bool: + """Sprawdza czy aplikacja działa w trybie deweloperskim.""" + return self.ENVIRONMENT == "development" + + +@lru_cache +def get_settings() -> Settings: + """Singleton dla ustawień. + Używa lru_cache aby załadować konfigurację tylko raz. + """ + return Settings() # type: ignore[call-arg] diff --git a/src/main.py b/src/main.py index ca543fa..73d5517 100644 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,18 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel +from src.config import get_settings from src.tasks import process_message -app = FastAPI(title="FastAPI Starter", description="A starter FastAPI application", version="1.0.0") +settings = get_settings() + +app = FastAPI( + title=settings.APP_NAME, + description="A starter FastAPI application", + version=settings.APP_VERSION, + debug=settings.DEBUG, +) -# CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], diff --git a/terraform/cloudwatch.tf b/terraform/cloudwatch.tf new file mode 100644 index 0000000..fbc6160 --- /dev/null +++ b/terraform/cloudwatch.tf @@ -0,0 +1,19 @@ +# CloudWatch Log Group +resource "aws_cloudwatch_log_group" "app" { + name = "/ecs/${var.project_name}" + retention_in_days = 7 + + tags = { + Name = "${var.project_name}-logs" + } +} + +# CloudWatch Log Group dla RabbitMQ +resource "aws_cloudwatch_log_group" "rabbitmq" { + name = "/ecs/${var.project_name}-rabbitmq" + retention_in_days = 7 + + tags = { + Name = "${var.project_name}-rabbitmq-logs" + } +} diff --git a/terraform/data.tf b/terraform/data.tf new file mode 100644 index 0000000..a1d067b --- /dev/null +++ b/terraform/data.tf @@ -0,0 +1,22 @@ +# Data source for availability zones +data "aws_availability_zones" "available" { + state = "available" +} + +# Data source do pobrania account ID +data "aws_caller_identity" "current" {} + +# ECR Repository - using existing repository (data source) +data "aws_ecr_repository" "app" { + name = var.ecr_repository_name +} + +# Data source do odczytu wszystkich parametrów z Parameter Store +data "aws_ssm_parameters_by_path" "app_secrets" { + path = "/${var.project_name}" + recursive = true + + depends_on = [ + aws_ssm_parameter.celery_broker_url + ] +} diff --git a/terraform/ecs.tf b/terraform/ecs.tf new file mode 100644 index 0000000..80d82e8 --- /dev/null +++ b/terraform/ecs.tf @@ -0,0 +1,172 @@ +# ECS Cluster +resource "aws_ecs_cluster" "main" { + name = "${var.project_name}-cluster" + + tags = { + Name = "${var.project_name}-cluster" + } +} + +# ECS Task Definition dla RabbitMQ +resource "aws_ecs_task_definition" "rabbitmq" { + family = "${var.project_name}-rabbitmq" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = 256 + memory = 512 + execution_role_arn = aws_iam_role.ecs_task_execution.arn + task_role_arn = aws_iam_role.ecs_task.arn + + container_definitions = jsonencode([ + { + name = "rabbitmq" + image = "rabbitmq:3.12-management-alpine" + + portMappings = [ + { + containerPort = 5672 + protocol = "tcp" + }, + { + containerPort = 15672 + protocol = "tcp" + } + ] + + environment = [ + { + name = "RABBITMQ_DEFAULT_USER" + value = var.rabbitmq_username + }, + { + name = "RABBITMQ_DEFAULT_PASS" + value = var.rabbitmq_password + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.rabbitmq.name + "awslogs-region" = var.aws_region + "awslogs-stream-prefix" = "ecs" + } + } + + healthCheck = { + command = ["CMD-SHELL", "rabbitmq-diagnostics ping"] + interval = 30 + timeout = 5 + retries = 3 + startPeriod = 60 + } + } + ]) + + tags = { + Name = "${var.project_name}-rabbitmq-task" + } +} + +# ECS Service dla RabbitMQ +resource "aws_ecs_service" "rabbitmq" { + name = "${var.project_name}-rabbitmq-service" + cluster = aws_ecs_cluster.main.id + task_definition = aws_ecs_task_definition.rabbitmq.arn + desired_count = 1 + launch_type = "FARGATE" + + network_configuration { + subnets = [aws_subnet.public.id] + security_groups = [aws_security_group.rabbitmq.id] + assign_public_ip = true + } + + # Service Discovery + service_registries { + registry_arn = aws_service_discovery_service.rabbitmq.arn + } + + depends_on = [ + aws_iam_role_policy_attachment.ecs_task_execution + ] + + tags = { + Name = "${var.project_name}-rabbitmq-service" + } +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "app" { + family = var.project_name + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = 256 + memory = 512 + execution_role_arn = aws_iam_role.ecs_task_execution.arn + task_role_arn = aws_iam_role.ecs_task.arn + + container_definitions = jsonencode([ + { + name = var.project_name + image = "${data.aws_ecr_repository.app.repository_url}:latest" + + portMappings = [ + { + containerPort = 8000 + protocol = "tcp" + } + ] + + environment = local.environment_vars + secrets = local.ssm_secrets + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.app.name + "awslogs-region" = var.aws_region + "awslogs-stream-prefix" = "ecs" + } + } + + healthCheck = { + command = ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] + interval = 30 + timeout = 5 + retries = 3 + startPeriod = 60 + } + } + ]) + + tags = { + Name = "${var.project_name}-task-definition" + } +} + +# ECS Service +resource "aws_ecs_service" "app" { + name = "${var.project_name}-service" + cluster = aws_ecs_cluster.main.id + task_definition = aws_ecs_task_definition.app.arn + desired_count = 1 + launch_type = "FARGATE" + + network_configuration { + subnets = [aws_subnet.public.id] + security_groups = [aws_security_group.ecs_tasks.id] + assign_public_ip = true + } + + depends_on = [ + aws_iam_role_policy_attachment.ecs_task_execution, + aws_ecs_service.rabbitmq, + aws_ssm_parameter.celery_broker_url, + aws_iam_role_policy.ecs_task_execution_ssm + ] + + tags = { + Name = "${var.project_name}-service" + } +} diff --git a/terraform/iam.tf b/terraform/iam.tf new file mode 100644 index 0000000..2033e28 --- /dev/null +++ b/terraform/iam.tf @@ -0,0 +1,84 @@ +# IAM Role for ECS Task Execution +resource "aws_iam_role" "ecs_task_execution" { + name = "${var.project_name}-ecs-task-execution-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${var.project_name}-ecs-task-execution-role" + } +} + +# IAM Role Policy Attachment for ECS Task Execution +resource "aws_iam_role_policy_attachment" "ecs_task_execution" { + role = aws_iam_role.ecs_task_execution.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# IAM Policy for Parameter Store access +resource "aws_iam_role_policy" "ecs_task_execution_ssm" { + name = "${var.project_name}-ecs-task-execution-ssm-policy" + role = aws_iam_role.ecs_task_execution.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParametersByPath" + ] + Resource = [ + "arn:aws:ssm:${var.aws_region}:*:parameter/${var.project_name}/*" + ] + }, + { + Effect = "Allow" + Action = [ + "kms:Decrypt" + ] + Resource = "*" + Condition = { + StringEquals = { + "kms:ViaService" = "ssm.${var.aws_region}.amazonaws.com" + } + } + } + ] + }) +} + +# IAM Role for ECS Task +resource "aws_iam_role" "ecs_task" { + name = "${var.project_name}-ecs-task-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${var.project_name}-ecs-task-role" + } +} diff --git a/terraform/main.tf b/terraform/main.tf deleted file mode 100644 index 57d32bd..0000000 --- a/terraform/main.tf +++ /dev/null @@ -1,241 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } -} - -provider "aws" { - region = var.aws_region -} - -# Data source for availability zones -data "aws_availability_zones" "available" { - state = "available" -} - -# VPC -resource "aws_vpc" "main" { - cidr_block = "10.0.0.0/16" - enable_dns_hostnames = true - enable_dns_support = true - - tags = { - Name = "${var.project_name}-vpc" - } -} - -# Internet Gateway -resource "aws_internet_gateway" "main" { - vpc_id = aws_vpc.main.id - - tags = { - Name = "${var.project_name}-igw" - } -} - -# Public Subnet (single subnet for simplicity) -resource "aws_subnet" "public" { - vpc_id = aws_vpc.main.id - cidr_block = "10.0.1.0/24" - availability_zone = data.aws_availability_zones.available.names[0] - - map_public_ip_on_launch = true - - tags = { - Name = "${var.project_name}-public-subnet" - } -} - -# Route Table for Public Subnet -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}-public-rt" - } -} - -# Route Table Association -resource "aws_route_table_association" "public" { - subnet_id = aws_subnet.public.id - route_table_id = aws_route_table.public.id -} - -# Security Group for ECS Tasks (direct access from internet) -resource "aws_security_group" "ecs_tasks" { - name = "${var.project_name}-ecs-sg" - description = "Security group for ECS tasks - allows HTTP access" - vpc_id = aws_vpc.main.id - - ingress { - description = "HTTP" - 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"] - } - - tags = { - Name = "${var.project_name}-ecs-sg" - } -} - -# CloudWatch Log Group -resource "aws_cloudwatch_log_group" "app" { - name = "/ecs/${var.project_name}" - retention_in_days = 7 - - tags = { - Name = "${var.project_name}-logs" - } -} - -# ECR Repository - using existing repository (data source) -data "aws_ecr_repository" "app" { - name = var.ecr_repository_name -} - -# IAM Role for ECS Task Execution -resource "aws_iam_role" "ecs_task_execution" { - name = "${var.project_name}-ecs-task-execution-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) - - tags = { - Name = "${var.project_name}-ecs-task-execution-role" - } -} - -# IAM Role Policy Attachment for ECS Task Execution -resource "aws_iam_role_policy_attachment" "ecs_task_execution" { - role = aws_iam_role.ecs_task_execution.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" -} - -# IAM Role for ECS Task -resource "aws_iam_role" "ecs_task" { - name = "${var.project_name}-ecs-task-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) - - tags = { - Name = "${var.project_name}-ecs-task-role" - } -} - -# ECS Cluster -resource "aws_ecs_cluster" "main" { - name = "${var.project_name}-cluster" - - tags = { - Name = "${var.project_name}-cluster" - } -} - -# ECS Task Definition -resource "aws_ecs_task_definition" "app" { - family = var.project_name - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = 256 - memory = 512 - execution_role_arn = aws_iam_role.ecs_task_execution.arn - task_role_arn = aws_iam_role.ecs_task.arn - - container_definitions = jsonencode([ - { - name = var.project_name - image = "${data.aws_ecr_repository.app.repository_url}:latest" - - portMappings = [ - { - containerPort = 8000 - protocol = "tcp" - } - ] - - logConfiguration = { - logDriver = "awslogs" - options = { - "awslogs-group" = aws_cloudwatch_log_group.app.name - "awslogs-region" = var.aws_region - "awslogs-stream-prefix" = "ecs" - } - } - - healthCheck = { - command = ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] - interval = 30 - timeout = 5 - retries = 3 - startPeriod = 60 - } - } - ]) - - tags = { - Name = "${var.project_name}-task-definition" - } -} - -# ECS Service -resource "aws_ecs_service" "app" { - name = "${var.project_name}-service" - cluster = aws_ecs_cluster.main.id - task_definition = aws_ecs_task_definition.app.arn - desired_count = 1 - launch_type = "FARGATE" - - network_configuration { - subnets = [aws_subnet.public.id] - security_groups = [aws_security_group.ecs_tasks.id] - assign_public_ip = true - } - - depends_on = [ - aws_iam_role_policy_attachment.ecs_task_execution - ] - - tags = { - Name = "${var.project_name}-service" - } -} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index cee4c9e..7a92b23 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -13,7 +13,28 @@ output "ecs_service_name" { value = aws_ecs_service.app.name } +output "ecs_rabbitmq_service_name" { + description = "Name of the RabbitMQ ECS service" + value = aws_ecs_service.rabbitmq.name +} + output "cloudwatch_log_group" { description = "CloudWatch log group name" value = aws_cloudwatch_log_group.app.name +} + +output "cloudwatch_log_group_rabbitmq" { + description = "CloudWatch log group name for RabbitMQ" + value = aws_cloudwatch_log_group.rabbitmq.name +} + +output "service_discovery_namespace" { + description = "Service Discovery namespace" + value = aws_service_discovery_private_dns_namespace.main.name +} + +output "celery_broker_url_parameter" { + description = "Parameter Store path for Celery broker URL" + value = aws_ssm_parameter.celery_broker_url.name + sensitive = true } \ No newline at end of file diff --git a/terraform/parameter_store.tf b/terraform/parameter_store.tf new file mode 100644 index 0000000..5ff8307 --- /dev/null +++ b/terraform/parameter_store.tf @@ -0,0 +1,45 @@ +# Utwórz URL RabbitMQ i zapisz w Parameter Store +resource "aws_ssm_parameter" "celery_broker_url" { + name = "/${var.project_name}/celery_broker_url" + type = "SecureString" + value = "amqp://${var.rabbitmq_username}:${var.rabbitmq_password}@rabbitmq.${aws_service_discovery_private_dns_namespace.main.name}:5672//" + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${var.project_name}-celery-broker-url" + ManagedBy = "Terraform" + Service = "RabbitMQ" + } +} + +# Lokalne zmienne do mapowania parametrów +locals { + # Automatyczne mapowanie parametrów na secrets dla ECS + ssm_secrets = [ + for param_path in data.aws_ssm_parameters_by_path.app_secrets.names : { + name = upper( + replace( + replace(param_path, "/${var.project_name}/", ""), + "/", + "_" + ) + ) + valueFrom = "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${param_path}" + } + ] + + # Zwykłe zmienne środowiskowe (nie-secrets) + environment_vars = [ + { + name = "ENVIRONMENT" + value = "production" + }, + { + name = "APP_NAME" + value = var.project_name + } + ] +} diff --git a/terraform/security_groups.tf b/terraform/security_groups.tf new file mode 100644 index 0000000..a263c80 --- /dev/null +++ b/terraform/security_groups.tf @@ -0,0 +1,59 @@ +# Security Group for ECS Tasks (direct access from internet) +resource "aws_security_group" "ecs_tasks" { + name = "${var.project_name}-ecs-sg" + description = "Security group for ECS tasks - allows HTTP access" + vpc_id = aws_vpc.main.id + + ingress { + description = "HTTP" + 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"] + } + + tags = { + Name = "${var.project_name}-ecs-sg" + } +} + +# Security Group dla RabbitMQ +resource "aws_security_group" "rabbitmq" { + name = "${var.project_name}-rabbitmq-sg" + description = "Security group for RabbitMQ" + vpc_id = aws_vpc.main.id + + ingress { + description = "AMQP from ECS tasks" + from_port = 5672 + to_port = 5672 + protocol = "tcp" + security_groups = [aws_security_group.ecs_tasks.id] + } + + ingress { + description = "Management UI from ECS tasks" + from_port = 15672 + to_port = 15672 + protocol = "tcp" + security_groups = [aws_security_group.ecs_tasks.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-rabbitmq-sg" + } +} diff --git a/terraform/service_discovery.tf b/terraform/service_discovery.tf new file mode 100644 index 0000000..8f07ad5 --- /dev/null +++ b/terraform/service_discovery.tf @@ -0,0 +1,20 @@ +# Service Discovery Namespace +resource "aws_service_discovery_private_dns_namespace" "main" { + name = "${var.project_name}.local" + description = "Service discovery namespace for ${var.project_name}" + vpc = aws_vpc.main.id +} + +# Service Discovery Service dla RabbitMQ +resource "aws_service_discovery_service" "rabbitmq" { + name = "rabbitmq" + + dns_config { + namespace_id = aws_service_discovery_private_dns_namespace.main.id + + dns_records { + ttl = 10 + type = "A" + } + } +} diff --git a/terraform/terraform.tf b/terraform/terraform.tf new file mode 100644 index 0000000..eb1d295 --- /dev/null +++ b/terraform/terraform.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region +} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index 0c9bb51..815ca60 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -7,3 +7,7 @@ project_name = "fastapi-app" # ECR Repository Configuration # Name of the existing ECR repository (must exist before running terraform apply) ecr_repository_name = "fastapi-app" + +# RabbitMQ +rabbitmq_username = "admin" +rabbitmq_password = "admin" \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf index 3128e72..dfc1b10 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -15,3 +15,16 @@ variable "ecr_repository_name" { type = string default = "fastapi-app" } + +variable "rabbitmq_username" { + description = "RabbitMQ username" + type = string + default = "admin" + sensitive = true +} + +variable "rabbitmq_password" { + description = "RabbitMQ password" + type = string + sensitive = true +} diff --git a/terraform/vpc.tf b/terraform/vpc.tf new file mode 100644 index 0000000..30ab692 --- /dev/null +++ b/terraform/vpc.tf @@ -0,0 +1,52 @@ +# VPC +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-vpc" + } +} + +# Internet Gateway +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project_name}-igw" + } +} + +# Public Subnet (single subnet for simplicity) +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-public-subnet" + } +} + +# Route Table for Public Subnet +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}-public-rt" + } +} + +# Route Table Association +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +}