## Task 36 ```bash 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 ```bash 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=`; us-east-1 errors if you add it.) ## Phase 2 — IAM policy + role + instance profile ```bash 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 ```bash 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 ```bash [ -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 ```bash 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 ```bash # 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`: ```bash 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) ```bash 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: ```bash 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 ```bash 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: ```bash 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 ```bash # 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`: ```bash 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-` 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 ```bash # 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 Found`** → `index.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-.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 ```bash 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) ```bash # ── 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: ```bash 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 ```bash # 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.