DEV Community

German Rodriguez
German Rodriguez

Posted on

The Cloud Resume - AWS

This is my version of the Cloud Resume Challenge - AWS. I discovered this project online a while ago and found myself starting it but never quite finishing—until now! Here, I provide an overview and some details of my work. I also include links to videos, blog posts, and web pages that were helpful to me during the process.

Parts 1 to 11 were mostly completed using the AWS Management Console. On part 12, everything was done using the CLI and Terraform, which I thoroughly enjoyed learning and working with.

Here's the link to the project's GitHub repository and here's the link for my resume's website.

Feel free to leave any comments or questions—let me know what you liked, what you didn’t, and how I can improve my skills. I’d greatly appreciate your feedback!

The project

Project diagram

1.        Certification

Earned the AWS Solutions Architect Associate in 2022.

2.        HTML

Wrote a basic html website for my cv.

3.        CSS

Provided simple styling using basic css.

4.        Static website

Uploaded the project’s directory to an S3 bucket and served the cv as an static website.

Had to configure the bucket as an static website which required disabling the Bloc all public access option, also created a bucket policy to allow getting objects from the bucket, and enabled Static website hosting.

5.        HTTPS

I deployed a CloudFront distribution to implement HTTPS for my website. I assigned my S3 bucket as the origin and configured an origin path to the specific "folder" where the latest version of my project is stored. I did this as a workaround to the fact that S3 automatically looks for an index.html file in the / directory. This approach allowed me to maintain multiple "directories" for different versions of my project within the same bucket.

6.        DNS

Using Route 53, I purchased a domain name: thecodemango. The cost was $14 USD. After the purchase, the fee to keep a hosted zone is $0.50 USD per month (as of the time of this publication). For details, check the service’s billing information webpage.

I also had to obtain a certificate from AWS Certificate Manager in order to add a custom domain name to the CloudFront distribution. The process was simple and straightforward.

7.        JavaScript

The challenge provided a link to a Codecademy JavaScript course, which I took and would recommend to anyone who wants to gain introductory knowledge about JavaScript.

Working on this section (and the next one, Database), I realized something important about myself: I tend to get impatient and sometimes don't fully understand what is being asked. Instead of taking the time to read the entire project specification and understand the task as a whole, I often jump into the practical part right away. This frequently leads to problems.

I spent a lot of time trying to make my JavaScript code communicate directly with the database, which resulted in large project files (inefficiency), hardcoded credentials (a security risk), and the need to take a few steps back to address these issues (wasting time).

Looking back, though, the time wasn't completely wasted. I was able to learn more about myself and pick up a few things about async programming in JavaScript. Special thanks to my friend Nery!

Now, for the technical details: I implemented the core functionality using Gabor Szabo’s tutorial. The code utilizes localStorage to save the counter’s value in the browser’s storage. I used this to store a random UUID, which helps identify a returning visitor, preventing the counter from increasing on every reload or refresh by the same user. The script also includes two functions that make API calls to DynamoDB—one to retrieve the counter value and another to increment and store the updated value in the database.

8.        Database

I encountered a common approach on multiple occasions: using the localStorage of the user's browser to store the counter's value. However, the challenge suggested using DynamoDB to store and retrieve the counter’s value.

After grasping the basic concepts of DynamoDB, creating the table through the console was straightforward, and working with the Python and JavaScript SDKs was easy, thanks to the comprehensive documentation AWS provides.

9.        API

To create the API, the challenge suggested using API Gateway and Lambda. I chose to use an HTTP API due to the simplicity of the task. My decision was based on the information provided in this video.

First, I created two Lambda functions: one to retrieve the counter and another to update the database with the counter's value. Then, I set up two routes—one for each function—and configured a separate integration for each Lambda function.

While working on the API, I also learned about CORS—its purpose, basic configuration, and importance for security.

10.  Python - Lambda

I wrote the two Lambda functions using Python. To better understand Lambda functions, I referred to this webpage and found this video particularly helpful.

An important part of this step was managing permissions. Since I started this project over a year after earning my certification, I needed to refresh my knowledge of roles and policies in AWS. To do so, I took the time to read and explore various AWS documentation related to roles and policies.

11.  Tests

At first, I wasn’t sure what to test, as the Python portion of the project consists of just two Lambda functions that perform a very simple task.

While reading about testing on Real Python, I learned about side effects. Side effects occur when executing a piece of code during tests alters other aspects of the environment, such as modifying a file on the filesystem or changing a value in a database.

I had to consider these side effects when writing my tests but decided not to invest time in creating highly elaborate tests at this stage. Instead, I implemented some basic tests, such as verifying that the correct input is passed to the put_item function.

Here's another video video I found useful.

12.  Infrastructure as Code

For this step, I completed the Terraform Basics Training Course on KodeKloud.com. However, I found myself in the same position as I was after finishing university and college—I went through the material and labs without truly absorbing the knowledge, mindlessly watching videos and completing tasks.

I realized how significant gaps between the days I worked on the project can drastically affect both my progress and learning. I felt overwhelmed by the uncertainty that arose from having to work on this task.

I decided to take a step back and break the workload into smaller chunks. Instead of focusing on the entire project, I started by defining a single resource and working to understand the process before moving on.

I started with S3. Ran into two problems after completing the web configuration for the bucket:

a. The website wasn’t being served properly. Every time I tried to access the endpoint URL for the resume, the browser would download the HTML file instead of displaying the CV. This happened because, initially, I uploaded the files using Terraform, which doesn’t set metadata values by default—metadata that is essential for proper HTTP access.

To fix this, I first uploaded the files manually using the AWS Management Console. While there is a Terraform module that can map MIME (or media) types to filenames, I chose not to use it. Later, while working with GitHub Actions, I discovered two AWS CLI commands that could address this issue: aws s3api put-object and aws s3 cp. I chose the latter because it automatically sets metadata. If you’d like to learn more, you can check out this blog post.

b. The counter wasn’t working. My JavaScript code uses the randomUUID() method from the Crypto interface, but this feature is only available in secure contexts (HTTPS). At this stage (setting up S3), HTTPS wasn’t implemented yet. To resolve this issue, I moved on to the next steps of the challenge, knowing that HTTPS would eventually be implemented during the CloudFront setup.

When configuring CloudFront I overlooked an important factor. After creating my S3 bucket I configured static web hosting to serve the CV over the internet.However, I didn’t realize that this was unnecessary—at least for my project—when using a CloudFront distribution to point to the S3 bucket.

As a result, I wrote the Terraform code for the S3 bucket as though it was supposed to serve the CV directly. This configuration conflicted with the setup needed to create the CloudFront distribution through Terraform. After disabling the static web hosting feature, I was able to successfully create the distribution, and the issue with the counter was resolved.

I also reused an existing SSL certificate to enable a CNAME (an alternate domain name for the distribution). The certificate included a wildcard (*) at the beginning of the domain name, allowing it to cover multiple subdomains with a single certificate. For more information on this, you can refer to AWS documentation.

For Route 53, I used the same hosted zone I had previously created when setting up the CV using the console. I fetched the information using a data block and created an A record (specifically, an alias record) pointing to the CloudFront distribution created with Terraform.

When creating the DynamoDB table, I encountered a minor issue. I was trying to define the table with two attributes: countID, which serves as my hash key, and counter, which is simply an attribute to store the value of the web page visitor counter. However, I kept getting the error: "All attributes must be indexed. Unused attributes: ...".

The solution came from this Stack Overflow answer: "The attribute blocks inside aws_dynamodb_table resources are not defining which attributes you can use in your application. They are defining the key schema for the table and indexes." Since the counter attribute is not part of the key schema, I realized I could create it later in my configuration.

When it was time to work on the Lambda functions, I hit a roadblock. I felt overwhelmed by the task, even though it wasn’t particularly difficult. Not knowing how to approach it caused me to procrastinate and waste a lot of time. A brief conversation with my friend Nery brought some clarity to the situation: when facing a challenge, it’s okay to step away from the problem for a while, but it’s crucial to make a commitment to return to it after a set period of time. Otherwise, you risk getting distracted and wasting a lot of time.

To create the functions, I followed the steps outlined in this video. While working with the roles and policies, I made a mistake. When following the video, I didn’t fully understand the process of creating a policy document and then setting up a role to assume that policy. My initial thought process was: create a role to attach to a lambda function that contains a policy with the necessary permissions for the function to access DynamoDB and write logs on CloudWatch. Which is basically correct but it doesn't take into consideration the trust policy.

When wrote the code on Terraform, I was writing the trust policy as it was the permission policy, and this was throwing a MalformedPolicyDocument: Has prohibited field Resource error. This means that the policy document associated with the IAM role doesn't comply with the expected format or structure because (in this case) it contains a field named "Resource" that shouldn't be there.

I didn't encounter any issues creating and configuring the API Gateway. The documentation is clear and the steps to follow are simple.

To follow a more secure approach I setup the state file on a S3 bucket. This backend also supports state locking and consistency checking via DynamoDB. For more information check this link.

13. Source Control

After creating a repository and uploading my code I learned the basics of GitHub Actions. This allowed me to implement CI/CD in an easy way.

14. CI/CD (Back end)

Before creating my first workflow, I decided to follow a more secure approach than hardcoding credentials to give GitHub Actions access to AWS. I configured an OpenID Connect (OIDC) identity provider (IdP) following this guide by AWS to use IAM roles for connecting GitHub Actions and allowing it to perform actions in AWS.

The connection configuration was straightforward, but scoping the permissions was more challenging. The permissions for the role should be restricted to the specific actions you want that role to perform—nothing more.

One approach could be to allow certain actions for specific resources that contain specific tags. for example, you could configure the role to allow performing certain actions on the lambda functions that contain the tag with the key project and the value cloud-resume-v1. YThis requires applying a condition to the permission policy to match your tag's key and value.

There are many ways to achieve the desired outcome, which is why it’s important to read the recommendations and best practices provided by the developers or providers of the tools you use.

To run the Python tests, I created a workflow that triggers on a push to the Lambda function’s Python files. This workflow executes a Python unit test, and if the test passes, the job proceeds to redeploy the Lambda function with the updated code using Terraform.

At the time of writing, I don’t have a workflow to trigger a job after changes to a Terraform file because I’m not yet comfortable with auto-approving infrastructure changes.

15. CI/CD (Front End)

The challenge suggests creating a second repository for the front end of the project. However, since my front end is relatively simple, all its files are contained in a subdirectory within the project's directory. Instead of creating a separate repository, I set up a workflow that triggers every time a file is pushed to the front end subdirectory. And as mentioned earlier, the workflow would make use of the aws s3 cp command to upload the files.

If I implement a more elaborated front end in a future version, I will consider using a different repository for it.

Conclusion

Thank you for making it all the way down here and taking the time to review my work! This project taught me a lot, not only technically but also personally. Here are some of my concluding points:

  • Terraform is a powerful tool that simplifies cloud resources provisioning and maintenance of infrastructure.
  • Working with GitHub Actions provides valuable hands-on experience with CI/CD, helping to deepen your understanding of these concepts.
  • Logging is crucial. Properly setting it up will save you a lot of time when troubleshooting.
  • Security should be a priority throughout the entire process.
  • Follow best practices and recommendations from the developers or providers of the tools you're using.
  • Learn how to properly document your projects since the beginning.
  • Consistency is key. This project took much longer than necessary because I wasn’t consistent. Find a system, framework, or process that works for you and stick to it.

Top comments (0)