Files
kodekloud-engineer/aws-36-x.md

34 KiB

Task 36

REGION=us-east-1

# 0. Default VPC, its default SG (goes on the ALB), two public subnets in 2 AZs
VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
  --region $REGION --query 'Vpcs[0].VpcId' --output text)

DEFAULT_SG=$(aws ec2 describe-security-groups \
  --filters "Name=group-name,Values=default" "Name=vpc-id,Values=$VPC_ID" \
  --region $REGION --query 'SecurityGroups[0].GroupId' --output text)

read -r SUBNET1 SUBNET2 <<< $(aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=$VPC_ID" "Name=map-public-ip-on-launch,Values=true" \
  --region $REGION --query 'Subnets[].SubnetId | [0:2]' --output text)

# 1. nautilus-sg (for the instance): allow 80 FROM the default SG (the ALB's SG)
NAUT_SG=$(aws ec2 create-security-group \
  --group-name nautilus-sg \
  --description "HTTP 80 from ALB default SG" \
  --vpc-id $VPC_ID --region $REGION \
  --query 'GroupId' --output text)

aws ec2 authorize-security-group-ingress \
  --group-id $NAUT_SG --protocol tcp --port 80 \
  --source-group $DEFAULT_SG --region $REGION

# 2. Make sure the default SG (on the ALB) allows 80 from the internet
aws ec2 authorize-security-group-ingress \
  --group-id $DEFAULT_SG --protocol tcp --port 80 --cidr 0.0.0.0/0 \
  --region $REGION 2>/dev/null || true

# 3. User-data: install + start Nginx (Ubuntu)
cat > /tmp/nginx-ud.sh << 'EOF'
#!/bin/bash
apt-get update -y
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
EOF

# 4. Launch nautilus-ec2 (Ubuntu 24.04 noble, amd64 for t2.micro) with nautilus-sg
IID=$(aws ec2 run-instances \
  --image-id resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id \
  --instance-type t2.micro \
  --security-group-ids $NAUT_SG \
  --subnet-id $SUBNET1 \
  --user-data file:///tmp/nginx-ud.sh \
  --region $REGION \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=nautilus-ec2}]' \
  --query 'Instances[0].InstanceId' --output text)

# aws ec2 wait instance-running --instance-ids $IID --region $REGION
STATE=""
until [ "$STATE" = "running" ]; do
  STATE=$(aws ec2 describe-instances --instance-ids $IID --region $REGION \
    --query 'Reservations[0].Instances[0].State.Name' --output text)
  echo "$IID: $STATE"
  [ "$STATE" = "running" ] || sleep 5
done

# 5. ALB across both subnets, wearing the default SG
ALB_ARN=$(aws elbv2 create-load-balancer \
  --name nautilus-alb \
  --subnets $SUBNET1 $SUBNET2 \
  --security-groups $DEFAULT_SG \
  --scheme internet-facing --type application \
  --region $REGION \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text)

# 6. Target group (HTTP:80, instance targets, in the VPC)
TG_ARN=$(aws elbv2 create-target-group \
  --name nautilus-tg \
  --protocol HTTP --port 80 \
  --vpc-id $VPC_ID --target-type instance \
  --region $REGION \
  --query 'TargetGroups[0].TargetGroupArn' --output text)

# 7. Register the instance
aws elbv2 register-targets \
  --target-group-arn $TG_ARN --targets Id=$IID --region $REGION

# 8. Listener 80 -> forward to TG
aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN \
  --protocol HTTP --port 80 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN \
  --region $REGION

# 9. Wait for ALB to provision
# aws elbv2 wait load-balancer-available \
#   --load-balancer-arns $ALB_ARN --region $REGION
STATE=""
until [ "$STATE" = "active" ]; do
  STATE=$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN --region $REGION \
    --query 'LoadBalancers[0].State.Code' --output text)
  echo "$ALB_ARN: $STATE"
  [ "$STATE" = "active" ] || sleep 5
done

ALB_DNS=$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN \
  --region $REGION --query 'LoadBalancers[0].DNSName' --output text)
echo "ALB DNS: http://$ALB_DNS"


# Verify
# Poll target health until healthy
for n in $(seq 1 10); do
  H=$(aws elbv2 describe-target-health --target-group-arn $TG_ARN --region $REGION \
    --query 'TargetHealthDescriptions[0].TargetHealth.State' --output text)
  echo "target: $H"
  [ "$H" = "healthy" ] && break
  sleep 20
done

# Hit the ALB DNS — should return Nginx's page
curl -I "http://$ALB_DNS"

Task 37

Task:

  1. EC2 Instance Setup:

An instance named devops-ec2 already exists. The instance requires access to an S3 bucket. 2) Setup SSH Keys:

Create new SSH key pair (id_rsa and id_rsa.pub) on the aws-client host and add the public key to the root user's authorized keys on the EC2 instance. 3) Create a Private S3 Bucket:

Name the bucket devops-s3-046473746767. Ensure the bucket is private. 4) Create an IAM Policy and Role:

Create an IAM policy allowing s3:PutObject, s3:ListBucket and s3:GetObject access to devops-s3-046473746767. Create an IAM role named devops-role. Attach the policy to the IAM role. Attach this role to the devops-ec2 instance. 5) Test the Access:

SSH into the EC2 instance and try to upload a file to devops-s3-046473746767 bucket using following command: aws s3 cp s3://devops-s3-046473746767/

Now run following command to list the upload file: aws s3 ls s3://devops-s3-046473746767/

Solution:

EC2 IAM Role → S3 Access Task

The IAM-role-to-EC2 task — the concept here is instance profiles. A role can't attach directly to an EC2 instance; it goes through an instance profile, a wrapper the CLI makes you create explicitly (the console hides this). And the S3 policy has a subtlety most people get wrong: ListBucket acts on the bucket ARN, GetObject/PutObject act on the object ARN (/*) — mixing those up makes the policy silently not work.

Phased, run on aws-client.

Phase 1 — Private S3 bucket

REGION=us-east-1
BUCKET=devops-s3-046473746767

# us-east-1: NO LocationConstraint (special case)
aws s3api create-bucket --bucket $BUCKET --region $REGION

Private by default — since 2023 all new buckets ship with Block Public Access fully on and ACLs disabled. Nothing extra needed. (Any other region would require --create-bucket-configuration LocationConstraint=<region>; us-east-1 errors if you add it.)

Phase 2 — IAM policy + role + instance profile

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Policy — note the TWO resource ARNs: bucket for ListBucket, /* for object ops
cat > /tmp/devops-s3-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::$BUCKET"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::$BUCKET/*"
    }
  ]
}
EOF

POLICY_ARN=$(aws iam create-policy \
  --policy-name devops-s3-policy \
  --policy-document file:///tmp/devops-s3-policy.json \
  --query 'Policy.Arn' --output text)

# Role with EC2 trust
cat > /tmp/ec2-trust.json << 'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}
EOF

aws iam create-role --role-name devops-role \
  --assume-role-policy-document file:///tmp/ec2-trust.json

aws iam attach-role-policy --role-name devops-role --policy-arn $POLICY_ARN

# Instance profile — the wrapper that lets a role attach to EC2
aws iam create-instance-profile --instance-profile-name devops-role
aws iam add-role-to-instance-profile \
  --instance-profile-name devops-role --role-name devops-role

The ListBucket vs GetObject/PutObject ARN split is the crux: ListBucket is a bucket-level action (it lists the bucket's contents), so its resource is the bucket ARN with no /*. GetObject/PutObject act on objects inside the bucket, so their resource needs the /* suffix. Put ListBucket on the /* ARN and your aws s3 ls fails with AccessDenied even though everything looks right. Two separate statements, two different ARNs.

Phase 3 — Attach the instance profile to devops-ec2

IID=$(aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=devops-ec2" "Name=instance-state-name,Values=running,stopped" \
  --region $REGION --query 'Reservations[0].Instances[0].InstanceId' --output text)

# Give IAM a few seconds to propagate the profile, then associate
sleep 8
aws ec2 associate-iam-instance-profile \
  --instance-id $IID \
  --iam-instance-profile Name=devops-role \
  --region $REGION

associate-iam-instance-profile attaches by the instance-profile name (devops-role here — profile named the same as the role, common convention). No reboot needed; the credentials appear via the instance metadata service (IMDS) within a minute or so. If the instance already has a profile attached, use replace-iam-instance-profile-association instead.

Phase 4 — SSH key + passwordless root

[ -f /root/.ssh/id_rsa ] || ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N "" -q
PUBKEY=$(cat /root/.ssh/id_rsa.pub)

EC2_AZ=$(aws ec2 describe-instances --instance-ids $IID --region $REGION \
  --query 'Reservations[0].Instances[0].Placement.AvailabilityZone' --output text)
EC2_IP=$(aws ec2 describe-instances --instance-ids $IID --region $REGION \
  --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)

# Bootstrap via EC2 Instance Connect (60s TTL), then persist into root's keys
# Ubuntu AMI -> default user is "ubuntu", not "ec2-user"
aws ec2-instance-connect send-ssh-public-key \
  --instance-id $IID --instance-os-user ubuntu \
  --ssh-public-key file:///root/.ssh/id_rsa.pub \
  --availability-zone $EC2_AZ --region $REGION

ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa ubuntu@$EC2_IP "
  set -e
  sudo mkdir -p /root/.ssh && sudo chmod 700 /root/.ssh
  echo '$PUBKEY' | sudo tee -a /root/.ssh/authorized_keys >/dev/null
  sudo chmod 600 /root/.ssh/authorized_keys
  echo 'PermitRootLogin prohibit-password' | sudo tee /etc/ssh/sshd_config.d/99-root.conf >/dev/null
  sudo systemctl restart sshd
"

(If port 22 isn't open, this SSH leg won't work — use SSM send-command to plant the key instead, or open 22 to aws-client's /32.)

Phase 5 — Test the S3 access from the instance

ssh -i /root/.ssh/id_rsa root@$EC2_IP "
  echo 'hello from devops-ec2' > /tmp/testfile.txt
  aws s3 cp /tmp/testfile.txt s3://$BUCKET/
  aws s3 ls s3://$BUCKET/
"

Both commands should succeed — the cp uploads (exercises PutObject), the ls lists (exercises ListBucket). No aws configure needed on the instance: the CLI picks up temporary credentials automatically from IMDS via the attached role. That's the whole point of the role — no static keys on the box.

Verify

# Role attached to the instance?
aws ec2 describe-instances --instance-ids $IID --region $REGION \
  --query 'Reservations[0].Instances[0].IamInstanceProfile.Arn'

# File actually landed?
aws s3 ls s3://$BUCKET/ --region $REGION

Want the instance showing an IamInstanceProfile.Arn ending in devops-role, and testfile.txt in the bucket listing. The successful cp+ls from inside the instance is the task's proof — it confirms the role → profile → instance chain delivered S3 permissions via IMDS.

Debug order if the S3 commands fail from the instance

  1. Unable to locate credentials → profile not attached yet (IMDS propagation) or association failed. Re-check Phase 3's verify; wait a minute.
  2. AccessDenied on ls but cp works (or vice versa) → the ListBucket/object ARN split is wrong. ls needs ListBucket on the bare bucket ARN; cp needs PutObject on /*.
  3. AccessDenied on both → policy didn't attach to the role, or wrong bucket name in the ARNs.

Wired as above it works clean.

Task 38

The Nautilus DevOps team is tasked with deploying a containerized application using Amazon's container services. They need to create a private Amazon Elastic Container Registry (ECR) to store their Docker images and use Amazon Elastic Container Service (ECS) to deploy the application. The process involves building a Docker image from a given Dockerfile, pushing it to the ECR, and then setting up an ECS cluster to run the application.

Create a Private ECR Repository:

Create a private ECR repository named datacenter-ecr to store Docker images. Build and Push Docker Image:

Use the Dockerfile located at /root/pyapp on the aws-client host. Build a Docker image using this Dockerfile. Tag the image with latest tag. Push the Docker image to the datacenter-ecr repository. Create and Configure ECS cluster:

Create an ECS cluster named datacenter-cluster using the Fargate launch type. Create an ECS Task Definition:

Define a task named datacenter-taskdefinition using the Docker image from the datacenter-ecr ECR repository. Specify necessary CPU and memory resources. Deploy the Application Using ECS Service:

Create a service named datacenter-service on the datacenter-cluster to run the task. Ensure the service runs at least one task.

Solution

ECS on Fargate Deployment Task (datacenter)

The full ECS-on-Fargate pipeline — the most involved task in the set because it chains ECR → cluster → task def → service, and Fargate has hard requirements the others don't: an execution role, awsvpc networking with explicit subnets/SG, and CPU/memory from a fixed valid-combinations table (you can't pick arbitrary values).

Phase 1 — ECR repo + build + push

On aws-client:

REGION=us-east-1

aws ecr create-repository --repository-name datacenter-ecr --region $REGION

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
REPO_URI="${REGISTRY}/datacenter-ecr"

# Auth docker to ECR (12h token)
aws ecr get-login-password --region $REGION \
  | docker login --username AWS --password-stdin "$REGISTRY"

# Build, tag latest, push
docker build -t datacenter-ecr:latest /root/pyapp
docker tag datacenter-ecr:latest "${REPO_URI}:latest"
docker push "${REPO_URI}:latest"

Phase 2 — ECS cluster (Fargate)

aws ecs create-cluster \
  --cluster-name datacenter-cluster \
  --capacity-providers FARGATE \
  --region $REGION

--capacity-providers FARGATE registers the cluster for the Fargate (serverless) launch type — no EC2 container instances to manage. Satisfies "Fargate launch type" at the cluster level; also specified per-task later.

Phase 3 — Execution role (the Fargate prerequisite people miss)

Fargate needs a task execution role — the role ECS itself assumes to pull the image from ECR and write logs to CloudWatch. Without it, the task fails at launch with a pull error. AWS has a canonical managed policy for exactly this:

cat > /tmp/ecs-trust.json << 'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}
EOF

aws iam create-role --role-name ecsTaskExecutionRole \
  --assume-role-policy-document file:///tmp/ecs-trust.json 2>/dev/null || true

aws iam attach-role-policy --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

EXEC_ROLE_ARN=$(aws iam get-role --role-name ecsTaskExecutionRole \
  --query 'Role.Arn' --output text)

The trust principal is ecs-tasks.amazonaws.com (not ec2 or ecs). AmazonECSTaskExecutionRolePolicy grants exactly ECR-pull + CloudWatch-logs-write — the two things ECS needs to stand up your container. This role often already exists in an account (|| true handles that).

Phase 4 — Task definition

cat > /tmp/datacenter-taskdef.json << EOF
{
  "family": "datacenter-taskdefinition",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "$EXEC_ROLE_ARN",
  "containerDefinitions": [
    {
      "name": "datacenter-container",
      "image": "${REPO_URI}:latest",
      "essential": true,
      "portMappings": [
        { "containerPort": 80, "protocol": "tcp" }
      ]
    }
  ]
}
EOF

TASKDEF_ARN=$(aws ecs register-task-definition \
  --cli-input-json file:///tmp/datacenter-taskdef.json \
  --region $REGION \
  --query 'taskDefinition.taskDefinitionArn' --output text)

The Fargate-specific requirements baked in here:

  • networkMode: awsvpc is mandatory for Fargate — every task gets its own ENI. bridge/host modes are EC2-launch-type only. Skip this and registration fails.
  • cpu/memory must be a valid pair from Fargate's table — not arbitrary. 256 CPU (.25 vCPU) pairs only with 512/1024/2048 MiB memory. Other valid CPU values: 512, 1024, 2048, 4096. 256/512 is the smallest, cheapest, and fine for "specify necessary CPU and memory." Pick a mismatched pair (e.g. cpu 256 / memory 4096) and it rejects.
  • executionRoleArn — the role from Phase 3. Required for Fargate to pull from ECR.
  • Values are strings ("256", not 256) in the JSON — ECS is picky about that.
  • image points at the ECR URI with :latest — the image you just pushed.

Phase 5 — ECS service (needs subnets + SG for awsvpc)

Because it's awsvpc, the service launch requires an explicit network config — subnets and a security group:

VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
  --region $REGION --query 'Vpcs[0].VpcId' --output text)

read -r SUBNET1 SUBNET2 <<< $(aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=$VPC_ID" "Name=map-public-ip-on-launch,Values=true" \
  --region $REGION --query 'Subnets[].SubnetId | [0:2]' --output text)

# SG for the task (allow inbound 80 if the app serves HTTP)
TASK_SG=$(aws ec2 create-security-group \
  --group-name datacenter-task-sg \
  --description "ECS task SG" --vpc-id $VPC_ID --region $REGION \
  --query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress \
  --group-id $TASK_SG --protocol tcp --port 80 --cidr 0.0.0.0/0 --region $REGION

# Create the service — desired count 1
aws ecs create-service \
  --cluster datacenter-cluster \
  --service-name datacenter-service \
  --task-definition datacenter-taskdefinition \
  --desired-count 1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[$SUBNET1,$SUBNET2],securityGroups=[$TASK_SG],assignPublicIp=ENABLED}" \
  --region $REGION

The service-level essentials:

  • --desired-count 1 = "runs at least one task" (the requirement). ECS keeps one task running, restarting it if it dies.
  • --network-configuration ... awsvpcConfiguration is required because of awsvpc mode — the task's ENI needs to land in specific subnets with a specific SG. Omit it and service creation errors.
  • assignPublicIp=ENABLED is critical on Fargate in public subnets: without a public IP, the task can't reach ECR/internet to pull the image (unless you have a NAT gateway), and it'll fail to launch. In public subnets this is the simplest working config. (Private-subnet Fargate would use a NAT gateway instead and set this DISABLED.)
  • --launch-type FARGATE — reaffirms Fargate at the service level.

Verify

# Image pushed
aws ecr describe-images --repository-name datacenter-ecr --region $REGION \
  --query 'imageDetails[].imageTags'

# Cluster active
aws ecs describe-clusters --clusters datacenter-cluster --region $REGION \
  --query 'clusters[0].{Name:clusterName,Status:status}'

# Service + running task count (poll until runningCount=1)
for n in $(seq 1 12); do
  RC=$(aws ecs describe-services --cluster datacenter-cluster \
    --services datacenter-service --region $REGION \
    --query 'services[0].runningCount' --output text)
  echo "runningCount: $RC"
  [ "$RC" = "1" ] && break
  sleep 15
done

Want: ECR showing ["latest"], cluster ACTIVE, and the service's runningCount reaching 1. That last one is the task's success signal — a task actually running means the whole chain (image pull via exec role → Fargate ENI in the subnet → container start) worked.

Debug order if runningCount stays 0

  1. aws ecs describe-services ... --query 'services[0].events[0].message' — ECS posts the failure reason here in plain English. Read it first.
  2. describe-tasks on the stopped task → stoppedReason. Common ones: CannotPullContainerError (public IP missing, or exec role can't reach ECR — check assignPublicIp=ENABLED and the exec role), ResourceInitializationError (SG blocking egress, or subnet has no internet path).
  3. Task def cpu/memory rejected at registration → invalid Fargate pair; fix to 256/512.

The two things that trip everyone on first Fargate deploy: forgetting the execution role (image pull fails) and forgetting assignPublicIp=ENABLED in a public subnet (image pull also fails, different error). Both covered above.

Task 39

The Nautilus DevOps team has been tasked with creating an internal information portal for public access. As part of this project, they need to host a static website on AWS using an S3 bucket. The S3 bucket must be configured for public access to allow external users to access the static website directly via the S3 website URL.

Task Requirements:

Create an S3 bucket named nautilus-web-205059040. Configure the S3 bucket for static website hosting with index.html as the index document. Allow public access to the bucket so that the website is publicly accessible. Upload the index.html file from the /root/ directory of the AWS client host to the S3 bucket. Verify that the website is accessible directly through the S3 website URL.

Solution

S3 Static Website Hosting Task (nautilus-web)

S3 static website hosting — this one fights the modern defaults hard. Since 2023 every new bucket ships with Block Public Access fully ON and ACLs disabled, but static website hosting requires the opposite: public reads via a bucket policy. So the real work is deliberately undoing the safe defaults — disable BPA, then attach a public-read policy. That's the crux; the hosting config itself is trivial.

Run on aws-client:

REGION=us-east-1
BUCKET=nautilus-web-205059040

# 1. Create the bucket (us-east-1: no LocationConstraint)
aws s3api create-bucket --bucket $BUCKET --region $REGION

# 2. Disable Block Public Access (the safe default that blocks website hosting)
aws s3api put-public-access-block \
  --bucket $BUCKET \
  --public-access-block-configuration \
    "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false" \
  --region $REGION

# 3. Attach a public-read bucket policy (this is what makes objects world-readable)
cat > /tmp/web-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::$BUCKET/*"
    }
  ]
}
EOF

aws s3api put-bucket-policy \
  --bucket $BUCKET \
  --policy file:///tmp/web-policy.json \
  --region $REGION

# 4. Enable static website hosting with index.html
aws s3 website s3://$BUCKET/ --index-document index.html

# 5. Upload index.html from /root
aws s3 cp /root/index.html s3://$BUCKET/index.html --region $REGION

Why each step is shaped this way

  • Step 2 must come before step 3. BlockPublicPolicy=true (the default) actively rejects any attempt to attach a public bucket policy — you'd get AccessDenied on the put-bucket-policy call. So you disable BPA first, then the policy takes. All four flags to false fully opens it; BlockPublicPolicy and RestrictPublicBuckets are the two that specifically gate policies, but for website hosting you clear all four.
  • The bucket policy — not ACLs — is what grants public read. The old way was --acl public-read, but ACLs are disabled by default now (bucket-owner-enforced) and AWS steers you away from them. The s3:GetObject policy with Principal: "*" on arn:.../* (object-level, note the /*) is the current correct mechanism. GetObject on /* = anyone can read any object = the website serves.
  • aws s3 website is the high-level shortcut for enabling hosting. Equivalent to s3api put-bucket-website with a config block, but one line. --index-document index.html is the required index doc; you could add --error-document error.html but the task doesn't ask.
  • create-bucket in us-east-1 takes no LocationConstraint — the special-region rule again.

The website URL — a specific endpoint format

This is NOT the regular bucket URL. Two different hostnames:

  • REST/object URL: https://nautilus-web-205059040.s3.amazonaws.com/index.html
  • Website endpoint (what the task wants): http://nautilus-web-205059040.s3-website-us-east-1.amazonaws.com

The website endpoint uses s3-website-<region> in the hostname and is HTTP only (no HTTPS on native S3 website hosting — you'd need CloudFront for TLS). That's the URL that serves index.html at the root.

Verify

# Confirm hosting config
aws s3api get-bucket-website --bucket $BUCKET --region $REGION

# Confirm BPA is off
aws s3api get-public-access-block --bucket $BUCKET --region $REGION \
  --query 'PublicAccessBlockConfiguration'

# Build the website URL and hit it
WEB_URL="http://${BUCKET}.s3-website-${REGION}.amazonaws.com"
echo "$WEB_URL"
curl -I "$WEB_URL"

Want the website config showing index.html, all four BPA flags false, and curl -I returning HTTP/1.1 200 OK. The 200 at the s3-website- URL is the task's success proof — it means public read + hosting + the uploaded index all line up.

Debug order if it doesn't serve

  1. 403 Forbidden → either BPA still blocking (re-check step 2 actually applied) or the bucket policy didn't attach. These are the two public-access gates; one is still closed.
  2. 404 Not Foundindex.html didn't upload, or hosting isn't enabled. Check aws s3 ls s3://$BUCKET/ shows the file and get-bucket-website returns a config.
  3. Wrong URL entirely → you hit the .s3.amazonaws.com REST endpoint instead of .s3-website-<region>.amazonaws.com. Only the website endpoint renders index.html at root.

Wired in this order it serves clean.

Task 40

The Nautilus Development Team recently deployed a new web application hosted on an EC2 instance within a public VPC named nautilus-vpc. The application, running on an Nginx server, should be accessible from the internet on port 80. Despite configuring the security group nautilus-sg to allow traffic on port 80 and verifying the EC2 instance settings, the application remains inaccessible from the internet. The team suspects that the issue might be related to the VPC configuration, as all other components appear to be set up correctly. The DevOps team has been asked to troubleshoot and resolve the issue to ensure the application is accessible to external users.

As a member of the Nautilus DevOps Team, your task is to perform the following:

Verify VPC Configuration: Ensure that the VPC nautilus-vpc is properly configured to allow internet access.

Ensure Accessibility: Make sure the EC2 instance nautilus-ec2 running the Nginx server is accessible from the internet on port 80.

Solution

VPC Internet Access Troubleshooting Task (nautilus-vpc / nautilus-ec2)

A troubleshooting task, not a build-from-scratch — so the approach is diagnose-then-fix, not blindly recreate. The scenario is textbook: SG allows 80, instance is fine, "suspect the VPC." The near-certain culprit in a public-VPC-that-doesn't-work is one of the route/IGW links being broken — either no IGW attached, or the subnet's route table lacks the 0.0.0.0/0 → IGW route, or the instance has no public IP.

Run on aws-client.

Diagnose — gather the full picture first

REGION=us-east-1

VPC_ID=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=nautilus-vpc \
  --region $REGION --query 'Vpcs[0].VpcId' --output text)
VPC_CIDR=$(aws ec2 describe-vpcs --vpc-ids $VPC_ID --region $REGION \
  --query 'Vpcs[0].CidrBlock' --output text)

IID=$(aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=nautilus-ec2" "Name=instance-state-name,Values=running,stopped" \
  --region $REGION --query 'Reservations[0].Instances[0].InstanceId' --output text)
SUBNET_ID=$(aws ec2 describe-instances --instance-ids $IID --region $REGION \
  --query 'Reservations[0].Instances[0].SubnetId' --output text)
PUB_IP=$(aws ec2 describe-instances --instance-ids $IID --region $REGION \
  --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)

echo "VPC=$VPC_ID  CIDR=$VPC_CIDR  instance=$IID  subnet=$SUBNET_ID  publicIP=$PUB_IP"

# ── DIAGNOSE #1: Is an IGW attached to this VPC? ──
IGW_ID=$(aws ec2 describe-internet-gateways \
  --filters "Name=attachment.vpc-id,Values=$VPC_ID" \
  --region $REGION --query 'InternetGateways[0].InternetGatewayId' --output text)
echo "IGW: $IGW_ID"

# ── DIAGNOSE #2: What route table serves the instance's subnet, and does it route to the IGW? ──
RTB_ID=$(aws ec2 describe-route-tables \
  --filters "Name=association.subnet-id,Values=$SUBNET_ID" \
  --region $REGION --query 'RouteTables[0].RouteTableId' --output text)
# if no explicit association, it's using the main RTB:
if [ "$RTB_ID" = "None" ] || [ -z "$RTB_ID" ]; then
  RTB_ID=$(aws ec2 describe-route-tables \
    --filters "Name=vpc-id,Values=$VPC_ID" "Name=association.main,Values=true" \
    --region $REGION --query 'RouteTables[0].RouteTableId' --output text)
fi
echo "Subnet's RTB: $RTB_ID"
aws ec2 describe-route-tables --route-table-ids $RTB_ID --region $REGION \
  --query 'RouteTables[0].Routes'

That output tells you exactly which link is broken.

Fix — each possible fault (idempotent, only creates what's missing)

# ── FIX #1: No IGW → create + attach one ──
if [ "$IGW_ID" = "None" ] || [ -z "$IGW_ID" ]; then
  IGW_ID=$(aws ec2 create-internet-gateway --region $REGION \
    --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=nautilus-igw}]' \
    --query 'InternetGateway.InternetGatewayId' --output text)
  aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID --region $REGION
  echo "Created + attached IGW: $IGW_ID"
fi

# ── FIX #2: Missing default route → add 0.0.0.0/0 -> IGW to the subnet's RTB ──
# (create-route errors if it exists; replace-route as fallback)
aws ec2 create-route --route-table-id $RTB_ID \
  --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID --region $REGION 2>/dev/null || \
aws ec2 replace-route --route-table-id $RTB_ID \
  --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID --region $REGION

# ── FIX #3: Subnet doesn't auto-assign public IPs (for future launches) ──
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_ID \
  --map-public-ip-on-launch --region $REGION

# ── FIX #4: The RUNNING instance has NO public IP ──
# map-public-ip-on-launch only affects NEW launches — an already-running instance
# without a public IP needs an Elastic IP associated.
if [ "$PUB_IP" = "None" ] || [ -z "$PUB_IP" ]; then
  ALLOC_ID=$(aws ec2 allocate-address --domain vpc --region $REGION \
    --query 'AllocationId' --output text)
  aws ec2 associate-address --instance-id $IID --allocation-id $ALLOC_ID --region $REGION
  PUB_IP=$(aws ec2 describe-instances --instance-ids $IID --region $REGION \
    --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
  echo "Associated EIP: $PUB_IP"
fi

The reasoning behind each fault

  • The #1 cause of "public VPC, SG's fine, still unreachable" is the IGW+route. SG allowing 80 is necessary but not sufficient — it governs the instance's firewall, but if the subnet has no route to an IGW, packets from the internet never reach the instance in the first place. The SG check passing is exactly why the team is confused: they verified the wrong layer.
  • map-public-ip-on-launch is retroactively useless. Setting it fixes future launches but does nothing for the already-running nautilus-ec2. If the running instance lacks a public IP, the only fix is associating an Elastic IP — that's FIX #4, and it's the subtle one people miss after correctly fixing the routing.
  • Explicit vs main route table matters. If the subnet has no explicit RTB association, it silently uses the VPC's main route table — and if someone added the IGW route to a custom RTB that the subnet isn't associated with, it looks configured but isn't. The diagnose step resolves which RTB actually serves the subnet, so you fix the right one.
  • Also worth a glance: the NACL. Security groups are stateful, but network ACLs are subnet-level, stateless, and could block 80 or the ephemeral return ports. Default NACLs allow all, so it's rarely the culprit, but if everything above checks out and it's still dead, that's the next place to look:
aws ec2 describe-network-acls \
  --filters "Name=association.subnet-id,Values=$SUBNET_ID" \
  --region $REGION --query 'NetworkAcls[0].Entries'

One thing to also confirm — that Nginx is actually running on the box (the task says it is, but "verify EC2 settings" is part of it). If you have SSH: systemctl status nginx. If not, the curl below tells you regardless.

Verify the fix end to end

# Full chain re-check
aws ec2 describe-route-tables --route-table-ids $RTB_ID --region $REGION \
  --query 'RouteTables[0].Routes[?DestinationCidrBlock==`0.0.0.0/0`]'

echo "Testing http://$PUB_IP ..."
curl -I --max-time 10 "http://$PUB_IP"

Want the route table showing 0.0.0.0/0 with a GatewayId of igw-... and State: active, and curl -I returning HTTP/1.1 200 OK with an nginx Server header. That 200 from the public IP is the resolution — external reachability on port 80 restored.

Most likely single fix

In order of probability for this scenario: missing 0.0.0.0/0 → IGW route (or the IGW not attached at all) is the classic "public VPC that isn't actually public." The EIP fix (#4) is the second-most-common gotcha. Run the diagnose block first to see which it actually is rather than applying all fixes blind — though the fix block is safe to run wholesale since each piece is guarded.