The Cloud Resume Challenge
Tags: aws, serverless, node, cloudskills
Published: 2022-09-04 14:39:39


## What is this challenge?
The Cloud Resume Challenge is a multiple-step resume project which helps build and demonstrate skills fundamental to pursuing a career as an AWS Cloud Engineer. The project was published by <a href="https://forrestbrazeal.com/">Forrest Brazeal.</a>

The final result [can be seen here](https://www.gabor-havasi.me/) and the code can be checked in [this GitHub repo](https://github.com/Szfinx5/Cloud-Resume-Challenge).


![Topology diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wcqu2eoq3zg5cmkx7jhs.png)
---

## How are the sprints structured?     

I have joined the August sprint. Sprint take place over 4 weeks.
Each week, we build a mini-project that’s valuable in its own right.

---

## The tasks and the progression      

---

#### 1. Certification 
   
The AWS Cloud Practitioner certificate is a requirement to complete this challenge.
To achieve the certificate, I have used only free materials and even got a 50% discount from the exam fee (normally $100)
 - [AWS Cloud Practitioner Essentials](https://explore.skillbuilder.aws/learn/course/134/aws-cloud-practitioner-essentials)
 - [Exam Prep: AWS Certified Cloud Practitioner](https://explore.skillbuilder.aws/learn/course/9449/exam-prep-aws-certified-cloud-practitioner)
 - The 50% discount code will be offered once the first AWS Cloud Practitioner Essentials training is completed.

#### 2. HTML 
The CV is needed to be a HTML document which is styled using CSS. It doesn't need to be an original masterpiece, any HTML CV template will be acceptable. You can find mine [here](https://gabor-havasi.me) for inspiration.

#### 3. CSS
The same goes to CSS too. You can write your own, or use tailwind or something similar. 

#### 4. Static Website
The HTML/CSS website should be deployed to an AWS S3 bucket. I have done it via CloudFormation template. The code is below:

 ```
 MyWebsite:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html
      BucketName: cloud-cv-website
```

#### 5. HTTPS
In order to be able to use HTTPS instead of HTTP, a CloudFront distribution was needed. I have done it via CloudFormation the following way:

 ```
 MyDistribution:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        # ViewerCertificate:
        #     AcmCertificateArn: !Ref MyCertificate
        #     SslSupportMethod: sni-only
        # Aliases:
        #   www.gabor-havasi.me
        #   gabor-havasi.me
        DefaultCacheBehavior:
          ViewerProtocolPolicy: allow-all
          TargetOriginId: cloud-cv-website.s3-website-eu-west-1.amazonaws.com
          DefaultTTL: 0
          MinTTL: 0
          MaxTTL: 0
          ForwardedValues:
            QueryString: false
        Origins:
          - DomainName: cloud-cv-website.s3-website-eu-west-1.amazonaws.com
            Id: cloud-cv-website.s3-website-eu-west-1.amazonaws.com
            CustomOriginConfig:
              OriginProtocolPolicy: match-viewer
        Enabled: "true"
        DefaultRootObject: index.html
```

#### 6. DNS
I have used AWS's Route 53 to point a custom domain name (bought on namecheap.com) to the CloudFront distribution which was set up in the previous step. I have used the following code to add the A records:

 ```
 myRoute53:
    Type: "AWS::Route53::RecordSetGroup"
    Properties:
      HostedZoneId: Z0787651UFL9JUUWJ3WQ
      RecordSets:
        - Name: gabor-havasi.me
          Type: A
          AliasTarget:
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: !GetAtt MyDistribution.DomainName

  MyRoute53www:
    Type: "AWS::Route53::RecordSetGroup"
    Properties:
      HostedZoneId: Z0787651UFL9JUUWJ3WQ
      RecordSets:
        - Name: www.gabor-havasi.me
          Type: A
          AliasTarget:
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: !GetAtt MyDistribution.DomainName
```


#### 7. Javascript
The CV webpage should include a visitor counter that displays how many people have accessed the site. This script should be written in JavaScript.
For the counter to work, we need to add a few extra line in our HTML website and write a JavaScript code:

The script itself:
```
const counter = document.querySelector(".counter-number");
async function updateCounter() {
  let response = await fetch(
    "https://ig4fgtqyx2.execute-api.eu-west-1.amazonaws.com/Prod"
  );
  let data = await response.json();
  counter.innerHTML = `You are the ${data}. visitor to my Cloud Resume Challenge site`;
}

updateCounter();
```
The HTML addition:
```
<div class="counter-number">Couldn't read the counter</div>
```

#### 8. Database
The visitor counter will need to retrieve and update its count in a database somewhere. I have used DynamoDB for this task as it was the most convenient solution. The code from the CloudFormation template to create the DynamoDB table:

```
DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: gabor-havasi-cv
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: "ID"
          AttributeType: "S"
      KeySchema:
        - AttributeName: "ID"
          KeyType: "HASH"
```

#### 9. API
We shouldn't communicate directly with DynamoDB from our Javascript code. Instead, an REAT API should be created that accepts requests from your web app and communicates with the database. 
I have created a single GET method and assigned the Lambda to it. I could have separate GET and POST method to read the counter and update it with two separate Lambda functions, but I choose the monolithic approach as it was simpler in our case. 


#### 10. Python
The Lambda script to retrieve the counter from the database and update the number should be written in Python. I have done it in Node.js at first, but later rewrote it in Python. The final script is below:

```
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('gabor-havasi-cv')
def lambda_handler(event, context):
    response = table.get_item(Key={
            'id':'1'
    })
    record_count = response['Item']['counter']
    record_count = str(int(record_count) + 1)
    print(record_count)
    response = table.put_item(Item={
            'id':'1',
            'counter': record_count
    })
    
    return {
    
    'statusCode': 200,
        'headers': {
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
        },
        'body': record_count}
```

And to deploy it via CloudFormation:
```
Lambdafunction:
    Type: AWS::Serverless::Function
    Properties:
      Policies:
        - DynamoDBCrudPolicy:
            TableName: gabor-havasi-cv
      CodeUri: get-count/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Events:
        getCount:
          Type: Api
          Properties:
            Path: /
            Method: get
```

#### 11. Tests
I have added a test, written in Node.js to test the API. This test will be added to GitHub action in the step 14. During the test, it calls the Lambda twice then check if the return value from the second call is 1 more than the return value from the first call.  

```
import { expect } from "@jest/globals";
import fetch from "node-fetch";

async function testCounter() {
  let response = await fetch(
    "https://ig4fgtqyx2.execute-api.eu-west-1.amazonaws.com/Prod"
  );
  let data = await response.json();
  return data;
}

describe("Testing the API", () => {
  it("API test", async () => {
    const firstResponse = await testCounter();
    console.log(firstResponse);
    const secondResponse = await testCounter();
    console.log(secondResponse);

    const expected = 1;
    expect(secondResponse - firstResponse).toEqual(expected);
  });
});
```


#### 12. Infrastructure as Code
I have used the AWS Serverless Application Model (SAM) template and deployed the different services using the AWS SAM CLI. The code snippets can be found under each step where applicable. 

#### 13. Source Control
I have used Git as source control and hosted the code on GitHub from the beginning of this project. 

#### 14. CI/CD (Back end)
In this step, we need to set up GitHub Actions such that when we push an update to our Serverless Application Model template or Lambda code, test get run. If the test passes, the SAM application should get packaged and deployed to AWS. 
**IMPORTANT, do not commit AWS credentials to source control! The access key and the secret key need to be saved as "SECRETS".** 
For this, I used the following workflow template snippets:

To run the test:
```
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: "npm"
      - run: npm install node-fetch
      - run: npm run build --if-present
      - run: npm run test
```
To deploy to AWS if the test passes:
```
  deploy-to-AWS:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v3
      - uses: aws-actions/setup-sam@v2
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-1
      - run: sam build
      - run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset
```

#### 15. CI/CD (Front end)
Create GitHub Actions such that when you push new website code, the S3 bucket automatically gets updated. The CloudFront cache didn't cause any issue here as I have set the TTL to 0 in step 5.
I have used  [jakejarvis, s3-sync-action](https://github.com/jakejarvis/s3-sync-action) code to sync to the S3 bucket.
The GitHub Action template snippet to achieve this:
```
deploy-site:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: jakejarvis/s3-sync-action@master
      with:
        args: --delete
      env:
        AWS_S3_BUCKET: cloud-cv-website
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        SOURCE_DIR: CV
```

#### 16. Blog post
The final step is to write a blog about the experience and what I have learned during the project work. You are reading this blog right now.

#### 16 + 1. What is next?
I am planning to redo this challenge using Terraform.