From f56c9d712a056820bb7d509930eb36b444fc6ded Mon Sep 17 00:00:00 2001 From: Shunsuke Hayashi Date: Mon, 26 Jan 2026 12:31:23 +0900 Subject: [PATCH] feat(aws): add ECS Fargate deployment for Clawdbot Discord bot - Create VPC with public subnets for outbound internet access - Create ECR repository for container images - Create IAM roles (Task Role with DynamoDB/S3 access, Execution Role) - Create ECS Task Definition (Fargate, 2048 MB memory, 256 CPU) - Create ECS Service (1 desired count) - Create CloudWatch Log Group - Add Security Group (outbound only for Discord bot) - Update Dockerfile with .buildstamp to prevent runtime rebuild - Add .env.example with Discord webhook and channel configurations - Update .gitignore to exclude Terraform state files Co-Authored-By: Claude --- .env.example | 13 ++ .gitignore | 8 ++ Dockerfile | 2 + .../terraform/.terraform.lock.hcl | 25 ++++ workers/clawdbot-aws/terraform/dynamodb.tf | 124 +++++++++++++++++ workers/clawdbot-aws/terraform/ecr.tf | 36 +++++ workers/clawdbot-aws/terraform/ecs.tf | 126 ++++++++++++++++++ workers/clawdbot-aws/terraform/iam.tf | 83 ++++++++++++ workers/clawdbot-aws/terraform/outputs.tf | 99 ++++++++++++++ workers/clawdbot-aws/terraform/s3.tf | 60 +++++++++ workers/clawdbot-aws/terraform/variables.tf | 66 +++++++++ workers/clawdbot-aws/terraform/versions.tf | 23 ++++ workers/clawdbot-aws/terraform/vpc.tf | 63 +++++++++ 13 files changed, 728 insertions(+) create mode 100644 workers/clawdbot-aws/terraform/.terraform.lock.hcl create mode 100644 workers/clawdbot-aws/terraform/dynamodb.tf create mode 100644 workers/clawdbot-aws/terraform/ecr.tf create mode 100644 workers/clawdbot-aws/terraform/ecs.tf create mode 100644 workers/clawdbot-aws/terraform/iam.tf create mode 100644 workers/clawdbot-aws/terraform/outputs.tf create mode 100644 workers/clawdbot-aws/terraform/s3.tf create mode 100644 workers/clawdbot-aws/terraform/variables.tf create mode 100644 workers/clawdbot-aws/terraform/versions.tf create mode 100644 workers/clawdbot-aws/terraform/vpc.tf diff --git a/.env.example b/.env.example index 29652fe46..1dfd2ed16 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,16 @@ TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_here # Must be a WhatsApp-enabled Twilio number, prefixed with whatsapp: TWILIO_WHATSAPP_FROM=whatsapp:+17343367101 + +# Discord Webhook (for simple notifications) +DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxx/yyy + +# Discord Notification Channel (for clawdbot connection) +# Default: #status channel (PPAL Server) +# Use Discord channel ID (e.g., "1465087451113722019") +DISCORD_NOTIFY_CHANNEL=1465087451113722019 + +# Discord Second Brain Channel (for memory/knowledge storage) +# Default: #general channel (PPAL Server) +# Use Discord channel ID (e.g., "1465087447225598207") +DISCORD_BRAIN_CHANNEL=1465087447225598207 diff --git a/.gitignore b/.gitignore index e1dcd0188..2941c49c7 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,11 @@ src/**/*.js.map src/**/*.d.ts src/**/*.d.ts.map + +# Terraform +workers/clawdbot-aws/terraform/.terraform/ +workers/clawdbot-aws/terraform/*.tfstate +workers/clawdbot-aws/terraform/*.tfstate.backup +workers/clawdbot-aws/terraform/*.tfplan +workers/clawdbot-aws/terraform/plan.tfplan +workers/clawdbot-aws/terraform/tfplan diff --git a/Dockerfile b/Dockerfile index a33f0077d..77d97f52a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,8 @@ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build +# Create .buildstamp to prevent runtime rebuild +RUN mkdir -p dist && echo "$(date +%s%N)" > dist/.buildstamp # Force pnpm for UI build (Bun may fail on ARM/Synology architectures) ENV CLAWDBOT_PREFER_PNPM=1 RUN pnpm ui:install diff --git a/workers/clawdbot-aws/terraform/.terraform.lock.hcl b/workers/clawdbot-aws/terraform/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/workers/clawdbot-aws/terraform/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# 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", + ] +} diff --git a/workers/clawdbot-aws/terraform/dynamodb.tf b/workers/clawdbot-aws/terraform/dynamodb.tf new file mode 100644 index 000000000..eb87a879e --- /dev/null +++ b/workers/clawdbot-aws/terraform/dynamodb.tf @@ -0,0 +1,124 @@ +/** + * DynamoDB Tables for θ-cycle Session Persistence & Artifact Storage + */ + +resource "aws_dynamodb_table" "sessions" { + name = var.sessions_table_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "sessionId" + + attribute { + name = "sessionId" + type = "S" + } + + attribute { + name = "userId" + type = "S" + } + + attribute { + name = "guildId" + type = "S" + } + + attribute { + name = "status" + type = "S" + } + + global_secondary_index { + name = "UserIndex" + hash_key = "userId" + range_key = "status" + projection_type = "ALL" + } + + global_secondary_index { + name = "GuildIndex" + hash_key = "guildId" + range_key = "status" + projection_type = "ALL" + } + + point_in_time_recovery { + enabled = true + } + + ttl { + attribute_name = "expiresAt" + } + + tags = { + Name = "${var.project_name}-sessions" + } +} + +resource "aws_dynamodb_table" "artifacts" { + name = var.artifacts_table_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "id" + + attribute { + name = "id" + type = "S" + } + + attribute { + name = "sessionId" + type = "S" + } + + attribute { + name = "userId" + type = "S" + } + + attribute { + name = "type" + type = "S" + } + + attribute { + name = "createdAt" + type = "N" + } + + attribute { + name = "expiresAt" + type = "N" + } + + global_secondary_index { + name = "SessionIndex" + hash_key = "sessionId" + range_key = "createdAt" + projection_type = "ALL" + } + + global_secondary_index { + name = "UserIndex" + hash_key = "userId" + range_key = "createdAt" + projection_type = "ALL" + } + + global_secondary_index { + name = "TypeIndex" + hash_key = "type" + range_key = "expiresAt" + projection_type = "ALL" + } + + point_in_time_recovery { + enabled = true + } + + ttl { + attribute_name = "expiresAt" + } + + tags = { + Name = "${var.project_name}-artifacts" + } +} diff --git a/workers/clawdbot-aws/terraform/ecr.tf b/workers/clawdbot-aws/terraform/ecr.tf new file mode 100644 index 000000000..10f7db6e9 --- /dev/null +++ b/workers/clawdbot-aws/terraform/ecr.tf @@ -0,0 +1,36 @@ +/** + * Amazon ECR Repository for Clawdbot Container Images + */ + +resource "aws_ecr_repository" "clawdbot" { + name = "${var.project_name}/bot" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + tags = { + Name = "${var.project_name}-ecr" + } +} + +# ECR Lifecycle Policy (keep last 10 images) +resource "aws_ecr_lifecycle_policy" "clawdbot" { + repository = aws_ecr_repository.clawdbot.name + + policy = jsonencode({ + rules = [{ + rulePriority = 1 + description = "Keep last 10 images" + selection = { + tagStatus = "any" + countType = "imageCountMoreThan" + countNumber = 10 + } + action = { + type = "expire" + } + }] + }) +} diff --git a/workers/clawdbot-aws/terraform/ecs.tf b/workers/clawdbot-aws/terraform/ecs.tf new file mode 100644 index 000000000..f1b42d326 --- /dev/null +++ b/workers/clawdbot-aws/terraform/ecs.tf @@ -0,0 +1,126 @@ +/** + * ECS Fargate Configuration for Clawdbot Discord Bot + */ + +# CloudWatch Log Group +resource "aws_cloudwatch_log_group" "clawdbot" { + name = "/ecs/${var.project_name}" + retention_in_days = 7 + + tags = { + Name = "${var.project_name}-logs" + } +} + +# ECS Cluster +resource "aws_ecs_cluster" "clawdbot" { + name = "${var.project_name}-cluster" + + tags = { + Name = "${var.project_name}-ecs" + } +} + +# Security Group (outbound only, no inbound needed for Discord bot) +resource "aws_security_group" "clawdbot" { + name = "${var.project_name}-sg" + description = "Security group for Clawdbot ECS tasks (outbound only)" + vpc_id = aws_vpc.clawdbot.id + + # No ingress rules needed - Discord bot initiates outbound WebSocket connections + + # Egress (outbound) for internet access + egress { + description = "Allow all outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-sg" + } +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "clawdbot" { + family = "${var.project_name}-task" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.ecs_task_cpu + memory = var.ecs_task_memory + execution_role_arn = aws_iam_role.ecs_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + + container_definitions = jsonencode([ + { + name = "${var.project_name}-container" + image = "${aws_ecr_repository.clawdbot.repository_url}:latest" + cpu = var.ecs_task_cpu + memory = var.ecs_task_memory + essential = true + command = ["node", "dist/entry.js", "gateway", "--allow-unconfigured"] + + environment = [ + { + name = "AWS_REGION" + value = var.aws_region + }, + { + name = "SESSIONS_TABLE_NAME" + value = aws_dynamodb_table.sessions.name + }, + { + name = "ARTIFACTS_TABLE_NAME" + value = aws_dynamodb_table.artifacts.name + }, + { + name = "ARTIFACTS_S3_BUCKET" + value = aws_s3_bucket.artifacts.id + }, + { + name = "DISCORD_BOT_TOKEN" + value = var.discord_token + }, + { + name = "CLAWDBOT_FORCE_BUILD" + value = "0" + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.clawdbot.name + "awslogs-region" = var.aws_region + "awslogs-stream-prefix" = "ecs" + "awslogs-create-group" = "true" + } + } + } + ]) + + tags = { + Name = "${var.project_name}-taskdef" + } +} + +# ECS Service +resource "aws_ecs_service" "clawdbot" { + name = "${var.project_name}-service" + cluster = aws_ecs_cluster.clawdbot.id + task_definition = aws_ecs_task_definition.clawdbot.arn + desired_count = var.ecs_service_desired_count + launch_type = "FARGATE" + + network_configuration { + subnets = aws_subnet.public[*].id + security_groups = [aws_security_group.clawdbot.id] + assign_public_ip = true + } + + tags = { + Name = "${var.project_name}-service" + } +} diff --git a/workers/clawdbot-aws/terraform/iam.tf b/workers/clawdbot-aws/terraform/iam.tf new file mode 100644 index 000000000..1ecf6cd1d --- /dev/null +++ b/workers/clawdbot-aws/terraform/iam.tf @@ -0,0 +1,83 @@ +/** + * IAM Roles for ECS Fargate Task Execution + */ + +# Assume role policy for ECS tasks +data "aws_iam_policy_document" "ecs_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +# ECS Task Execution Role (for ECR pull and CloudWatch logs) +resource "aws_iam_role" "ecs_execution_role" { + name = "${var.project_name}-ecs-execution-role" + assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json +} + +# Attach AmazonECSTaskExecutionRolePolicy (required for Fargate) +resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" { + role = aws_iam_role.ecs_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# ECS Task Role (for application access to DynamoDB and S3) +resource "aws_iam_role" "ecs_task_role" { + name = "${var.project_name}-ecs-task-role" + assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json +} + +# Task Role Policy: DynamoDB access +resource "aws_iam_role_policy" "ecs_task_dynamodb" { + name = "${var.project_name}-dynamodb-access" + role = aws_iam_role.ecs_task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan" + ] + Resource = [ + aws_dynamodb_table.sessions.arn, + "${aws_dynamodb_table.sessions.arn}/index/*", + aws_dynamodb_table.artifacts.arn, + "${aws_dynamodb_table.artifacts.arn}/index/*" + ] + } + ] + }) +} + +# Task Role Policy: S3 access +resource "aws_iam_role_policy" "ecs_task_s3" { + name = "${var.project_name}-s3-access" + role = aws_iam_role.ecs_task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject" + ] + Resource = "${aws_s3_bucket.artifacts.arn}/*" + } + ] + }) +} diff --git a/workers/clawdbot-aws/terraform/outputs.tf b/workers/clawdbot-aws/terraform/outputs.tf new file mode 100644 index 000000000..f20b0700b --- /dev/null +++ b/workers/clawdbot-aws/terraform/outputs.tf @@ -0,0 +1,99 @@ +/** + * Deployment Outputs + */ + +output "sessions_table_name" { + description = "DynamoDB table name for session storage" + value = aws_dynamodb_table.sessions.name +} + +output "artifacts_table_name" { + description = "DynamoDB table name for artifacts metadata" + value = aws_dynamodb_table.artifacts.name +} + +output "artifacts_bucket_name" { + description = "S3 bucket name for artifact storage" + value = aws_s3_bucket.artifacts.id +} + +output "artifacts_bucket_arn" { + description = "S3 bucket ARN" + value = aws_s3_bucket.artifacts.arn +} + +output "sessions_table_arn" { + description = "DynamoDB sessions table ARN" + value = aws_dynamodb_table.sessions.arn +} + +output "artifacts_table_arn" { + description = "DynamoDB artifacts table ARN" + value = aws_dynamodb_table.artifacts.arn +} + +output "environment_variables" { + description = "Required environment variables for clawdbot" + value = { + SESSIONS_TABLE_NAME = aws_dynamodb_table.sessions.name + ARTIFACTS_TABLE_NAME = aws_dynamodb_table.artifacts.name + ARTIFACTS_S3_BUCKET = aws_s3_bucket.artifacts.id + AWS_REGION = var.aws_region + } +} + +# ECS Outputs +output "ecs_cluster_name" { + description = "ECS cluster name" + value = aws_ecs_cluster.clawdbot.name +} + +output "ecs_service_name" { + description = "ECS service name" + value = aws_ecs_service.clawdbot.name +} + +output "ecs_task_definition_family" { + description = "ECS task definition family" + value = aws_ecs_task_definition.clawdbot.family +} + +output "ecr_repository_url" { + description = "ECR repository URL for container images" + value = aws_ecr_repository.clawdbot.repository_url +} + +output "ecr_repository_name" { + description = "ECR repository name" + value = aws_ecr_repository.clawdbot.name +} + +output "cloudwatch_log_group_name" { + description = "CloudWatch Log Group name" + value = aws_cloudwatch_log_group.clawdbot.name +} + +output "vpc_id" { + description = "VPC ID" + value = aws_vpc.clawdbot.id +} + +output "public_subnet_ids" { + description = "Public subnet IDs" + value = aws_subnet.public[*].id +} + +output "security_group_id" { + description = "Security Group ID" + value = aws_security_group.clawdbot.id +} + +output "ecs_task_role_arn" { + description = "ECS Task Role ARN" + value = aws_iam_role.ecs_task_role.arn +} + +output "ecs_execution_role_arn" { + description = "ECS Execution Role ARN" + value = aws_iam_role.ecs_execution_role.arn +} diff --git a/workers/clawdbot-aws/terraform/s3.tf b/workers/clawdbot-aws/terraform/s3.tf new file mode 100644 index 000000000..eb6bc5780 --- /dev/null +++ b/workers/clawdbot-aws/terraform/s3.tf @@ -0,0 +1,60 @@ +/** + * S3 Bucket for Artifact Storage + */ + +resource "aws_s3_bucket" "artifacts" { + bucket = var.artifacts_bucket_name + + tags = { + Name = "${var.project_name}-artifacts" + } +} + +# S3 bucket versioning (disabled for cost optimization) +resource "aws_s3_bucket_versioning" "artifacts" { + bucket = aws_s3_bucket.artifacts.bucket + versioning_configuration { + status = "Suspended" + } +} + +# S3 bucket server-side encryption (SSE-S3) +resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# S3 bucket public access block (private bucket) +resource "aws_s3_bucket_public_access_block" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# S3 bucket lifecycle configuration (auto-expire old artifacts) +resource "aws_s3_bucket_lifecycle_configuration" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + id = "expire-old-artifacts" + status = "Enabled" + + filter {} + + expiration { + days = var.artifacts_bucket_ttl_days + } + + noncurrent_version_expiration { + noncurrent_days = 1 + } + } +} diff --git a/workers/clawdbot-aws/terraform/variables.tf b/workers/clawdbot-aws/terraform/variables.tf new file mode 100644 index 000000000..38075ca03 --- /dev/null +++ b/workers/clawdbot-aws/terraform/variables.tf @@ -0,0 +1,66 @@ +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "ap-northeast-1" +} + +variable "environment" { + description = "Environment name (e.g., production, staging)" + type = string + default = "production" +} + +variable "project_name" { + description = "Project name used for resource naming" + type = string + default = "clawdbot" +} + +variable "sessions_table_name" { + description = "DynamoDB table name for session storage" + type = string + default = "clawdbot-sessions" +} + +variable "artifacts_table_name" { + description = "DynamoDB table name for artifacts metadata" + type = string + default = "clawdbot-artifacts" +} + +variable "artifacts_bucket_name" { + description = "S3 bucket name for artifact storage" + type = string + default = "clawdbot-artifacts" +} + +variable "artifacts_bucket_ttl_days" { + description = "S3 bucket lifecycle TTL in days" + type = number + default = 7 +} + +# ECS Fargate Configuration +variable "ecs_task_cpu" { + description = "ECS task CPU units (256 = 0.25 vCPU)" + type = number + default = 256 +} + +variable "ecs_task_memory" { + description = "ECS task memory in MB" + type = number + default = 2048 +} + +variable "ecs_service_desired_count" { + description = "Desired number of ECS tasks" + type = number + default = 1 +} + +variable "discord_token" { + description = "Discord bot token (DISCORD_BOT_TOKEN) - sensitive, do not commit to version control" + type = string + sensitive = true +} diff --git a/workers/clawdbot-aws/terraform/versions.tf b/workers/clawdbot-aws/terraform/versions.tf new file mode 100644 index 000000000..55d3796f6 --- /dev/null +++ b/workers/clawdbot-aws/terraform/versions.tf @@ -0,0 +1,23 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + Component = "clawdbot-backend" + } + } +} diff --git a/workers/clawdbot-aws/terraform/vpc.tf b/workers/clawdbot-aws/terraform/vpc.tf new file mode 100644 index 000000000..6a6f0b462 --- /dev/null +++ b/workers/clawdbot-aws/terraform/vpc.tf @@ -0,0 +1,63 @@ +/** + * VPC for Clawdbot ECS Fargate Deployment + * Discord bot requires outbound internet access for WebSocket connections + */ + +# VPC +resource "aws_vpc" "clawdbot" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-vpc" + } +} + +# Internet Gateway for outbound internet access +resource "aws_internet_gateway" "clawdbot" { + vpc_id = aws_vpc.clawdbot.id + + tags = { + Name = "${var.project_name}-igw" + } +} + +# Public Subnets (for Fargate tasks with public IP) +resource "aws_subnet" "public" { + count = 2 + vpc_id = aws_vpc.clawdbot.id + cidr_block = cidrsubnet(aws_vpc.clawdbot.cidr_block, 8, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-public-${count.index + 1}" + } +} + +# Route Table for public subnets +resource "aws_route_table" "public" { + vpc_id = aws_vpc.clawdbot.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.clawdbot.id + } + + tags = { + Name = "${var.project_name}-public-rt" + } +} + +# Route Table Association for public subnets +resource "aws_route_table_association" "public" { + count = 2 + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +# Data source for available AZs +data "aws_availability_zones" "available" { + state = "available" +}