Building a Cloud-Native Website with AWS: The Cloud Resume Challenge
Introduction
As a tech enthusiast and cloud professional looking to expand my AWS skills, I recently took on the Cloud Resume Challenge. This project, created by Forrest Brazeal, is an excellent opportunity for anyone – from beginners to experienced professionals – to gain hands-on experience with a variety of AWS services.
The Cloud Resume Challenge is more than just a simple project; it's a comprehensive, real-world application of cloud technologies. Here's why it's such a valuable learning experience:
Practical Application: You're not just learning theory; you're building a functional, cloud-native application.
Service Integration: The challenge requires you to use multiple AWS services together, mimicking real-world scenarios.
Full-Stack Development: From frontend to backend, you'll touch every part of the application stack.
Best Practices: The project encourages the use of Infrastructure as Code (IaC) and CI/CD pipelines, essential skills in modern cloud development.
Portfolio Building: Upon completion, you have a tangible project to showcase to potential employers or clients.
In this blog post, I'll walk you through my journey of completing the Cloud Resume Challenge, detailing each step, the AWS services used, and the valuable insights gained along the way. Whether you're new to AWS or looking to deepen your expertise, I hope my experience will inspire and guide you in your own cloud learning journey.
Table of Contents
Setting Up a Static Website with S3
Amazon S3 (Simple Storage Service) is the foundation of our cloud resume. It's a highly scalable object storage service that can host static websites efficiently and cost-effectively. Here's a step-by-step guide on how to set it up:
Create an S3 bucket:
Log into the AWS Management Console and navigate to S3.
Click "Create bucket" and choose a globally unique name (e.g., "my-cloud-resume-yourname").
In the bucket settings, enable "Static website hosting".
Set the index document to "index.html".
Upload your website files:
Prepare your HTML resume. If you're not comfortable with HTML, you can use a template or convert your existing resume to HTML using online tools.
Upload your HTML, CSS, and any JavaScript files to the bucket.
Ensure the files have public read permissions.
Configure bucket policy:
- In the bucket permissions, add a bucket policy to allow public read access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
Replace "your-bucket-name" with your actual bucket name.
Why S3 for static website hosting?
Scalability: S3 can handle any amount of traffic without you needing to manage servers.
Cost-effective: You only pay for the storage you use and the data transferred.
Simplicity: Easy to set up and requires minimal ongoing maintenance.
Securing the Website with CloudFront
While S3 provides a solid foundation, CloudFront provides a Content Delivery Network (CDN) service. This not only improves the performance of our website but also adds an extra layer of security. Here's how to set it up:
Create a CloudFront distribution:
In the AWS Console, go to CloudFront and click "Create Distribution".
Set the origin domain to your S3 bucket's website endpoint.
Enable "Redirect HTTP to HTTPS" for added security.
Configure SSL/TLS certificate:
Use AWS Certificate Manager (ACM) to create a free SSL/TLS certificate for your domain.
In your CloudFront distribution settings, select this certificate.
Set up Origin Access Identity (OAI):
Create an OAI and associate it with your distribution.
Update your S3 bucket policy to only allow access from this OAI.
Why use CloudFront?
Improved Performance: CloudFront caches your content at edge locations worldwide, reducing latency for global visitors.
Enhanced Security: HTTPS encryption and the ability to configure security headers.
Cost Optimization: CloudFront can reduce your S3 data transfer costs.
Configuring DNS with Route 53
Now that we have our website hosted and a CDN set up, we need to give it a user-friendly domain name. This is where Route 53, AWS's Domain Name System (DNS) web service, comes into play. Here's how to set it up:
Create a hosted zone:
- In Route 53, create a hosted zone for your domain.
Update name servers:
- If your domain is registered elsewhere, update the name servers at your registrar to use Route 53's name servers.
Create record sets:
Create an A record for your domain, aliasing it to your CloudFront distribution.
If needed, create a CNAME record for www subdomain.
Why use Route 53?
Integration: Seamless integration with other AWS services.
Reliability: Backed by AWS's global infrastructure.
Advanced Features: Health checks, traffic flow, and domain registration in one place.
Implementing a Visitor Counter
Now comes the exciting part – adding dynamic functionality to our static website. We'll implement a visitor counter using a serverless architecture with DynamoDB, Lambda, and API Gateway. This showcases how even a simple static site can incorporate dynamic elements using cloud services.
DynamoDB for Data Storage
DynamoDB is AWS's fully managed NoSQL database service. It's perfect for our use case due to its scalability and low latency. Here's how to set it up:
Create a DynamoDB table:
Name: "VisitorCount"
Partition key: "id" (String)
Add an item with id "visitors" and an attribute "count" set to 0.
Why DynamoDB?
Fully Managed: No servers to provision or manage.
Scalability: Automatically scales to handle any amount of traffic.
Performance: Single-digit millisecond latency at any scale.
Lambda for Backend Logic
AWS Lambda allows us to run code without provisioning or managing servers. Our Lambda function will update the visitor count in DynamoDB. Here's how to set it up:
Create a Lambda function:
Runtime: Node.js 20.x
Code:
const { DynamoDBClient, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
const client = new DynamoDBClient();
exports.handler = async (event) => {
const params = {
TableName: process.env.TABLE_NAME,
Key: { id: { S: 'visitors' } },
UpdateExpression: 'ADD visitorCount :inc',
ExpressionAttributeValues: { ':inc': { N: '1' } },
ReturnValues: 'UPDATED_NEW'
};
try {
const command = new UpdateItemCommand(params);
const data = await client.send(command);
const visitorCount = data.Attributes.visitorCount.N;
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "https://yourdomain.com",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST"
},
body: JSON.stringify({ visitorCount: parseInt(visitorCount) })
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
headers: {
"Access-Control-Allow-Origin": "https://yourdomain.com",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST"
},
body: JSON.stringify({ error: 'Failed to update visitor count' })
};
}
};
Set up environment variables:
- Add TABLE_NAME environment variable with your DynamoDB table name.
Configure permissions:
- Give the Lambda function permission to read/write to your DynamoDB table.
Why Lambda?
Pay per Use: You only pay for the compute time you consume.
No Server Management: AWS handles all the infrastructure management.
Automatic Scaling: Lambda automatically scales your application by running code in response to each trigger.
API Gateway for HTTP Endpoints
Amazon API Gateway acts as the "front door" for our Lambda function, allowing it to be triggered via HTTP requests. Here's how to set it up:
Create a new API:
- Choose HTTP API type for simplicity and cost-effectiveness.
Create a resource and method:
Create a resource named "visitorcount".
Add a POST method to this resource, integrating it with your Lambda function.
Enable CORS:
- In the API settings, enable CORS for your domain.
Deploy the API:
- Create a new stage (e.g., "prod") and deploy your API.
Why API Gateway?
Managed Service: No need to manage any infrastructure.
Scalability: Handles any number of API calls simultaneously.
Security: Provides authorization and access control.
Frontend JavaScript Integration
Now that our backend is set up, let's integrate it with our frontend:
Add HTML for the visitor counter:
<div id="visitor-count">Visitors: <span id="count">0</span></div>
Add JavaScript to call our API:
function updateVisitorCount() { fetch('https://your-api-gateway-url/visitorcount', { method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: 'cors' }) .then(response => response.json()) .then(data => { document.getElementById('count').textContent = data.visitorCount; }) .catch(error => console.error('Error:', error)); } document.addEventListener('DOMContentLoaded', updateVisitorCount);
Replace 'https://your-api-gateway-url' with your actual API Gateway URL.
Testing the JavaScript Code
Testing is a crucial part of any development process. For our visitor counter, we can use Jest, a popular JavaScript testing framework. Here's how to set it up:
Install Jest:
npm install --save-dev jest
Create a test file (e.g.,
visitorCounter.test.js
):// Mock fetch function global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ visitorCount: 5 }), }) ); // Import the function to test const { updateVisitorCount } = require('./visitorCounter'); test('updates visitor count correctly', async () => { // Set up our document body document.body.innerHTML = '<div id="count">0</div>'; // Call the function await updateVisitorCount(); // Check that the count was updated expect(document.getElementById('count').textContent).toBe('5'); });
Run the tests:
npx jest
This test mocks the fetch function to simulate an API call and checks if our updateVisitorCount function correctly updates the DOM with the returned count.
Additional testing examples could include:
Testing error handling:
test('handles errors gracefully', async () => { global.fetch.mockImplementationOnce(() => Promise.reject("API is down")); console.error = jest.fn(); await updateVisitorCount(); expect(console.error).toHaveBeenCalled(); });
Testing with different response values:
test('handles large numbers correctly', async () => { global.fetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ visitorCount: 1000000 }), }) ); document.body.innerHTML = '<div id="count">0</div>'; await updateVisitorCount(); expect(document.getElementById('count').textContent).toBe('1000000'); });
These tests help ensure our visitor counter works correctly under various conditions, improving the reliability of our cloud resume.
Conclusion
Completing the Cloud Resume Challenge is an excellent way to gain hands-on experience with a variety of AWS services. Throughout this project, we've touched on several key aspects of cloud computing:
Static Web Hosting: Using S3 to host our resume website.
Content Delivery: Implementing CloudFront to improve performance and security.
DNS Management: Configuring Route 53 for a custom domain.
Serverless Computing: Using Lambda for our backend logic.
NoSQL Databases: Utilizing DynamoDB for data storage.
API Development: Creating an API with API Gateway.
Frontend Development: Integrating our backend with JavaScript.
Testing: Implementing unit tests for our JavaScript code.
This challenge provides a comprehensive overview of building a cloud-native application, from frontend to backend. It's an excellent starting point for anyone looking to dive deeper into AWS services or to showcase their cloud skills to potential employers.
In the next part of this series, we'll explore how to automate the deployment of our cloud resume using CI/CD practices with GitHub Actions, further enhancing our cloud development skills.
Remember, the cloud is constantly evolving, and there's always more to learn. Keep exploring, keep building, and most importantly, keep challenging yourself!
Subscribe to my newsletter
Read articles from Gus Feliciano directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by