{"id":143,"date":"2026-01-03T12:00:00","date_gmt":"2026-01-03T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=143"},"modified":"2026-10-30T06:59:13","modified_gmt":"2026-10-30T06:59:13","slug":"aws-full-stack-infrastructure-dns-to-database-with-terraform","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=143","title":{"rendered":"AWS Full-Stack Infrastructure: DNS to Database with Terraform"},"content":{"rendered":"<p class=\"wp-block-paragraph\">After deploying load balancers, auto scaling, and monitoring separately, I wanted to build something real a complete production-ready three-tier application with:<\/p><ul class=\"wp-block-list\"><li>Custom domain DNS<\/li><li>SSL certificates<\/li><li>Load balancing with path-based routing<\/li><li>Multiple applications (static HTML + Java Spring Boot)<\/li><li>Multi-AZ RDS MySQL database<\/li><li>Proper security layers<\/li><li>Bastion host for secure access<\/li><\/ul><p class=\"wp-block-paragraph\">This is the project that ties everything together DNS to Database, fully automated with Terraform.<\/p><h3 class=\"wp-block-heading\">What I Built<\/h3><p class=\"wp-block-paragraph\">A complete three-tier architecture running three different applications:<\/p><pre class=\"wp-block-code\"><code> Internet\n                       \u2502\n                       \u25bc\n            [Route53: dns-to-db.rezaops.com]\n                       \u2502\n                       \u25bc\n              [ACM SSL Certificate]\n                       \u2502\n                       \u25bc\n         [Application Load Balancer - HTTPS]\n                       \u2502\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n          \u2502            \u2502            \u2502\n     \/app1\/*       \/app2\/*        \/*\n          \u2502            \u2502            \u2502\n          \u25bc            \u25bc            \u25bc\n    [App1 TG]    [App2 TG]    [App3 TG]\n    Port 80      Port 80      Port 8080\n          \u2502            \u2502            \u2502\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510\n    \u2502 App1-AZ1 \u2502 \u2502 App2-AZ1 \u2502 \u2502 App3-AZ1 \u2502\n    \u2502 App1-AZ2 \u2502 \u2502 App2-AZ2 \u2502 \u2502 App3-AZ2 \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518\n                                     \u2502\n                                     \u25bc\n                    [RDS MySQL Multi-AZ Database]\n                         Primary: us-east-1a\n                         Standby: us-east-1b<\/code><\/pre><h3 class=\"wp-block-heading\">Three Applications Explained<\/h3><h3 class=\"wp-block-heading\">App1 &#8211; Static HTML Application (Pink Theme)<\/h3><p class=\"wp-block-paragraph\">Simple static website on port 80:<\/p><ul class=\"wp-block-list\"><li>Background: Pink (rgb(250, 210, 210))<\/li><li>Displays instance metadata<\/li><li>Health check: \/app1\/index.html<\/li><li>URL: https:\/\/dns-to-db.rezaops.com\/app1\/<\/li><\/ul><h3 class=\"wp-block-heading\">App2 &#8211; Static HTML Application (Teal Theme)<\/h3><p class=\"wp-block-paragraph\">Another static website on port 80:<\/p><ul class=\"wp-block-list\"><li>Background: Teal (rgb(15, 232, 192))<\/li><li>Displays instance metadata<\/li><li>Health check: \/app2\/index.html<\/li><li>URL: https:\/\/dns-to-db.rezaops.com\/app2\/<\/li><\/ul><h3 class=\"wp-block-heading\">App3 &#8211; Java Spring Boot User Management System<\/h3><p class=\"wp-block-paragraph\">Real application with database on port 8080:<\/p><ul class=\"wp-block-list\"><li>User registration and login<\/li><li>MySQL database backend<\/li><li>CRUD operations (Create, Read, Update, Delete users)<\/li><li>Health check: \/login<\/li><li>URL: https:\/\/dns-to-db.rezaops.com\/ (default route)<\/li><\/ul><h3 class=\"wp-block-heading\">Complete Technical Architecture<\/h3><h3 class=\"wp-block-heading\">Network Layer (3-Tier Subnets)<\/h3><pre class=\"wp-block-code\"><code># VPC: 10.0.0.0\/16\nmodule \"vpc\" {\n  source = \"terraform-aws-modules\/vpc\/aws\"\n  version = \"5.13.0\"\n\n  name = \"${local.name}-${var.vpc_name}\"\n  cidr = \"10.0.0.0\/16\"\n  azs = [\"us-east-1a\", \"us-east-1b\"]\n\n  # Public Subnets (for ALB and Bastion)\n  public_subnets = [\"10.0.101.0\/24\", \"10.0.102.0\/24\"]\n\n  # Private Subnets (for applications)\n  private_subnets = [\"10.0.1.0\/24\", \"10.0.2.0\/24\"]\n\n  # Database Subnets (for RDS)\n  database_subnets = [\"10.0.151.0\/24\", \"10.0.152.0\/24\"]\n\n  create_database_subnet_group = true\n  create_database_subnet_route_table = true\n\n  enable_nat_gateway = true\n  single_nat_gateway = true\n\n  enable_dns_hostnames = true\n  enable_dns_support = true\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Subnet architecture explained:<\/p><p class=\"wp-block-paragraph\">Public Subnets (10.0.101.0\/24, 10.0.102.0\/24):<\/p><ul class=\"wp-block-list\"><li>Resources: ALB, Bastion Host<\/li><li>Internet Gateway attached<\/li><li>Public IP addresses assigned<\/li><li>Route: 0.0.0.0\/0 \u2192 Internet Gateway<\/li><\/ul><p class=\"wp-block-paragraph\">Private Subnets (10.0.1.0\/24, 10.0.2.0\/24):<\/p><ul class=\"wp-block-list\"><li>Resources: App1, App2, App3 EC2 instances<\/li><li>No public IP addresses<\/li><li>Route: 0.0.0.0\/0 \u2192 NAT Gateway<\/li><li>Can access internet for updates<\/li><li>Not directly accessible from internet<\/li><\/ul><p class=\"wp-block-paragraph\">Database Subnets (10.0.151.0\/24, 10.0.152.0\/24):<\/p><ul class=\"wp-block-list\"><li>Resources: RDS MySQL instances<\/li><li>No internet access (no NAT Gateway route)<\/li><li>Complete isolation from internet<\/li><li>Only accessible from private subnets<\/li><\/ul><p class=\"wp-block-paragraph\">Why separate database subnets?<\/p><p class=\"wp-block-paragraph\">Without separate subnets:<\/p><p class=\"wp-block-paragraph\">Private subnet: Apps + Database mixed<\/p><p class=\"wp-block-paragraph\">Risk: If app compromised, database directly accessible<\/p><p class=\"wp-block-paragraph\">With separate subnets:<\/p><p class=\"wp-block-paragraph\">Private: Apps only<\/p><p class=\"wp-block-paragraph\">Database: Database only + separate security group<\/p><p class=\"wp-block-paragraph\">Result: Additional security layer, database completely isolated<\/p><p class=\"wp-block-paragraph\">create_database_subnet_route_table = true:<\/p><p class=\"wp-block-paragraph\">This creates a separate route table for database subnets:<\/p><p class=\"wp-block-paragraph\">Without this:<\/p><pre class=\"wp-block-code\"><code>Database subnets use private subnet route table:\n  0.0.0.0\/0 \u2192 NAT Gateway\nResult: Database has internet access (not ideal)<\/code><\/pre><p class=\"wp-block-paragraph\">With this:<\/p><pre class=\"wp-block-code\"><code>Database subnets get own route table:\n  10.0.0.0\/16 \u2192 local (VPC only)\nResult: Database completely isolated from internet<\/code><\/pre><h3 class=\"wp-block-heading\">ALB Path-Based Routing<\/h3><p class=\"wp-block-paragraph\">This is the core routing logic:<\/p><pre class=\"wp-block-code\"><code>listeners = {\n  # HTTP \u2192 HTTPS Redirect\n  my-http-https-redirect = {\n    port = 80\n    protocol = \"HTTP\"\n    redirect = {\n      port = \"443\"\n      protocol = \"HTTPS\"\n      status_code = \"HTTP_301\"\n    }\n  }\n\n  # HTTPS Listener with Path Routing\n  my-https-listener = {\n    port = 443\n    protocol = \"HTTPS\"\n    ssl_policy = \"ELBSecurityPolicy-TLS13-1-2-Res-2021-06\"\n    certificate_arn = module.acm.acm_certificate_arn\n\n    # Default fixed response\n    fixed_response = {\n      content_type = \"text\/plain\"\n      message_body = \"Fixed Static message - for Root Context\"\n      status_code = \"200\"\n    }\n\n    rules = {\n      # Rule 1: App1 routing\n      myapp1-rule = {\n        priority = 10\n        actions = [{\n          type = \"weighted-forward\"\n          target_groups = [{\n            target_group_key = \"mytg1\"\n            weight = 1\n          }]\n          stickiness = {\n            enabled = true\n            duration = 3600\n          }\n        }]\n        conditions = [{\n          path_pattern = {\n            values = [\"\/app1*\"]\n          }\n        }]\n      }\n\n      # Rule 2: App2 routing\n      myapp2-rule = {\n        priority = 20\n        actions = [{\n          type = \"weighted-forward\"\n          target_groups = [{\n            target_group_key = \"mytg2\"\n            weight = 1\n          }]\n          stickiness = {\n            enabled = true\n            duration = 3600\n          }\n        }]\n        conditions = [{\n          path_pattern = {\n            values = [\"\/app2*\"]\n          }\n        }]\n      }\n\n      # Rule 3: App3 routing (default catch-all)\n      myapp3-rule = {\n        priority = 30\n        actions = [{\n          type = \"weighted-forward\"\n          target_groups = [{\n            target_group_key = \"mytg3\"\n            weight = 1\n          }]\n          stickiness = {\n            enabled = true\n            duration = 3600\n          }\n        }]\n        conditions = [{\n          path_pattern = {\n            values = [\"\/*\"]\n          }\n        }]\n      }\n    }\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Rule priority explained:<\/p><pre class=\"wp-block-code\"><code>myapp1-rule: priority = 10\nmyapp2-rule: priority = 20\nmyapp3-rule: priority = 30<\/code><\/pre><p class=\"wp-block-paragraph\">ALB evaluates rules in priority order (lowest number first):<\/p><pre class=\"wp-block-code\"><code>Request: GET \/app1\/users\n\nPriority 10: \/app1* \u2192 MATCH! Forward to mytg1\n(Rules 20, 30 never evaluated)\n\nRequest: GET \/app2\/products\n\nPriority 10: \/app1* \u2192 No match\nPriority 20: \/app2* \u2192 MATCH! Forward to mytg2\n(Rule 30 never evaluated)\n\nRequest: GET \/login\n\nPriority 10: \/app1* \u2192 No match\nPriority 20: \/app2* \u2192 No match\nPriority 30: \/* \u2192 MATCH! Forward to mytg3 (catches everything)<\/code><\/pre><p class=\"wp-block-paragraph\">Why App3 uses \/* pattern:<\/p><p class=\"wp-block-paragraph\">This makes App3 the default application:<\/p><ul class=\"wp-block-list\"><li>Any path not matching \/app1* or \/app2* goes to App3<\/li><li>Perfect for main application or landing page<\/li><li>Ensures no 404 errors for unmapped paths<\/li><\/ul><p class=\"wp-block-paragraph\">Path pattern wildcards:<\/p><pre class=\"wp-block-code\"><code>\"\/app1*\"  # Matches:\n  \/app1\n  \/app1\/\n  \/app1\/users\n  \/app1\/users\/123\n  \/app1\/anything\/else\n\n\"\/*\"  # Matches:\n  \/\n  \/login\n  \/dashboard\n  \/any\/path\/at\/all<\/code><\/pre><h3 class=\"wp-block-heading\">Target Groups for Different Ports<\/h3><p class=\"wp-block-paragraph\">Notice App3 uses port 8080, not 80:<\/p><pre class=\"wp-block-code\"><code>target_groups = {\n  # App1 &amp; App2: HTTP on port 80\n  mytg1 = {\n    protocol = \"HTTP\"\n    port = 80\n    health_check = {\n      path = \"\/app1\/index.html\"\n    }\n  }\n\n  mytg2 = {\n    protocol = \"HTTP\"\n    port = 80\n    health_check = {\n      path = \"\/app2\/index.html\"\n    }\n  }\n\n  # App3: HTTP on port 8080 (Java app)\n  mytg3 = {\n    protocol = \"HTTP\"\n    port = 8080  # Different port!\n    health_check = {\n      path = \"\/login\"  # Java endpoint\n    }\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Why App3 uses port 8080:<\/p><p class=\"wp-block-paragraph\">Java Spring Boot defaults to 8080:<\/p><pre class=\"wp-block-code\"><code># App1\/App2 (Apache):\ncurl http:\/\/instance-ip:80\/app1\/  # Works\n\n# App3 (Spring Boot):\ncurl http:\/\/instance-ip:8080\/login  # Works\ncurl http:\/\/instance-ip:80\/login    # Connection refused!<\/code><\/pre><p class=\"wp-block-paragraph\">ALB handles port translation:<\/p><p class=\"wp-block-paragraph\">User request:<\/p><pre class=\"wp-block-code\"><code>https:\/\/dns-to-db.rezaops.com\/login<\/code><\/pre><p class=\"wp-block-paragraph\">ALB receives on port 443:<\/p><ol class=\"wp-block-list\"><li>Matches rule priority 30 (\/*)<\/li><li>Forwards to mytg3<\/li><li>Translates to port 8080<\/li><li>Sends to instance: http:\/\/10.0.1.5:8080\/login<\/li><\/ol><p class=\"wp-block-paragraph\">User never knows about port 8080!<\/p><h3 class=\"wp-block-heading\">Target Group Attachments<\/h3><p class=\"wp-block-paragraph\">This is critical &#8211; we attach instances to target groups manually:<\/p><pre class=\"wp-block-code\"><code># App1 Attachment\nresource \"aws_lb_target_group_attachment\" \"mytg1\" {\n  for_each = {for k,v in module.ec2_private_app1: k =&gt; v}\n  target_group_arn = module.alb.target_groups[\"mytg1\"].arn\n  target_id = each.value.id\n  port = 80\n}\n\n# App2 Attachment\nresource \"aws_lb_target_group_attachment\" \"mytg2\" {\n  for_each = {for k,v in module.ec2_private_app2: k =&gt; v}\n  target_group_arn = module.alb.target_groups[\"mytg2\"].arn\n  target_id = each.value.id\n  port = 80\n}\n\n# App3 Attachment\nresource \"aws_lb_target_group_attachment\" \"mytg3\" {\n  for_each = {for k,v in module.ec2_private_app3: k =&gt; v}\n  target_group_arn = module.alb.target_groups[\"mytg3\"].arn\n  target_id = each.value.id\n  port = 8080  # Java app port\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Why manual attachment?<\/p><pre class=\"wp-block-code\"><code># In target group definition:\ncreate_attachment = false<\/code><\/pre><p class=\"wp-block-paragraph\">Reason: We use for_each to create 2 instances per app:<\/p><pre class=\"wp-block-code\"><code>module \"ec2_private_app1\" {\n  for_each = toset([\"0\", \"1\"])  # Creates 2 instances\n  # ...\n}<\/code><\/pre><p class=\"wp-block-paragraph\">If we don&#8217;t use manual attachment:<\/p><ul class=\"wp-block-list\"><li>Module tries to attach instances automatically<\/li><li>Conflicts with our for_each pattern<\/li><li>Results in errors<\/li><\/ul><p class=\"wp-block-paragraph\">With manual attachment:<\/p><pre class=\"wp-block-code\"><code>for_each = {for k,v in module.ec2_private_app1: k =&gt; v}<\/code><\/pre><p class=\"wp-block-paragraph\">This loops through:<\/p><pre class=\"wp-block-code\"><code>k=\"0\", v=instance_object_1 \u2192 Attach instance 1\nk=\"1\", v=instance_object_2 \u2192 Attach instance 2<\/code><\/pre><p class=\"wp-block-paragraph\">Perfect! Both instances attached correctly.<\/p><h3 class=\"wp-block-heading\">RDS MySQL Multi-AZ Database<\/h3><h3 class=\"wp-block-heading\">RDS Module Configuration<\/h3><pre class=\"wp-block-code\"><code>module \"db\" {\n  source = \"terraform-aws-modules\/rds\/aws\"\n  version = \"6.5.2\"\n\n  identifier = \"${local.name}-database\"\n\n  engine = \"mysql\"\n  engine_version = \"8.0\"\n  family = \"mysql8.0\"\n  major_engine_version = \"8.0\"\n  instance_class = \"db.t3.micro\"\n\n  allocated_storage = 20\n  max_allocated_storage = 100\n  storage_type = \"gp2\"\n  storage_encrypted = true\n\n  db_name = \"mydb\"\n  username = var.db_username\n  password = var.db_password\n  manage_master_user_password = false\n\n  multi_az = true\n  db_subnet_group_name = module.vpc.database_subnet_group_name\n  vpc_security_group_ids = [module.db_security_group.security_group_id]\n\n  backup_retention_period = 7\n  backup_window = \"03:00-06:00\"\n  maintenance_window = \"Mon:00:00-Mon:03:00\"\n\n  performance_insights_enabled = true\n  performance_insights_retention_period = 7\n\n  monitoring_interval = 60\n  monitoring_role_name = \"rds-monitoring-role\"\n  create_monitoring_role = true\n\n  deletion_protection = false\n  skip_final_snapshot = true\n  delete_automated_backups = true\n}<\/code><\/pre><p class=\"wp-block-paragraph\">multi_az = true explained:<\/p><p class=\"wp-block-paragraph\">Without Multi-AZ:<\/p><pre class=\"wp-block-code\"><code>Single RDS instance in us-east-1a\n\nFailure: AZ outage \u2192 Database DOWN\nDowntime: Until AZ recovers (hours)<\/code><\/pre><p class=\"wp-block-paragraph\">With Multi-AZ:<\/p><pre class=\"wp-block-code\"><code>Primary instance: us-east-1a\nStandby instance: us-east-1b (synchronous replication)\n\nFailure: Primary fails \u2192 Automatic failover (60-120 seconds)\nDowntime: Minimal (DNS endpoint updates automatically)<\/code><\/pre><p class=\"wp-block-paragraph\">How Multi-AZ works:<\/p><p class=\"wp-block-paragraph\">Write operation:<\/p><pre class=\"wp-block-code\"><code>App writes to primary (us-east-1a)\nPrimary synchronously replicates to standby (us-east-1b)\nWrite confirmed only after standby acknowledges<\/code><\/pre><p class=\"wp-block-paragraph\">Read operation:<\/p><p class=\"wp-block-paragraph\">All reads go to primary (standby not readable in Multi-AZ)<\/p><p class=\"wp-block-paragraph\">Failover:<\/p><p class=\"wp-block-paragraph\">AWS automatically switches endpoint to standby instance<\/p><p class=\"wp-block-paragraph\">max_allocated_storage = 100:<\/p><pre class=\"wp-block-code\"><code>Initial: 20 GB\nAuto-scaling enabled up to: 100 GB<\/code><\/pre><p class=\"wp-block-paragraph\">What happens:<\/p><pre class=\"wp-block-code\"><code>Storage reaches 90% full (18 GB used)\nAWS automatically increases storage by 10%\nStorage: 20 GB \u2192 22 GB\nNo downtime during scaling!<\/code><\/pre><p class=\"wp-block-paragraph\">Performance Insights:<\/p><pre class=\"wp-block-code\"><code>performance_insights_enabled = true\nperformance_insights_retention_period = 7<\/code><\/pre><p class=\"wp-block-paragraph\">This gives you:<\/p><ul class=\"wp-block-list\"><li>Query performance analysis<\/li><li>Top SQL statements by load<\/li><li>Wait event analysis<\/li><li>Database load visualization<\/li><\/ul><p class=\"wp-block-paragraph\">Free tier: 7 days retention Paid: Up to 2 years retention ($0.01\/vCPU\/hour)<\/p><p class=\"wp-block-paragraph\">Enhanced Monitoring:<\/p><pre class=\"wp-block-code\"><code>monitoring_interval = 60<\/code><\/pre><p class=\"wp-block-paragraph\">Monitors:<\/p><ul class=\"wp-block-list\"><li>CPU utilization (process-level)<\/li><li>Memory usage<\/li><li>File system usage<\/li><li>Disk I\/O<\/li><li>Network throughput<\/li><\/ul><p class=\"wp-block-paragraph\">More detailed than CloudWatch standard metrics!<\/p><h3 class=\"wp-block-heading\">RDS Security Group<\/h3><pre class=\"wp-block-code\"><code>module \"db_security_group\" {\n  source = \"terraform-aws-modules\/security-group\/aws\"\n  version = \"5.1.0\"\n\n  name = \"${local.name}-db-sg\"\n  vpc_id = module.vpc.vpc_id\n\n  ingress_with_cidr_blocks = [\n    {\n      from_port = 3306\n      to_port = 3306\n      protocol = \"tcp\"\n      cidr_blocks = module.vpc.vpc_cidr_block\n      description = \"MySQL access from VPC\"\n    }\n  ]\n\n  egress_rules = [\"all-all\"]\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Why allow entire VPC CIDR?<\/p><pre class=\"wp-block-code\"><code>cidr_blocks = module.vpc.vpc_cidr_block  # 10.0.0.0\/16<\/code><\/pre><p class=\"wp-block-paragraph\">This allows:<\/p><ul class=\"wp-block-list\"><li>App3 instances (private subnets) \u2192 Database<\/li><li>Bastion host (public subnet) \u2192 Database<\/li><li>Any resource in VPC \u2192 Database on port 3306<\/li><\/ul><p class=\"wp-block-paragraph\">Alternative (more restrictive):<\/p><pre class=\"wp-block-code\"><code># Only allow specific security groups:\ningress_with_source_security_group_id = [\n  {\n    from_port = 3306\n    to_port = 3306\n    protocol = \"tcp\"\n    source_security_group_id = module.app3_security_group.security_group_id\n    description = \"MySQL from App3 only\"\n  }\n]<\/code><\/pre><p class=\"wp-block-paragraph\">Result:<\/p><ul class=\"wp-block-list\"><li>More secure (only App3 can connect)<\/li><li>Less convenient (bastion can&#8217;t connect for debugging)<\/li><\/ul><p class=\"wp-block-paragraph\">Trade-off: Security vs convenience for debugging.<\/p><p class=\"wp-block-paragraph\">For production: Restrict to private subnets only!<\/p><h3 class=\"wp-block-heading\">Secrets Management<\/h3><p class=\"wp-block-paragraph\">Database credentials are managed securely:<\/p><pre class=\"wp-block-code\"><code># secrets.tfvars (NOT committed to git)\ndb_username = \"admin\"\ndb_password = \"dbpassword11\"<\/code><\/pre><pre class=\"wp-block-code\"><code># variables.tf\nvariable \"db_username\" {\n  description = \"Database username\"\n  type = string\n  sensitive = true\n}\n\nvariable \"db_password\" {\n  description = \"Database password\"\n  type = string\n  sensitive = true\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Why separate file?<\/p><pre class=\"wp-block-code\"><code># .gitignore\n*.tfvars\n!example.tfvars<\/code><\/pre><p class=\"wp-block-paragraph\">Result:<\/p><ul class=\"wp-block-list\"><li>Credentials never committed to version control<\/li><li>Different passwords per environment<\/li><li>Team members use their own secrets.tfvars<\/li><\/ul><p class=\"wp-block-paragraph\">Variable marked as sensitive:<\/p><pre class=\"wp-block-code\"><code>variable \"db_password\" {\n  sensitive = true\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Effect:<\/p><pre class=\"wp-block-code\"><code># Terraform output:\npassword = &lt;sensitive&gt;<\/code><\/pre><p class=\"wp-block-paragraph\">Terraform never shows the actual value in:<\/p><ul class=\"wp-block-list\"><li>Plan output<\/li><li>Apply output<\/li><li>State file display<\/li><li>Logs<\/li><\/ul><h3 class=\"wp-block-heading\">App3 Template File with RDS Endpoint<\/h3><p class=\"wp-block-paragraph\">App3 needs the database endpoint at launch time. We use Terraform&#8217;s templatefile function:<\/p><pre class=\"wp-block-code\"><code>module \"ec2_private_app3\" {\n  source = \"terraform-aws-modules\/ec2-instance\/aws\"\n  version = \"5.6.0\"\n\n  for_each = toset([\"0\", \"1\"])\n  name = \"${local.name}-app3-${each.key}\"\n  ami = data.aws_ami.amzn2.id\n  instance_type = \"t2.micro\"\n  subnet_id = element(module.vpc.private_subnets, tonumber(each.key))\n  vpc_security_group_ids = [module.app3_security_group.security_group_id]\n  iam_instance_profile = aws_iam_instance_profile.ssm_profile.name\n\n  user_data = base64encode(templatefile(\"${path.module}\/app3-ums-install.tmpl\", {\n    rds_endpoint = module.db.db_instance_endpoint\n    rds_username = var.db_username\n    rds_password = var.db_password\n    rds_dbname = \"mydb\"\n  }))\n\n  depends_on = [module.db]\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Template file (app3-ums-install.tmpl):<\/p><pre class=\"wp-block-code\"><code>#!\/bin\/bash\nyum update -y\n\nyum install -y java-17-amazon-corretto\n\nyum install -y mysql\n\nmkdir -p \/home\/ec2-user\/app3\ncd \/home\/ec2-user\/app3\n\ncat &gt; application.properties &lt;&lt; 'EOF'\nspring.datasource.url=jdbc:mysql:\/\/${rds_endpoint}\/${rds_dbname}\nspring.datasource.username=${rds_username}\nspring.datasource.password=${rds_password}\nspring.jpa.hibernate.ddl-auto=update\nEOF\n\njava -jar app3-ums.jar &amp;&amp; echo \"App3 started successfully\"<\/code><\/pre><p class=\"wp-block-paragraph\">How templatefile works:<\/p><p class=\"wp-block-paragraph\">Before rendering:<\/p><pre class=\"wp-block-code\"><code>spring.datasource.url=jdbc:mysql:\/\/${rds_endpoint}\/${rds_dbname}<\/code><\/pre><p class=\"wp-block-paragraph\">After rendering (Terraform substitutes):<\/p><pre class=\"wp-block-code\"><code>spring.datasource.url=jdbc:mysql:\/\/dns-db-database.c9xyz.us-east-1.rds.amazonaws.com:3306\/mydb<\/code><\/pre><p class=\"wp-block-paragraph\">Why this is necessary:<\/p><p class=\"wp-block-paragraph\">Database endpoint is not known until RDS is created:<\/p><pre class=\"wp-block-code\"><code>Phase 1: Terraform creates RDS\n  \u2192 Endpoint generated: dns-db-database.c9xyz.us-east-1.rds.amazonaws.com:3306\n\nPhase 2: Terraform renders template with endpoint\n  \u2192 User data contains real endpoint\n\nPhase 3: Terraform creates App3 instances\n  \u2192 Instances boot with correct database connection<\/code><\/pre><p class=\"wp-block-paragraph\">Depends_on ensures correct order:<\/p><pre class=\"wp-block-code\"><code>depends_on = [module.db]<\/code><\/pre><p class=\"wp-block-paragraph\">Resource creation order:<\/p><pre class=\"wp-block-code\"><code>1. VPC created\n2. RDS created (takes 15-20 minutes)\n3. App3 instances created (after RDS ready)\n4. App3 connects to database successfully<\/code><\/pre><h3 class=\"wp-block-heading\">Bastion Host Setup<\/h3><p class=\"wp-block-paragraph\">Public EC2 instance for secure access to private resources:<\/p><pre class=\"wp-block-code\"><code>module \"ec2_public\" {\n  source = \"terraform-aws-modules\/ec2-instance\/aws\"\n  version = \"5.6.0\"\n\n  name = \"${local.name}-bastion\"\n  ami = data.aws_ami.amzn2.id\n  instance_type = \"t2.micro\"\n  key_name = var.key_name\n  subnet_id = module.vpc.public_subnets[0]\n  vpc_security_group_ids = [module.bastion_security_group.security_group_id]\n  associate_public_ip_address = true\n  iam_instance_profile = aws_iam_instance_profile.ssm_profile.name\n\n  user_data = file(\"${path.module}\/jumpbox-install.sh\")\n}\n\n# Elastic IP for stable address\nresource \"aws_eip\" \"bastion_eip\" {\n  instance = module.ec2_public.id\n  domain = \"vpc\"\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Bastion user data (jumpbox-install.sh):<\/p><pre class=\"wp-block-code\"><code>#!\/bin\/bash\nyum update -y\n\nyum install -y mysql\n\n# Tools for debugging\nyum install -y telnet nc\n\necho \"Bastion host ready\" &gt; \/tmp\/bastion-ready.txt<\/code><\/pre><p class=\"wp-block-paragraph\">Why install MySQL client?<\/p><p class=\"wp-block-paragraph\">Connect to RDS from bastion:<\/p><pre class=\"wp-block-code\"><code>ssh -i private-key.pem ec2-user@&lt;bastion-eip&gt;\n\n# From bastion:\nmysql -h &lt;rds-endpoint&gt; -u admin -p\n\nEnter password: dbpassword11\n\nmysql&gt; SHOW DATABASES;\n+--------------------+\n| Database           |\n+--------------------+\n| information_schema |\n| mydb               |\n| mysql              |\n+--------------------+<\/code><\/pre><h3 class=\"wp-block-heading\">SSH Key Provisioning<\/h3><p class=\"wp-block-paragraph\">Copy private key to bastion for accessing private instances:<\/p><pre class=\"wp-block-code\"><code>resource \"null_resource\" \"copy_key_to_bastion\" {\n  depends_on = [module.ec2_public]\n\n  connection {\n    type = \"ssh\"\n    host = aws_eip.bastion_eip.public_ip\n    user = \"ec2-user\"\n    private_key = file(\"private-key\/${var.key_name}.pem\")\n  }\n\n  provisioner \"file\" {\n    source = \"private-key\/${var.key_name}.pem\"\n    destination = \"\/tmp\/${var.key_name}.pem\"\n  }\n\n  provisioner \"remote-exec\" {\n    inline = [\n      \"sudo chmod 400 \/tmp\/${var.key_name}.pem\",\n      \"sudo cp \/tmp\/${var.key_name}.pem \/home\/ec2-user\/\",\n      \"sudo chown ec2-user:ec2-user \/home\/ec2-user\/${var.key_name}.pem\",\n      \"echo 'Key copied successfully' &gt; \/tmp\/key-copy.log\"\n    ]\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">What this does:<\/p><ol class=\"wp-block-list\"><li>Waits for bastion: depends_on = [module.ec2_public]<\/li><li>Connects via SSH: Using Elastic IP<\/li><li>Uploads private key: From local private-key\/ to bastion<\/li><li>Sets permissions: chmod 400 (read-only for owner)<\/li><li>Creates log file: Documents VPC creation<\/li><\/ol><p class=\"wp-block-paragraph\">Why copy key to bastion?<\/p><p class=\"wp-block-paragraph\">Access flow:<\/p><pre class=\"wp-block-code\"><code>Your Laptop\n  \u2192 SSH to Bastion (with key)\n  \u2192 SSH to Private Instance (with key on bastion)\n  \u2192 Private instance accessible!<\/code><\/pre><p class=\"wp-block-paragraph\">Without copied key:<\/p><pre class=\"wp-block-code\"><code>Your Laptop\n  \u2192 SSH to Bastion \u2713\n  \u2192 SSH to Private Instance \u2717 (no key on bastion!)<\/code><\/pre><p class=\"wp-block-paragraph\">Security consideration:<\/p><p class=\"wp-block-paragraph\">Copying private keys to bastion is convenient but risky. Better alternatives:<\/p><ul class=\"wp-block-list\"><li>SSH agent forwarding<\/li><li>AWS Systems Manager Session Manager<\/li><li>Temporary keys via AWS IAM<\/li><\/ul><p class=\"wp-block-paragraph\">For production: Use Session Manager, not key copying!<\/p><h3 class=\"wp-block-heading\">Complete Security Group Architecture<\/h3><p class=\"wp-block-paragraph\">Four security groups create defense-in-depth:<\/p><h3 class=\"wp-block-heading\">1. Bastion Security Group<\/h3><pre class=\"wp-block-code\"><code>module \"bastion_security_group\" {\n  source = \"terraform-aws-modules\/security-group\/aws\"\n  version = \"5.1.0\"\n\n  name = \"${local.name}-bastion-sg\"\n  vpc_id = module.vpc.vpc_id\n\n  ingress_rules = [\"ssh-tcp\"]\n  ingress_cidr_blocks = [\"0.0.0.0\/0\"]\n\n  egress_rules = [\"all-all\"]\n}<\/code><\/pre><h3 class=\"wp-block-heading\">2. Private Instance Security Group<\/h3><pre class=\"wp-block-code\"><code>module \"app_security_group\" {\n  source = \"terraform-aws-modules\/security-group\/aws\"\n  version = \"5.1.0\"\n\n  name = \"${local.name}-app-sg\"\n  vpc_id = module.vpc.vpc_id\n\n  ingress_with_source_security_group_id = [\n    {\n      from_port = 80\n      to_port = 80\n      protocol = \"tcp\"\n      source_security_group_id = module.alb_security_group.security_group_id\n      description = \"HTTP from ALB\"\n    },\n    {\n      from_port = 8080\n      to_port = 8080\n      protocol = \"tcp\"\n      source_security_group_id = module.alb_security_group.security_group_id\n      description = \"App3 from ALB\"\n    },\n    {\n      from_port = 22\n      to_port = 22\n      protocol = \"tcp\"\n      source_security_group_id = module.bastion_security_group.security_group_id\n      description = \"SSH from Bastion\"\n    }\n  ]\n\n  egress_rules = [\"all-all\"]\n}<\/code><\/pre><h3 class=\"wp-block-heading\">3. Load Balancer Security Group<\/h3><pre class=\"wp-block-code\"><code>module \"alb_security_group\" {\n  source = \"terraform-aws-modules\/security-group\/aws\"\n  version = \"5.1.0\"\n\n  name = \"${local.name}-alb-sg\"\n  vpc_id = module.vpc.vpc_id\n\n  ingress_rules = [\"http-80-tcp\", \"https-443-tcp\"]\n  ingress_cidr_blocks = [\"0.0.0.0\/0\"]\n\n  egress_rules = [\"all-all\"]\n}<\/code><\/pre><h3 class=\"wp-block-heading\">4. RDS Database Security Group<\/h3><pre class=\"wp-block-code\"><code>module \"db_security_group\" {\n  source = \"terraform-aws-modules\/security-group\/aws\"\n  version = \"5.1.0\"\n\n  name = \"${local.name}-db-sg\"\n  vpc_id = module.vpc.vpc_id\n\n  ingress_with_cidr_blocks = [\n    {\n      from_port = 3306\n      to_port = 3306\n      protocol = \"tcp\"\n      cidr_blocks = module.vpc.vpc_cidr_block\n      description = \"MySQL from VPC\"\n    }\n  ]\n\n  egress_rules = [\"all-all\"]\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Security group matrix:<\/p><p class=\"wp-block-paragraph\">Source Destination Ports Purpose Internet ALB 80, 443 Public web traffic ALB App Instances 80, 8080 Application traffic Internet Bastion 22 SSH access (restrict in prod!) Bastion App Instances 22 SSH for debugging App Instances RDS 3306 Database queries Internet RDS BLOCKED No direct access<\/p><p class=\"wp-block-paragraph\">Defense in depth demonstrated:<\/p><p class=\"wp-block-paragraph\">Attacker wants to access database:<\/p><p class=\"wp-block-paragraph\">Layer 1: Internet \u2192 Database<\/p><pre class=\"wp-block-code\"><code>Result: BLOCKED (database in isolated subnet, no internet route)<\/code><\/pre><p class=\"wp-block-paragraph\">Layer 2: Internet \u2192 Bastion \u2192 Database<\/p><pre class=\"wp-block-code\"><code>Requires: SSH key + bastion access\nResult: BLOCKED (unless attacker has SSH key)<\/code><\/pre><p class=\"wp-block-paragraph\">Layer 3: ALB \u2192 App \u2192 Database<\/p><pre class=\"wp-block-code\"><code>Requires: Compromise app instance first\nResult: BLOCKED (security group allows only VPC CIDR on 3306)<\/code><\/pre><p class=\"wp-block-paragraph\">Layer 4: Compromised App \u2192 Database<\/p><pre class=\"wp-block-code\"><code>Result: Possible, but requires app compromise + database credentials<\/code><\/pre><p class=\"wp-block-paragraph\">This is why:<\/p><ul class=\"wp-block-list\"><li>Bastion should have MFA<\/li><li>Log all commands<\/li><li>Minimal tools<\/li><\/ul><h3 class=\"wp-block-heading\">Deployment Process<\/h3><h3 class=\"wp-block-heading\">Step 1: Pre-deployment Checklist<\/h3><pre class=\"wp-block-code\"><code># Check AWS credentials\naws sts get-caller-identity\n\n# Check Terraform version\nterraform version\n\n# Check domain in Route53\naws route53 list-hosted-zones\n\n# Generate SSH key\nssh-keygen -t rsa -b 4096 -f private-key\/dns-db-key.pem<\/code><\/pre><h3 class=\"wp-block-heading\">Step 2: Initialize Terraform<\/h3><pre class=\"wp-block-code\"><code>terraform init<\/code><\/pre><p class=\"wp-block-paragraph\">Expected output:<\/p><pre class=\"wp-block-code\"><code>Initializing modules...\n- vpc in .terraform\/modules\/vpc\n- alb in .terraform\/modules\/alb\n- db in .terraform\/modules\/db\n\nTerraform has been successfully initialized!<\/code><\/pre><h3 class=\"wp-block-heading\">Step 3: Plan Deployment<\/h3><pre class=\"wp-block-code\"><code>terraform plan -var-file=\"secrets.tfvars\"<\/code><\/pre><p class=\"wp-block-paragraph\">Review resources to be created:<\/p><pre class=\"wp-block-code\"><code>Plan: 85 to add, 0 to change, 0 to destroy.<\/code><\/pre><h3 class=\"wp-block-heading\">Step 4: Apply Configuration<\/h3><pre class=\"wp-block-code\"><code>terraform apply -var-file=\"secrets.tfvars\"<\/code><\/pre><h3 class=\"wp-block-heading\">Step 5: Verify Deployment<\/h3><pre class=\"wp-block-code\"><code># Check ALB DNS\nterraform output alb_dns_name\n\n# Check target group health\naws elbv2 describe-target-health \\\n  --target-group-arn &lt;arn&gt;\n\n# Check RDS endpoint\nterraform output db_instance_endpoint<\/code><\/pre><h3 class=\"wp-block-heading\">Step 6: Access Applications<\/h3><pre class=\"wp-block-code\"><code># App1 (Pink)\nhttps:\/\/dns-to-db.rezaops.com\/app1\/\n\n# App2 (Teal)\nhttps:\/\/dns-to-db.rezaops.com\/app2\/\n\n# App3 (Java UMS)\nhttps:\/\/dns-to-db.rezaops.com\/\n\n# Bastion SSH\nssh -i private-key\/dns-db-key.pem ec2-user@&lt;bastion-eip&gt;\n\n# RDS from Bastion\nmysql -h &lt;rds-endpoint&gt; -u admin -p<\/code><\/pre><h3 class=\"wp-block-heading\">Real-World Testing Scenarios<\/h3><h3 class=\"wp-block-heading\">Scenario 1: Test Path-Based Routing<\/h3><pre class=\"wp-block-code\"><code># Test App1\ncurl https:\/\/dns-to-db.rezaops.com\/app1\/\n# Expected: Pink background page\n\n# Test App2\ncurl https:\/\/dns-to-db.rezaops.com\/app2\/\n# Expected: Teal background page\n\n# Test App3 (default)\ncurl https:\/\/dns-to-db.rezaops.com\/\n# Expected: Java login page<\/code><\/pre><h3 class=\"wp-block-heading\">Scenario 2: Test Load Balancing<\/h3><pre class=\"wp-block-code\"><code>for i in {1..10}; do\n  curl -s https:\/\/dns-to-db.rezaops.com\/app1\/ | grep \"Instance ID\"\ndone<\/code><\/pre><p class=\"wp-block-paragraph\">Expected output alternates between instances:<\/p><pre class=\"wp-block-code\"><code>Instance ID: i-0123456789abcdef0 (AZ1)\nInstance ID: i-0fedcba9876543210 (AZ2)\nInstance ID: i-0123456789abcdef0 (AZ1)\nInstance ID: i-0fedcba9876543210 (AZ2)<\/code><\/pre><h3 class=\"wp-block-heading\">Scenario 3: Test Multi-AZ Failover<\/h3><pre class=\"wp-block-code\"><code># Simulate primary failure\naws rds reboot-db-instance \\\n  --db-instance-identifier dns-db-database \\\n  --force-failover\n\n# Monitor failover\naws rds describe-db-instances \\\n  --db-instance-identifier dns-db-database \\\n  --query 'DBInstances[0].DBInstanceStatus'\n\n# Expected: Brief connection interruption (60-120 seconds)\n# Then: Automatic recovery, no manual intervention<\/code><\/pre><h3 class=\"wp-block-heading\">Scenario 4: Test Database Connection from App3<\/h3><pre class=\"wp-block-code\"><code># SSH to App3 via bastion\nssh -i private-key\/dns-db-key.pem ec2-user@&lt;bastion-eip&gt;\nssh -i dns-db-key.pem ec2-user@&lt;app3-private-ip&gt;\n\n# Test database connection\nmysql -h &lt;rds-endpoint&gt; -u admin -p mydb\n\nmysql&gt; USE mydb;\nmysql&gt; SHOW TABLES;\n+----------------+\n| Tables_in_mydb |\n+----------------+\n| users          |\n+----------------+<\/code><\/pre><h3 class=\"wp-block-heading\">Lessons Learned<\/h3><h3 class=\"wp-block-heading\">1. RDS Takes Forever to Create<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><pre class=\"wp-block-code\"><code>module.db.aws_db_instance.this: Still creating... [10m00s elapsed]<\/code><\/pre><p class=\"wp-block-paragraph\">Reality: Multi-AZ RDS creation takes 15-20 minutes:<\/p><ul class=\"wp-block-list\"><li>Primary instance creation: 10 minutes<\/li><li>Standby instance creation: 5 minutes<\/li><li>Replication setup: 3 minutes<\/li><li>Backup configuration: 2 minutes<\/li><li>Total: ~20 minutes<\/li><\/ul><p class=\"wp-block-paragraph\">Solution:<\/p><p class=\"wp-block-paragraph\">Be patient! This is normal.<\/p><p class=\"wp-block-paragraph\">For faster testing:<\/p><pre class=\"wp-block-code\"><code>multi_az = false  # Single AZ for testing (5 minutes)<\/code><\/pre><h3 class=\"wp-block-heading\">2. App3 Wouldn&#8217;t Start &#8211; Java Not Found<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><p class=\"wp-block-paragraph\">App3 instance running but application not responding:<\/p><pre class=\"wp-block-code\"><code>curl http:\/\/localhost:8080\/login\ncurl: (7) Failed to connect to localhost port 8080: Connection refused<\/code><\/pre><p class=\"wp-block-paragraph\">Cause:<\/p><pre class=\"wp-block-code\"><code># User data script failed:\nyum install -y java-17-amazon-corretto\n# Error: Package not found (wrong AMI)<\/code><\/pre><p class=\"wp-block-paragraph\">Solution:<\/p><p class=\"wp-block-paragraph\">Use Amazon Linux 2 AMI (not Amazon Linux 2023):<\/p><pre class=\"wp-block-code\"><code>data \"aws_ami\" \"amzn2\" {\n  most_recent = true\n  owners = [\"amazon\"]\n  filter {\n    name = \"name\"\n    values = [\"amzn2-ami-hvm-*-x86_64-gp2\"]\n  }\n}<\/code><\/pre><h3 class=\"wp-block-heading\">3. App3 Couldn&#8217;t Connect to Database<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><p class=\"wp-block-paragraph\">App3 starts but database connection fails:<\/p><p class=\"wp-block-paragraph\">Debug process:<\/p><p class=\"wp-block-paragraph\">From App3 instance:<\/p><pre class=\"wp-block-code\"><code>mysql -h &lt;rds-endpoint&gt; -u admin -p\nERROR 2003 (HY000): Can't connect to MySQL server<\/code><\/pre><p class=\"wp-block-paragraph\">Cause:<\/p><p class=\"wp-block-paragraph\">Security group allows only VPC CIDR, but App3 instance in private subnet with NAT Gateway &#8211; source IP is NAT Gateway IP, not instance IP!<\/p><p class=\"wp-block-paragraph\">Wrong:<\/p><pre class=\"wp-block-code\"><code># Security group allows instance security group only\nsource_security_group_id = module.app3_security_group.security_group_id<\/code><\/pre><p class=\"wp-block-paragraph\">Correct:<\/p><pre class=\"wp-block-code\"><code># Security group allows entire VPC CIDR\ncidr_blocks = module.vpc.vpc_cidr_block<\/code><\/pre><h3 class=\"wp-block-heading\">4. ACM Certificate Validation Stuck<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><pre class=\"wp-block-code\"><code>module.acm.aws_acm_certificate.this: Still creating... [20m00s elapsed]<\/code><\/pre><p class=\"wp-block-paragraph\">Cause:<\/p><p class=\"wp-block-paragraph\">DNS validation records not created &#8211; Route53 zone not configured correctly.<\/p><p class=\"wp-block-paragraph\">Debug:<\/p><pre class=\"wp-block-code\"><code>aws acm describe-certificate --certificate-arn &lt;arn&gt;\n# Check DomainValidationOptions<\/code><\/pre><p class=\"wp-block-paragraph\">Solution:<\/p><pre class=\"wp-block-code\"><code>module \"acm\" {\n  source = \"terraform-aws-modules\/acm\/aws\"\n  version = \"4.3.2\"\n\n  domain_name = \"dns-to-db.rezaops.com\"\n  zone_id = data.aws_route53_zone.main.zone_id\n\n  validation_method = \"DNS\"\n  wait_for_validation = true\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Module automatically creates validation CNAME records.<\/p><h3 class=\"wp-block-heading\">5. Target Group Health Checks Failing<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><p class=\"wp-block-paragraph\">ALB shows targets as unhealthy:<\/p><p class=\"wp-block-paragraph\">Debug:<\/p><pre class=\"wp-block-code\"><code>aws elbv2 describe-target-health --target-group-arn &lt;arn&gt;\n# Output: State: unhealthy, Reason: Timeout<\/code><\/pre><p class=\"wp-block-paragraph\">Cause:<\/p><p class=\"wp-block-paragraph\">Health check path wrong &#8211; App1 health check was \/index.html but actual file is \/app1\/index.html:<\/p><pre class=\"wp-block-code\"><code>health_check = {\n  path = \"\/app1\/index.html\"  # Correct\n  # path = \"\/index.html\"  # Wrong!\n}<\/code><\/pre><p class=\"wp-block-paragraph\">File exists at \/var\/www\/html\/app1\/index.html, not \/var\/www\/html\/index.html.<\/p><h3 class=\"wp-block-heading\">6. Bastion Can&#8217;t Connect to RDS &#8211; Wrong Subnet<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><p class=\"wp-block-paragraph\">From bastion:<\/p><pre class=\"wp-block-code\"><code>mysql -h &lt;rds-endpoint&gt; -u admin -p\nERROR 2003: Can't connect (timeout)<\/code><\/pre><p class=\"wp-block-paragraph\">Debug:<\/p><pre class=\"wp-block-code\"><code># Check bastion security group\naws ec2 describe-security-groups --group-ids &lt;sg-id&gt;\n\n# Check RDS security group\naws ec2 describe-security-groups --group-ids &lt;db-sg-id&gt;<\/code><\/pre><p class=\"wp-block-paragraph\">Cause:<\/p><p class=\"wp-block-paragraph\">RDS security group allows only private subnets, bastion is in public subnet!<\/p><pre class=\"wp-block-code\"><code># Wrong:\ningress_cidr_blocks = [\"10.0.1.0\/24\", \"10.0.2.0\/24\"]  # Private only<\/code><\/pre><p class=\"wp-block-paragraph\">Solution:<\/p><p class=\"wp-block-paragraph\">Allow entire VPC CIDR:<\/p><pre class=\"wp-block-code\"><code>ingress_cidr_blocks = [\"10.0.0.0\/16\"]  # Entire VPC<\/code><\/pre><h3 class=\"wp-block-heading\">7. ALB Returns 503 Service Temporarily Unavailable<\/h3><p class=\"wp-block-paragraph\">Problem:<\/p><pre class=\"wp-block-code\"><code>&lt;html&gt;\n&lt;body&gt;\n&lt;h1&gt;503 Service Temporarily Unavailable&lt;\/h1&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre><p class=\"wp-block-paragraph\">Cause:<\/p><p class=\"wp-block-paragraph\">No healthy targets in target group &#8211; instances still initializing or health checks failing.<\/p><p class=\"wp-block-paragraph\">Debug process:<\/p><p class=\"wp-block-paragraph\">Check target group health:<\/p><pre class=\"wp-block-code\"><code>aws elbv2 describe-target-health --target-group-arn &lt;arn&gt;<\/code><\/pre><p class=\"wp-block-paragraph\">Check security groups:<\/p><pre class=\"wp-block-code\"><code># ALB SG must allow egress to instances\n# Instance SG must allow ingress from ALB SG<\/code><\/pre><p class=\"wp-block-paragraph\">SSH to instance:<\/p><pre class=\"wp-block-code\"><code>ssh -i key.pem ec2-user@&lt;bastion-eip&gt;\nssh -i key.pem ec2-user@&lt;private-ip&gt;\ncurl http:\/\/localhost:80\/app1\/<\/code><\/pre><p class=\"wp-block-paragraph\">Check ALB to instance:<\/p><pre class=\"wp-block-code\"><code>curl http:\/\/&lt;private-ip&gt;:80\/app1\/<\/code><\/pre><p class=\"wp-block-paragraph\">Solution:<\/p><ul class=\"wp-block-list\"><li>30 seconds health check interval<\/li><li>3 consecutive successes needed<\/li><li>Total: 90 seconds minimum<\/li><\/ul><p class=\"wp-block-paragraph\">After 2 minutes: All targets healthy, ALB works!<\/p><h3 class=\"wp-block-heading\">What I Learned<\/h3><ol class=\"wp-block-list\"><li>Path-based routing enables multiple applications on one ALB, saving costs<\/li><li>Multi-AZ RDS provides automatic failover but takes time to create<\/li><li>Security groups + subnet isolation create defense-in-depth<\/li><li>Template files solve the chicken-and-egg problem of database endpoints<\/li><li>Bastion hosts are essential for private subnet debugging<\/li><li>Health check paths must match actual file locations exactly<\/li><li>Security group rules must consider NAT Gateway IP translation<\/li><li>ACM DNS validation requires correct Route53 zone configuration<\/li><li>Terraform depends_on ensures correct resource creation order<\/li><li>Full-stack infrastructure is complex but manageable with modules<\/li><\/ol>","protected":false},"excerpt":{"rendered":"<p>After deploying load balancers, auto scaling, and monitoring separately, I wanted to build something real a complete production-ready three-tier application with: Custom&hellip;<\/p>\n","protected":false},"author":2,"featured_media":145,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"5","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/aws-full-stack-infrastructure-dns-database-terraform-reza-chegini-q8tuc\/","footnotes":""},"categories":[85,10],"tags":[60,8,92,72,87],"series":[86],"class_list":["post-143","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aws","category-devops","tag-aws","tag-devops","tag-rds","tag-route-53","tag-terraform","series-aws-with-terraform"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/aws-full-stack-infrastructure-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/143","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=143"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/143\/revisions"}],"predecessor-version":[{"id":144,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/143\/revisions\/144"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/145"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=143"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=143"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=143"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=143"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}