AWS re/Start Lab · Storage

Creating a Website on S3

Deployed the Café & Bakery static site to an Amazon S3 bucket using the AWS CLI from an EC2 instance. Provisioned a dedicated IAM user with managed S3 access, opened the bucket to public reads via ACLs, and built a small bash script to make subsequent deploys repeatable.

Lab Summary

Connected to an Amazon Linux EC2 instance through SSM Session Manager, configured the AWS CLI, and created an S3 bucket named quijanolab in us-west-2. Created an IAM user (awsS3user), attached the AmazonS3FullAccess managed policy, and re-logged in as that user. Enabled ACLs on the bucket, uploaded the static site with aws s3 cp --acl public-read, and turned the bucket into a website endpoint with aws s3 website. Finished by automating the deploy with a one-line bash script — which hit a shebang bug on the first try.

Bucket & Region

Bucket quijanolab created in us-west-2 with explicit LocationConstraint, which is required for every region other than us-east-1.

IAM Scoping

Dedicated user awsS3user with the AWS managed policy AmazonS3FullAccess. The user signs in with the account ID and password Training123!.

Public Hosting

Public access unblocked, Object Ownership set to ACLs enabled so that --acl public-read on upload would actually apply to the objects.

Walkthrough

The lab fits on a single linear path: connect, configure, provision, secure, deploy, automate.

01

Connect to EC2 via SSM and configure the AWS CLI

  • Opened the InstanceSessionUrl in a new tab to reach the EC2 instance through AWS Systems Manager. SSM avoids needing an SSH key or open port 22.
  • Switched from ssm-user to ec2-user with sudo su -l ec2-user so subsequent file operations land under /home/ec2-user.
  • Ran aws configure and pasted the lab's access key, secret key, region us-west-2 and output format json.
Why us-west-2: the lab fixes the region. Picking anything else would have made the bucket creation fail because the credentials are issued for that region only.
02

Create the S3 bucket

  • Picked the bucket name quijanolab — must be globally unique across all AWS accounts.
  • Created the bucket with the explicit LocationConstraint:
aws-clisuccess
aws s3api create-bucket \
  --bucket quijanolab \
  --region us-west-2 \
  --create-bucket-configuration LocationConstraint=us-west-2

Response:

response.json
{
    "Location": "http://quijanolab.s3.amazonaws.com/"
}
Gotcha: outside us-east-1, omitting --create-bucket-configuration LocationConstraint=<region> returns IllegalLocationConstraintException. us-east-1 is the only region that rejects the flag.
03

Create the IAM user and attach the S3 policy

  • Created the user and a console login profile:
aws-cli
aws iam create-user --user-name awsS3user
aws iam create-login-profile --user-name awsS3user --password Training123!
  • Listed AWS managed policies filtered by name to find the right one without guessing the ARN:
aws-cli
aws iam list-policies --query "Policies[?contains(PolicyName,'S3')]"

The output included AmazonS3FullAccess with ARN arn:aws:iam::aws:policy/AmazonS3FullAccess. That is the policy attached:

aws-cli
aws iam attach-user-policy \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
  --user-name awsS3user
  • Copied the 12-digit Account ID from the console, signed out of the lab user, and re-signed in as awsS3user with password Training123!.
Why a managed policy: for a scoped, well-known permission like "full S3 access", AWS managed policies remove guesswork. They are versioned, auditable, and attach by ARN with no JSON authoring.
04

Open the bucket for public reads (Block public access + ACLs)

  • Under Permissions → Block public access, unchecked Block all public access and confirmed.
  • Under Permissions → Object Ownership, switched from the default ACLs disabled to ACLs enabled and acknowledged that the bucket-owner-enforced setting would be turned off.
Why ACLs matter here: the upload command in the next step uses --acl public-read. That flag only takes effect when Object Ownership is set to ACLs enabled. Without it, every object request would 403, even with public access unblocked at the bucket level.
05

Extract the site, enable static hosting, and upload

  • Extracted the lab's archive and entered the resulting directory:
bash
cd ~/sysops-activity-files
tar xvzf static-website-v2.tar.gz
cd static-website
ls   # css/  images/  index.html
  • Turned the bucket into a static website endpoint and defined the index document:
aws-cli
aws s3 website s3://quijanolab/ --index-document index.html
  • Uploaded the whole site recursively with the canned ACL public-read:
aws-cli
aws s3 cp /home/ec2-user/sysops-activity-files/static-website/ \
  s3://quijanolab/ \
  --recursive --acl public-read

aws s3 ls quijanolab   # verify objects

The Shebang Bug

Task 8 asked for a small bash script to re-run the deploy after editing the local files. The first attempt failed twice with the same error. The cause was a single missing newline.

Symptom: ./update-website.sh printed /bin/bash: aws s3 cp /home/ec2-user/sysops-activity-files/static-website/ s3://quijanolab/ --recursive --acl public-read: No such file or directory and the script exited without uploading anything.
update-website.sh — first attempt broken
#!/bin/bash aws s3 cp /home/ec2-user/sysops-activity-files/static-website/ s3://quijanolab/ --recursive --acl public-read

Everything is on one line. Bash never gets to see the aws command.

update-website.sh — fixed works
#!/bin/bash
aws s3 cp /home/ec2-user/sysops-activity-files/static-website/ s3://quijanolab/ --recursive --acl public-read

Shebang on its own line. The command starts on the next line as a normal bash statement.

Why the broken version fails

The shebang line has a very specific shape: #! followed by the absolute path of an interpreter, and then at most one literal argument string. The kernel parses that first line when it executes the script and does roughly:

execve("/bin/bash", ["/bin/bash", "<everything after the first space>", "./update-website.sh"], envp)

So in the broken file, /bin/bash was being invoked with the single argument "aws s3 cp /home/ec2-user/sysops-activity-files/static-website/ s3://quijanolab/ --recursive --acl public-read". Bash treated that whole string as the path to a script to run — and there is no file with that absurdly long name, hence No such file or directory. The aws binary was never reached.

Splitting the shebang onto its own line makes the kernel invoke /bin/bash ./update-website.sh, and bash then reads the script line by line like any other source file. The second line becomes a normal command and runs as expected.

After fixing the script:

bash
chmod +x update-website.sh
./update-website.sh
Takeaway: shebangs are not bash syntax — they are a kernel-level loader convention. Bash itself would happily run a script with the shebang merged onto one line, because to bash the #!... is just a comment. The kernel is the one that cares, and it only looks at that very first line before exec'ing the interpreter.

Command Reference

The CLI commands that drove this lab, grouped by purpose.

s3api

Bucket lifecycle

  • aws s3api create-bucket --bucket <name> --region us-west-2 --create-bucket-configuration LocationConstraint=us-west-2 : creates a bucket. Outside us-east-1, the LocationConstraint is mandatory.
s3

Static hosting + uploads

  • aws s3 website s3://<bucket>/ --index-document index.html : turns on static website hosting and points the endpoint at index.html.
  • aws s3 cp <local> s3://<bucket>/ --recursive --acl public-read : recursive upload, applying the canned ACL public-read to every object.
  • aws s3 ls <bucket> : quick verification that objects landed.
iam

IAM user provisioning

  • aws iam create-user --user-name <name> : creates the IAM identity.
  • aws iam create-login-profile --user-name <name> --password <pwd> : enables console sign-in.
  • aws iam list-policies --query "Policies[?contains(PolicyName,'S3')]" : finds managed policies by name using JMESPath, no need to memorize ARNs.
  • aws iam attach-user-policy --policy-arn <arn> --user-name <name> : attaches a managed policy to the user.
bash

Deploy automation

  • Final update-website.sh:
update-website.sh
#!/bin/bash
aws s3 cp /home/ec2-user/sysops-activity-files/static-website/ s3://quijanolab/ --recursive --acl public-read
  • chmod +x update-website.sh : make it executable.
  • ./update-website.sh : push the local changes to S3 in one step.

Key Learnings

What Was Actually Learned

How to host a fully public static site on S3 using only the AWS CLI from EC2.
LocationConstraint is required everywhere except us-east-1.
Object Ownership has to be set to ACLs enabled for --acl public-read to take effect; unblocking public access alone is not enough.
Scoping an IAM user by attaching a managed policy (AmazonS3FullAccess) via ARN is faster than writing inline JSON for a well-known permission set.
The shebang is a kernel convention, not bash syntax. Everything after the interpreter path is passed as a single argument, which is why #!/bin/bash <cmd> on one line breaks.

Technical Conclusion

S3 static hosting is one of the cheapest "deploy a website" paths AWS offers: no compute, no load balancer, just a bucket configured for web requests. The lab also reinforces that public access on S3 is a layered problem — bucket-level Block Public Access, Object Ownership, and per-object ACLs all have to align before unauthenticated reads succeed.

The deploy automation step was deceptively simple but produced the most valuable lesson of the lab: a misplaced newline turned a one-line script into a kernel-level error. That detail is the kind of thing only debugging by hand teaches.

The optional aws s3 sync challenge was not completed — the base lab is the verified deliverable here.