Ravindra BagaleCourses & study guides मराठी

Chapter 45: Terraform 3-Tier Architecture in a Single Availability Zone

45.4 Database tier: DB subnet group and single-AZ RDS

Story. Suppose we are adding the HR portal's employee database. It must sit in the DB subnet, accept only the app servers, and never show a password in code. For a pilot a single-AZ database is acceptable: if AZ-a has a problem, we restore from backup. AWS still asks for a DB subnet group across two zones, so we give it AZ-b as well and pin the database to AZ-a.

DB subnet group db_a + db_b db SG :3306 from app RDS MySQL AZ-a secret in Secrets Manager
The subnet group covers two zones, but the single-AZ database runs only in AZ-a.

Steps

  1. Add db.tf and outputs.tf.
  2. Run terraform validate.
  3. Run terraform plan and confirm the total.
# ---- db.tf ----
resource "aws_db_subnet_group" "db" {
  name       = "${var.project}-db"
  subnet_ids = [aws_subnet.db_a.id, aws_subnet.db_b.id]
}

resource "aws_db_instance" "main" {
  identifier                  = "${var.project}-mysql"
  engine                      = "mysql"
  engine_version              = "8.0"
  instance_class              = "db.t3.micro"
  allocated_storage           = 20
  storage_type                = "gp3"
  storage_encrypted           = true
  db_name                     = "appdb"
  username                    = var.db_username
  manage_master_user_password = true
  db_subnet_group_name        = aws_db_subnet_group.db.name
  vpc_security_group_ids      = [aws_security_group.db.id]
  availability_zone           = local.az_a
  multi_az                    = false
  publicly_accessible         = false
  backup_retention_period     = 7
  skip_final_snapshot         = true
}

# ---- outputs.tf ----
output "web_url" {
  value = "http://${aws_instance.web.public_ip}"
}

output "app_private_ip" {
  value = aws_instance.app.private_ip
}

output "db_endpoint" {
  value = aws_db_instance.main.endpoint
}

What each line does

  • subnet_ids = [db_a, db_b] — Two zones, which the DB subnet group requires.
  • availability_zone = local.az_a — Keeps the database in the same zone as web and app.
  • multi_az = false — No standby. Chapter 46 turns this on.
  • manage_master_user_password = true — Password lives in Secrets Manager, not in code.
  • publicly_accessible = false — No public endpoint.
  • vpc_security_group_ids = [db] — Only the app tier can connect.
  • output "web_url" — The address to test after apply.

When it is green.

aws_db_instance.main: Creation complete after 6m40s
Apply complete! Resources: 24 added, 0 changed, 0 destroyed.

When it is red.

Error: creating RDS DB Instance: InvalidVPCNetworkStateFault:
Cannot create a db.t3.micro instance because ... subnet group does not cover enough AZs

Fix. Make sure db_b uses local.az_b, a different zone from db_a.

What: every term in plain English

  • RDS — Amazon Relational Database Service. AWS runs MySQL or PostgreSQL for you, including backups and patching.
  • DB subnet group — A list of subnets RDS may use. AWS requires subnets in at least two zones, even for a single-AZ database.
  • single-AZ RDS — One database instance in one zone, with no standby. multi_az = false.
  • manage_master_user_password — Lets RDS create the admin password and store it in AWS Secrets Manager, so it never appears in your code or state.
  • endpoint — The DNS name and port the app uses to connect, for example hrportal-mysql.abc123.ap-south-1.rds.amazonaws.com:3306.
  • backup_retention_period — How many days of automatic backups RDS keeps. 0 turns backups off.
  • skip_final_snapshot — If true, destroy deletes the database without a last backup. Fine for labs, never for production.
  • storage_encrypted — Encrypts the disk with KMS. Required by almost every company security policy.

Example 1: Why two DB subnets for a one-zone database

Learners at a Cognizant lab often delete db_b to "keep it single-AZ" and the apply fails. AWS needs two zones in the group so it can move the database later.

  1. Create db_a in local.az_a.
  2. Create db_b in local.az_b.
  3. Put both in the subnet group.
resource "aws_db_subnet_group" "db" {
  name       = "${var.project}-db"
  subnet_ids = [aws_subnet.db_a.id, aws_subnet.db_b.id]
  tags = {
    Name = "${var.project}-db"
  }
}

Output

aws_db_subnet_group.db: Creation complete after 1s [id=hrportal-db]

What it shows: The database still runs only in az_a because availability_zone = local.az_a. db_b is just allowed, not used.

Example 2: Let RDS own the password

Real passwords in tfvars end up in Git and in the state file. Banks such as Axis Bank ban that in audits. manage_master_user_password puts the password in Secrets Manager instead.

  1. Set manage_master_user_password = true.
  2. Do not set password.
  3. Read the secret ARN from the resource.
resource "aws_db_instance" "main" {
  identifier                  = "${var.project}-mysql"
  engine                      = "mysql"
  engine_version              = "8.0"
  instance_class              = "db.t3.micro"
  allocated_storage           = 20
  username                    = var.db_username
  manage_master_user_password = true
  db_subnet_group_name        = aws_db_subnet_group.db.name
  vpc_security_group_ids      = [aws_security_group.db.id]
  publicly_accessible         = false
  skip_final_snapshot         = true
}

output "db_secret_arn" {
  value = aws_db_instance.main.master_user_secret[0].secret_arn
}

Output

db_secret_arn = "arn:aws:secretsmanager:ap-south-1:123456789012:secret:rds!db-1a2b3c4d-AbCdEf"

What it shows: Only the ARN is an output. The password itself is never in Terraform code, plan or state.

Example 3: Fetch the password at run time from the app server

The app reads the secret when it starts, using its instance role. Nobody copies passwords into chat.

  1. Give the app role secretsmanager:GetSecretValue on that ARN.
  2. Read the secret with the CLI.
  3. Parse the password field.
aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" \
  --query SecretString --output text | python3 -c "import sys,json; print(json.load(sys.stdin)['username'])"

Output

admin

What it shows: We print only the username in the lab. In the app, read the password the same way and never log it.

Example 4: Confirm the database is single-AZ and private

Before go-live at a Mahindra pilot, the reviewer asks three things: which zone, is it public, is it encrypted. One CLI call answers all.

  1. Describe the DB instance.
  2. Check MultiAZ, PubliclyAccessible and StorageEncrypted.
  3. Check the zone.
aws rds describe-db-instances --db-instance-identifier hrportal-mysql \
  --query "DBInstances[0].[AvailabilityZone,MultiAZ,PubliclyAccessible,StorageEncrypted,BackupRetentionPeriod]" --output table

Output

--------------------------
|   DescribeDBInstances  |
+------------------------+
|  ap-south-1a           |
|  False                 |
|  False                 |
|  True                  |
|  7                     |
+------------------------+

What it shows: Zone a, no standby, not public, encrypted, 7 days of backups. Exactly what the code asked for.

Example 5: Connect from the app server only

The DB group allows 3306 only from the app group. Testing from the app server proves the chain; testing from your laptop should fail.

  1. Install the MySQL client on the app server.
  2. Connect to the endpoint.
  3. Try the same from your laptop and see it time out.
sudo dnf install -y mariadb105
mysql -h "$(echo $DB_ENDPOINT | cut -d: -f1)" -u admin -p -e "SELECT VERSION();"

Output

+-----------+
| VERSION() |
+-----------+
| 8.0.39    |
+-----------+

What it shows: The -p flag asks for the password interactively, so it never lands in shell history.

Command cheat sheet

Command What it does When to use it
aws rds describe-​db-​instances Shows DB settings Audit
manage_​master_​user_​password = true Password in Secrets Manager Always
multi_az = false Single-AZ Labs and pilots
publicly_​accessible = false No public endpoint Always
aws secretsmanager get-​secret-​value Reads the secret App start-up
skip_​final_​snapshot = true No last backup Labs only

Verify: did it work?

  1. DB is private

    aws rds describe-db-instances --db-instance-identifier hrportal-mysql --query "DBInstances[0].PubliclyAccessible"
    

    Output

    false
    
  2. DB is encrypted

    aws rds describe-db-instances --db-instance-identifier hrportal-mysql --query "DBInstances[0].StorageEncrypted"
    

    Output

    true
    
  3. No password in code

    grep -c "password *=" *.tf
    

    Output

    0
    

When it is red 1

Error: creating RDS DB Subnet Group: DBSubnetGroupDoesNotCoverEnoughAZs: The DB subnet group doesn't meet Availability Zone (AZ) coverage requirement.

Cause. Both DB subnets are in the same zone.

Fix, step by step

  1. Put db_b in local.az_b.
  2. Apply again.

When it is red 2

Error: creating RDS DB Instance: InvalidParameterCombination: RDS does not support creating a DB instance with the following combination: DBInstanceClass=db.t2.micro, Engine=mysql, EngineVersion=8.0

Cause. An old instance class does not support this engine version.

Fix, step by step

  1. Use db.t3.micro or db.t4g.micro.
  2. Check supported combinations with aws rds describe-orderable-db-instance-options.

When it is red 3

Error: deleting RDS DB Instance: InvalidParameterCombination: FinalSnapshotIdentifier is required when a final snapshot is required

Cause. skip_final_snapshot is false and no snapshot name was given.

Fix, step by step

  1. For labs set skip_final_snapshot = true and apply.
  2. For production set final_snapshot_identifier.

Cleanup

terraform destroy -auto-approve

Ravindra Bagale's Tip

RDS takes 5 to 10 minutes to create and 5 minutes to delete. Start the apply, then read the plan output for other tiers while you wait.

Practice tasks

Easy Read the settings

Why: Audit a DB.

What to do: Run describe-db-instances and list zone, MultiAZ and encryption.

How:
  1. Use --query.

Check: ap-south-1a, False, True.

Easy Find the secret

Why: No passwords in code.

What to do: Output the secret ARN and open it in Secrets Manager.

How:
  1. master_user_secret.

Check: An rds! secret.

Medium Connect from app

Why: Prove the chain.

What to do: Run SELECT VERSION() from the app server.

How:
  1. mariadb105 client.

Check: 8.0.x.

Medium Break the group

Why: Learn the error.

What to do: Put db_b in az_a and plan/apply.

How:
  1. Coverage error.

Check: DBSubnetGroupDoesNotCoverEnoughAZs.

Easy Say it in two lines

Why: If you can say it simply, you understand it.

What to do: Write two short lines about this lesson for a fresher.

How:
  1. Write the problem it solves.
  2. Write the one command or file you use.
  3. Read it aloud.

Check: A fresher understands it without a question.

Easy Start a safe lab folder

Why: Practice needs a throwaway place.

What to do: Make a new folder with versions.tf and a random_pet resource, so no AWS cost.

How:
  1. mkdir tf-lab and cd into it.
  2. Add the hashicorp/random provider and one random_pet.
  3. Run terraform init and terraform apply.

Check: Apply complete! with 1 added.

Medium Break it on purpose

Why: You must know the red output by face.

What to do: Cause one error from the red part of this lesson and fix it.

How:
  1. Follow one red section.
  2. Copy the exact message to your notes.
  3. Write the fix in one line.

Check: You fixed it without searching the web.

Medium Run it twice

Why: A second run must be quiet.

What to do: Run the same plan two times with no edits.

How:
  1. Run terraform plan.
  2. Run it again.
  3. Explain any change it still shows.

Check: The second plan says No changes, or you can explain each line.

Medium Write a runbook page

Why: At 2 a.m. nobody remembers flags.

What to do: Write a one-page runbook for this lesson.

How:
  1. Title, when to use, exact commands, how to check, how to undo.

Check: A teammate can follow it alone.

Medium Run it from a pipeline

Why: Real teams run Terraform from CI, not from laptops.

What to do: Run the main command of this lesson inside a GitHub Actions or Jenkins job.

How:
  1. Add a job with hashicorp/setup-terraform or a Terraform Docker image.
  2. Give it AWS access through an IAM role, never a key in the file.
  3. Run init, then the command from this lesson.

Check: The job log shows the same output you saw on your laptop.

Medium Check the cost

Why: Labs left running cost real money.

What to do: Find what this lesson would cost per day if you forgot to destroy it.

How:
  1. Open the AWS Pricing Calculator.
  2. Add each resource from your plan.
  3. Write the daily cost in your notes.

Check: You know the daily cost and you ran terraform destroy.

Hard Draw the flow top to bottom

Why: A drawing shows the order of work.

What to do: Draw this lesson as boxes from top to bottom.

How:
  1. Start at your code, go through plan and state, end at AWS.
  2. Mark where each command reads or writes.

Check: Each box matches a real step you ran.

Hard Explain it like an interview

Why: Interviewers want real stories.

What to do: Record a 60 second answer about this lesson.

How:
  1. Say the problem, the command, and one error you hit.

Check: Your answer has a real command and a real error message.

Homework: try at home

Try at home

Change the engine to PostgreSQL 16 in a copy of db.tf: list every line that must change, including the port in the security group.

Recap

  • A DB subnet group needs two zones.
  • multi_az = false gives single-AZ.
  • Let RDS manage the password in Secrets Manager.
  • Keep the database private and encrypted.
  • skip_final_snapshot is for labs only.

FAQ

Why is the second subnet needed if it is unused?

AWS requires it so the database can move or become multi-AZ later.

Can I see the password in state?

Not with manage_master_user_password. That is why we use it.

How long are backups kept?

backup_retention_period days, 7 in this lab.

Can I connect from my laptop?

No, by design. Use the app server or a bastion via Session Manager port forwarding.

Interview questions

  1. Why does a single-AZ RDS still need two subnets?

    The DB subnet group must cover at least two zones.

  2. How do you keep the RDS password out of Terraform state?

    manage_master_user_password = true so RDS stores it in Secrets Manager.

  3. Single-AZ versus Multi-AZ RDS?

    Single-AZ has one instance; Multi-AZ adds a synchronous standby in another zone with automatic failover.

  4. What does skip_final_snapshot do?

    Deletes the DB on destroy without taking a last snapshot.

Quick quiz

Q1. Minimum zones in a DB subnet group?

2

Q2. Setting for single-AZ?

multi_az = false

Q3. Where does RDS store the managed password?

Secrets Manager

Q4. MySQL port?

3306

Q5. Backup days in this lab?

7

Samajla ka?

Ravi Sir's Cards for this lesson

मराठी cards