DEV Community

Cover image for Hands-On AWS: Spin Up IAM, S3 and a Jenkins Server on EC2 (and Fix What Breaks)
David Cletus
David Cletus

Posted on

Hands-On AWS: Spin Up IAM, S3 and a Jenkins Server on EC2 (and Fix What Breaks)

A step-by-step AWS lab with lots screenshots: create and delete an IAM user, host a site on S3, launch an EC2 server and run Jenkins on it, including the two errors I hit and how I fixed them.

Almost every AWS learning path starts with the same three moves: decide who is allowed to do what, store a file somewhere, and start a server. That is IAM, S3 and EC2, and nearly everything else on AWS sits on top of those three.

So I did all three for real, took a screenshot of every click, hit two errors along the way, fixed them, and then deleted everything at the end of each part so nothing was left running on my bill.

This post is the full walkthrough. By the end you will have:

  • Created an IAM user, signed in as that user, forced a password reset, and deleted the user
  • Created an S3 bucket, uploaded a website into it, made it public, and opened it in a browser
  • Launched an Ubuntu server on EC2, connected over SSH, installed Java and Jenkins, and unlocked Jenkins in the browser
  • Seen exactly what two common AWS errors look like, why they happen, and how to fix them
  • Cleaned up every resource so you do not pay for what you are not using

The plan

Part Service What I built How I cleaned up
1 IAM A user called 4thman with console access and a forced password reset Deleted the user
2 S3 A bucket called 4thman-bucket serving my CV website Emptied and deleted the bucket
3 EC2 An Ubuntu server called 4thman-server running Jenkins Stopped, then terminated the instance

Before you start

  • An AWS account. I worked as the root user for this lab (more on why that is not ideal in a moment).
  • A terminal with SSH. I used Git Bash on Windows. macOS and Linux terminals work the same way.
  • A small static website if you want to repeat the S3 part. I used my own CV website, a one-page site built from a Tooplate template called StackCraft: one index.html, one CSS file, one JavaScript file and an images folder. Any simple HTML, CSS and JS site will do.
  • A second browser window in incognito or private mode, for testing the IAM user.

A note on cost: everything here is small, but "small" only stays small if you clean up. That is why every part ends with a teardown. Free tier and credits differ between accounts, so check your own billing page.


Part 1: IAM, deciding who gets to do what

Why I did not just keep using root

Look at the top right corner of the next screenshot. It says root. The root user owns the account and can do absolutely anything, including closing it. You use it to get started, and then you stop using it. Day-to-day work belongs to an IAM user with only the permissions it needs. That principle is called least privilege, and IAM is the service that enforces it.

AWS Console Home, signed in as the root user

Step 1: Open IAM

In the search bar at the top of the console, type iam and pick IAM from the Services list.

Searching for IAM in the console search bar

Step 2: Create the user

In the sidebar, open IAM users. The count says (0), so this account has no IAM users yet. Click Create user.

IAM users page with no users yet and the Create user button

Step 3: Fill in the user details

This screen has four decisions on it:

  1. User name: 4thman
  2. Provide user access to the AWS Management Console: ticked, because I want this user to be able to sign in on the website and not just use the CLI
  3. Console password: I chose Custom password so I could pick my own. The rules are at least 8 characters and a mix of at least three of: uppercase, lowercase, numbers and symbols.
  4. Users must create a new password at next sign-in: ticked. AWS marks this as recommended, and it is a good habit. Whoever receives the temporary password has to replace it with their own on first login.

User details: name, console access, custom password and forced reset

Step 4: Permissions

The next step offers three ways to give permissions: add the user to a group, copy permissions from another user, or attach policies directly. AWS recommends groups, because you manage "the Developers" once instead of managing every developer separately.

For this lab I did not want this user to be able to do anything yet, so I left the defaults alone and clicked Next.

Set permissions step, left on the defaults

Step 5: Review and create

The review page shows the choices I made: custom password, password reset required. The part I want you to notice is Permissions summary. I never attached a policy, but one is listed: IAMUserChangePassword. AWS adds it automatically when you force a password reset, because the user needs permission to change their own password. It lets them do that and nothing else, which is exactly least privilege at work.

Click Create user.

Review page showing IAMUserChangePassword as the only policy

Step 6: Save the sign-in details

The next screen is the only time AWS shows you the console password. It also gives you the sign-in URL, which follows this pattern:

https://<your-account-id>.signin.aws.amazon.com/console
Enter fullscreen mode Exit fullscreen mode

Copy the details or click Download .csv file before leaving this page.

User created, with the console sign-in URL, user name and password

Step 7: Sign in as the new user

I opened an incognito window so I could stay signed in as root in my normal window. On the IAM user sign-in page I entered the account ID, the user name 4thman and the temporary password.

Signing in as the new IAM user in an incognito window

Straight after clicking Sign in, AWS showed this:

Password reset screen shown at first sign-in

That message says the password "has expired or requires a reset", which sounds like a problem. It is not. It is the "must create a new password" option from Step 3 doing its job. I entered the old password once, the new one twice, watched the green Matches appear, and clicked Confirm Password Change.

Password reset successful

Then I signed in again with the new password and landed on the console as 4thman. Notice the account menu at the top right now shows the user name instead of root.

Console Home as the 4thman IAM user

One thing to watch: this console opened in Europe (Stockholm), because the sign-in page had redirected me to a Stockholm sign-in endpoint. IAM users are global, so that does not affect the user. But most other services are regional, so always check the region selector before you create anything.

This user can open the console but cannot do much with it. The only policy attached lets them change their own password. That is the point of starting with nothing and adding permissions deliberately.

Step 8: Delete the user

Back in my root window, the IAM users list now shows 4thman with real activity: last activity and console sign-in a couple of minutes ago, password age of a few minutes. I ticked the box next to the user and clicked Delete user.

Selecting the user and clicking Delete user

AWS makes you type confirm before it deletes anything. It is a small speed bump on purpose. Deleting a user also removes its credentials, so there is no undo.

Typing confirm to delete the user

IAM done. One user created, tested and removed.


Part 2: S3, storing files and serving a website

What S3 actually is

S3 is object storage. You create a bucket (a container), and everything you put in it is an object. Two details are worth knowing before you start:

  • Bucket names are unique across all AWS accounts in the world, not just yours.
  • S3 has no real folders. When the console shows an images/ folder, that is just a shared prefix in the file names. You will see this when the folder appears in the object list later.

Step 1: Find S3 and create a bucket

Search for s3 buckets and open S3. If you have no buckets yet, the landing page shows a big Create bucket button.

Searching for S3 in the console

The S3 landing page and its Create bucket button

Step 2: Name the bucket

I confirmed the region was US East (N. Virginia) us-east-1, kept the type as General purpose, kept the default global namespace, and named the bucket 4thman-bucket. Bucket names must be 3 to 63 characters and can only contain lowercase letters, numbers, periods and hyphens.

Region, bucket type and the bucket name 4thman-bucket

Step 3: Leave the safe defaults alone

Scroll down and you will see two settings I deliberately did not change:

  • Object Ownership: ACLs disabled (recommended). Access is controlled through policies only.
  • Block all public access: ticked. All four sub-settings are on.

AWS builds buckets private by default, and that is a good thing. These two defaults are also the reason I hit an error in a few minutes, so keep them in mind.

Object Ownership with ACLs disabled and Block all public access ticked

At the bottom, default encryption is already on (SSE-S3, with a bucket key enabled). I left it and clicked Create bucket.

Default encryption settings and the final Create bucket button

Step 4: Upload the CV website

The bucket now exists and says Objects (0). Click Upload.

Bucket created and empty, with the Upload button

I added the four files that make up my CV website: index.html, tooplate-stackcraft-script.js, tooplate-stackcraft-style.css, and about.jpg inside an images/ folder. The upload summary reports 4 files totalling 229.1 KB with 0 failures. Notice the destination, s3://4thman-bucket, and that about.jpg shows images/ in the Folder column.

Upload succeeded: 4 files, 229.1 KB, 0 failed

The files are in, but because the bucket is private, nobody on the internet can open them yet. Time to change that.

Error 1: "Access denied" when making the files public

I went back to the object list, ticked every file and folder, and opened the Actions menu to choose Make public using ACL.

)

Selecting all objects and opening the Actions menu

The next page shows two banners, and both matter:

  • A green one: Successfully enabled ACLs (Bucket owner preferred). Because I created the bucket with ACLs disabled, and "Make public" works by setting an object ACL, the console switched the ownership setting for me.
  • A blue one: Public access is blocked because Block Public Access settings are turned on for this bucket.

I read the green one, ignored the blue one, and clicked Make public.

First Make public attempt, with the ACL banner and the Block Public Access warning

The result, for all four objects:

Failed to edit public access: Access denied on all four objects

Failed to edit public access: Access denied.

Reading the error properly

My first instinct with "Access denied" was to check my permissions. But I was the root user, so permissions were not the problem. The blue banner had already told me the cause.

There are two separate gates on public access in S3:

  1. Object ACLs and ownership, which decide how an individual object can be made public. The console fixed this one for me.
  2. Block Public Access, a bucket-level safety switch that overrides ACLs and bucket policies. It was still on, and it wins every time.

Even if the object says "public", Block Public Access says "no".

The fix

Open the bucket and go to the Permissions tab.

Opening the Permissions tab of the bucket

Under Block public access (bucket settings) it says Block all public access: On. Click Edit.

)

Block all public access is On, with the Edit button

Untick Block all public access. That clears all four boxes underneath it. Click Save changes and confirm the warning S3 shows you.

)

Block all public access unticked, then Save changes

Now repeat the original action. Back on the Objects tab I selected everything again and opened Actions.

Selecting all objects again and opening Actions

On the Make public page, I clicked Make public again. This time there was no blue warning.

Second Make public attempt, with no blocking warning

The summary now reads: Successfully edited public access, 4 objects, 229.1 KB, 0 failed.

Successfully edited public access: 4 objects, 0 failed

See it live

I ticked index.html only and clicked Copy URL. The console confirmed with Object URL Copied.

Copying the object URL of index.html

Pasting it into a new tab loaded my full CV website straight from S3, with my name, role, skills, work and experience sections all in place. The URL has this shape:

https://<bucket-name>.s3.<region>.amazonaws.com/index.html
Enter fullscreen mode Exit fullscreen mode

The CSS, JavaScript and image all loaded as well, because I made every object public, not just the HTML file.

My CV website loading straight from S3

A word of caution. Opening a whole bucket to the internet is fine for a throwaway demo. For a real site, keep the bucket private and put CloudFront in front of it, or at least use a bucket policy that only allows reading the specific objects you want public.

Cleanup: empty the bucket, then delete it

S3 will not delete a bucket that still has objects in it. I selected everything and clicked Delete.

Selecting all objects and clicking Delete

S3 asks you to type permanently delete to confirm. This cannot be undone. (The Empty button on the bucket list does the same job in one step.)

Typing permanently delete to confirm

With the bucket empty, I went back to the bucket list, selected 4thman-bucket and clicked Delete.

Selecting the empty bucket and clicking Delete

To confirm, I typed the bucket name. Read the warnings on this page: once a bucket is deleted, its name is released and another AWS user can claim it.

Typing the bucket name to confirm deletion

S3 done. A bucket created, a website served from it, one error fixed, everything removed.


Part 3: EC2, a server in the cloud, running Jenkins

The vocabulary you need first

Term What it means
EC2 instance A virtual server running in AWS
AMI The image the server starts from: operating system plus preinstalled software. I used an Ubuntu image
Instance type The size of the machine. I used t3.micro (2 vCPU, 1 GiB memory)
Key pair A public and private key used to log in over SSH. AWS keeps the public half. You keep the private .pem file
Security group A virtual firewall around the instance that decides which ports accept traffic
Jenkins An open source automation server, widely used to build, test and deploy code (CI/CD)

Step 1: Launch the wizard

Search for ec2 and open EC2. (The green banner partly visible behind the search box is the bucket deletion confirming.)

Searching for EC2 in the console

On the EC2 landing page, click Launch instance.

The EC2 landing page and its Launch instance button

AWS then offered a choice of walkthroughs. I picked Learn about EC2 and launch test instances. That is why a Tutorial panel appears on the right side of the next screenshots: it lists the steps and, handily, ends with a reminder to terminate your instance.

Choosing the Learn about EC2 walkthrough

Step 2: Name it and pick an image

I named the instance 4thman-server, chose Ubuntu from the Quick Start images, and left the number of instances at 1. The Summary panel on the right tracks everything you choose: image, instance type, firewall and storage.

Naming the instance, choosing Ubuntu and setting the number of instances

Step 3: Instance type and key pair

I picked t3.micro. Scrolling down to Key pair (login), I clicked Create new key pair.

Instance type and the Create new key pair link

I named it 4thman-key, kept RSA as the type and .pem as the format (the format for OpenSSH, which is what Git Bash uses), and clicked Create key pair.

Creating the key pair 4thman-key as RSA and .pem

The browser downloaded 4thman-key.pem immediately.

4thman-key.pem downloaded by the browser

Guard this file. AWS only keeps the public half of the key. This download is the only copy of the private half. Lose it and you cannot SSH into the server. Never commit it to GitHub.

Step 4: Network and security group

In Network settings:

  • Auto-assign public IP: Enable, so the server is reachable from the internet
  • Create security group (a new firewall for this instance)
  • Allow SSH traffic from: Anywhere (0.0.0.0/0), so I can log in
  • Allow HTTP traffic from the internet: ticked
  • Allow HTTPS traffic from the internet: left unticked

AWS shows a yellow warning that 0.0.0.0/0 means every IP address. For a short-lived lab that is acceptable. For anything longer, choose My IP instead of Anywhere.

Network settings: public IP, new security group, SSH and HTTP allowed

Step 5: Review and launch

Storage stayed at the default 8 GiB gp3 root volume. The Summary panel showed one instance, the Ubuntu image, t3.micro, a new security group and one 8 GiB volume. I clicked Launch instance.

Summary panel and the Launch instance button

A note on the screenshots in this section. Some of the launch screenshots (the key pair, security group, launch summary, success banner and running view) were captured on an earlier launch of the same setup, so the instance size, instance ID and IP address differ from the server used in the SSH and Jenkins steps. The clicks are identical. When I talk about the server's public IP from here on, use the one on your own instance page.

AWS replied with a green Success banner and the new instance ID. The page also suggests next steps, including Create billing usage alerts. Next time I will set that up before launching anything.

Instance launch initiated successfully

On the Instances page the new server went to Running, and the status checks passed. In the details panel, copy the Public IPv4 address. You need it to connect.

Instance running, with its public IPv4 address

Step 6: Lock down the key file and connect over SSH

While you are here, fix the key's permissions. SSH refuses to use a private key that other users on the machine can read, so restrict it:

cd Downloads
ls -l 4thman-key.pem
chmod 400 4thman-key.pem
ls -l 4thman-key.pem
Enter fullscreen mode Exit fullscreen mode

chmod 400 means read-only for the file owner and nothing for anyone else. In the screenshot the listing goes from -rw-r--r-- to -r--r--r--. Because this is Git Bash on a Windows drive, permission bits do not map exactly like on Linux, but SSH accepted the key anyway.

Restricting the key file with chmod 400

Now connect. The default user on Ubuntu images is ubuntu:

ssh -i "4thman-key.pem" ubuntu@<your-public-ip>
Enter fullscreen mode Exit fullscreen mode

The first time, SSH asks whether you trust this host. Type yes. The Ubuntu welcome banner appears and the prompt changes to ubuntu@ip-172-31-25-206:~$, which means every command from now on runs on the server in AWS and not on my laptop.

Connected to the server over SSH

Step 7: Update the server

sudo apt update
sudo apt upgrade -y
Enter fullscreen mode Exit fullscreen mode

apt update refreshes the list of available packages. apt upgrade -y installs updates without asking for confirmation. On a fresh image there was nothing pending, and apt reported All packages are up to date.

sudo apt update and sudo apt upgrade -y

Step 8: Install Java

Jenkins is a Java application, so Java comes first. fontconfig is a small dependency Jenkins needs for rendering fonts.

sudo apt install fontconfig openjdk-21-jre -y
Enter fullscreen mode Exit fullscreen mode

Installing fontconfig and OpenJDK 21

Check it worked:

java -version
Enter fullscreen mode Exit fullscreen mode

You should see an OpenJDK 21 version string. You can see mine at the top of the screenshot in the next step.

Step 9: Add the Jenkins repository

Ubuntu's default repositories do not include Jenkins. The Jenkins project publishes its own package repository, so we add that and tell apt to trust its signing key:

sudo mkdir -p /etc/apt/keyrings

sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \
  https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key

echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" \
  | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null

sudo apt update
Enter fullscreen mode Exit fullscreen mode

What each piece does:

  • mkdir -p /etc/apt/keyrings makes sure the folder for trusted keys exists
  • wget downloads the Jenkins signing key into it, so apt can verify the packages really come from the Jenkins project
  • echo ... | sudo tee writes a new repository entry that points at pkg.jenkins.io and says which key to check it against
  • apt update reads the new repository. In the output you can see pkg.jenkins.io/debian-stable being fetched

The key file name contains a year. If this step fails, check the official Jenkins installation page for the current key name.

Java version check, Jenkins signing key, repository entry and apt update

Step 10: Install Jenkins

sudo apt install jenkins -y
Enter fullscreen mode Exit fullscreen mode

The output shows Jenkins 2.580.1 (about 53.9 MB) installing along with a small helper package called net-tools. At the very end, Ubuntu prints a message about a pending kernel upgrade and suggests a reboot. That is a notice, not an error. Jenkins installs and runs fine without the reboot.

Installing Jenkins with apt

Step 11: Start Jenkins and check it

sudo systemctl enable --now jenkins
sudo systemctl status jenkins
Enter fullscreen mode Exit fullscreen mode

enable --now does two things at once: it starts Jenkins immediately and makes it start automatically every time the server boots. status shows the result. Look for the green active (running) line, and notice the Java process and the memory use (about 300 MB for me). Press q to leave the status view.

Enabling Jenkins and confirming active (running)

Jenkins is running. Time to open it in the browser.

Error 2: "This site can't be reached"

I took the server's public IP, added Jenkins' default port, and opened http://<your-public-ip>:8080 in Chrome.

Chrome showing ERR_CONNECTION_TIMED_OUT on port 8080

This site can't be reached. The server took too long to respond. ERR_CONNECTION_TIMED_OUT.

Reading the error properly

I had just seen Jenkins report active (running), so the service was fine. The clue is in the wording:

  • A connection refused error means the machine answered and said "nothing is listening on that port". That points at the service.
  • A timed out error means the packets never got an answer at all. Something in between silently dropped them. On AWS, that is almost always the security group.

When I created the instance, I only allowed port 22 (SSH) and port 80 (HTTP). Jenkins listens on 8080, and nothing allowed that port in.

The fix: open port 8080 in the security group

In the EC2 console, open Instances, select the server, and click the Security tab. Under Security groups, click the group's link (mine was launch-wizard-2).

Instances, Security tab and the security group link

On the security group page, the Inbound rules tab lists only two rules, HTTP and SSH. Click Edit inbound rules.

Inbound rules showing only HTTP and SSH, with Edit inbound rules

Click Add rule. A new row appears with the type already set to Custom TCP and the port at 0.

Edit inbound rules with the Add rule button

Set the port range to 8080 and the source to Anywhere-IPv4 (0.0.0.0/0). AWS warns again that this opens the port to everyone, which is fine for a lab and something to tighten later. Click Save rules.

Custom TCP rule for port 8080 from Anywhere-IPv4, then Save rules

Security group changes apply instantly. No restart, no reboot. I reloaded the browser tab and the error was gone.

Unlock Jenkins

Jenkins greeted me with Unlock Jenkins. The browser shows Not secure in the address bar because we are on plain HTTP and not HTTPS. That is expected for a quick lab, and I will come back to it at the end.

The Unlock Jenkins page at port 8080

The page says the first admin password lives in a file on the server. Back in the SSH session:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword
Enter fullscreen mode Exit fullscreen mode

You need sudo because only root can read that file.

Reading the initial admin password on the server

I copied the long string it printed, pasted it into the Administrator password box and clicked Continue.

Pasting the administrator password and clicking Continue

Next came Customize Jenkins, with two choices: Install suggested plugins (the safe default) or Select plugins to install. Jenkins 2.580.1 is up and answering on my server's public IP.

Customize Jenkins: install suggested plugins or select plugins

That is where I stopped for this post. From here Jenkins would install its plugins and ask you to create your first admin user. The goal of this lab was getting Jenkins running on a server I built myself, and that is done.

Cleanup: stop and terminate the server

This is the part people skip, and it is the part that costs money. In the EC2 dashboard, the Resources box shows exactly what is still alive: 1 running instance, 1 key pair, 3 security groups and 1 volume.

EC2 dashboard Resources box before cleanup

I selected the instance and opened the Instance state menu.

Selecting the instance and opening the Instance state menu

I stopped it first. The status moves to Stopping.

Instance stopping

Then I terminated it. Terminated means deleted for good, so the state ends at Terminated, as in the final screenshot. Both green banners are visible here: stopping and termination were both initiated.

Instance terminated

A difference worth remembering: stopped is not deleted. A stopped instance stops charging for compute, but its storage volume still exists and still bills. Terminate when you are done.

The key pair and security groups stay behind after termination. They are free, but delete them in the EC2 console if you will not reuse them, and delete the .pem from your computer too.


The two errors, side by side

Where What I saw Real cause Fix
1 S3, Make public Failed to edit public access: Access denied on all 4 objects Block Public Access was on, and it overrides ACLs. (ACLs were also disabled by default, which the console fixed for me.) Permissions tab, Edit Block public access, untick, Save. Then Make public again
2 Jenkins in the browser ERR_CONNECTION_TIMED_OUT on port 8080 The security group only allowed ports 22 and 80 Add an inbound Custom TCP rule for 8080

And three smaller things that look scarier than they are:

  • "Password has expired or requires a reset" at first IAM sign-in is the forced reset working as designed
  • "Pending kernel upgrade" after installing Jenkins is a reboot suggestion, not a failure
  • chmod 400 showing -r--r--r-- on Git Bash is a Windows quirk, and SSH was fine with it

What to would do differently during production

This lab was about learning the moves, so I took shortcuts that I would not take on anything real:

  • Stop using root. I worked as root all the way through, which is the exact habit Part 1 is meant to break.
  • Do not use 0.0.0.0/0 for SSH. Pick My IP so only my own address can reach port 22.
  • Restrict port 8080 too, and put Jenkins behind HTTPS with a reverse proxy before anyone enters a real password into it.
  • Keep S3 buckets private and serve public sites through CloudFront.
  • Set a billing alert first, before launching anything.
  • Use groups for permissions, not individual users.

Wrap up

Three services, three cleanups, two errors:

  • IAM taught me that new users start with almost no power, and that is a feature
  • S3 taught me that "Access denied" does not always mean your permissions, and that public access has more than one gate
  • EC2 taught me that a timeout and a refusal are different problems, and that the security group is the first place to look when a running service will not answer

Every one of the fixes took less than a minute once I understood the message. The hard part was reading the error properly, not fixing it.

Tried it yourself and ran into a different error? Drop the exact error message in the comments, and let’s troubleshoot it together! If this lab helped you, show some love, hit follow, and join me as I tackle new challenges and share real lessons from my cloud and DevOps journey, one lab at a time.

Top comments (0)