DEV Community

Cover image for Cloud Resume Challenge: Hosting a React CV using S3, CloudFront, Route 53, Lambda, DynamoDB and GitHub actions for CI/CD
J. Alexander
J. Alexander

Posted on

Cloud Resume Challenge: Hosting a React CV using S3, CloudFront, Route 53, Lambda, DynamoDB and GitHub actions for CI/CD

Introduction

I will be attempting to complete the Cloud Resume Challenge. By doing the Cloud Resume challenge, I was inspired to take and pass the AWS Cloud Practitioner certification. Shout out to Forrest Brazeal for coming up with such a fantastic project and book for those looking to enter the cloud IT field.
Through this, I'll walk you through the process I used to deploy my React CV resume website using AWS services and GitHub Actions for continuous integration and deployment (CI/CD). I will cover the setup of AWS Organizations, AWS S3, CloudFront, Route 53,Lambda, DynamoDB, GitHub Actions and Terraform.
Since completing the Cloud Resume challenge, I have passed the AWS Cloud Practitioner and AWS Certified Solutions Architect exams.

Step 1: Setting Up Your AWS Account, Organization, IAM

Overview of AWS Accounts and IAM

AWS accounts are typically managed through AWS Organizations. AWS (Amazon Web Services) provides a secure and scalable infrastructure for deploying web applications. An AWS account is necessary to access and manage AWS services. AWS Identity and Access Management (IAM) enables you to manage access to AWS services and resources securely.

First, sign up for an AWS account if you don't have one. After logging into the root user account, navigate to the IAM console. Enable IAM from the console and create a new IAM user by setting up AWS Organizations in the root account. Create 2 Organizational Units (OUs) to hold the production and test account. ![Screenshot of AWS IAM console showing the creation of a new IAM user]Next, choose the Identity Source and create a new user in the Identity Source, entering your username and credentials to receive a one-time password for signing into the AWS access portal. Copy your sign-in credentials into notepad. Then, create an Admin Permission Set using the predefined AdministratorAccess. Set up account access for the Identity Center Admin User by going to AWS accounts, giving administrative access to your Organizational Units, and adding the user created in step 3 with Administrator Access to the user in the Test account of the Organization. Finally, sign into the AWS Portal using your username and one-time password, then set the new password. You can also perform command line or programmatic access using short-term access keys. Screenshot of the AWS sign-in portal

Step 2: Create the React CV website and deploy it to GitHub

I purchased a custom domain name from Namecheap and transferred the name server to Route 53. First, I created an S3 bucket named to match my custom domain, using the subdomain WWW. To host my website in the S3 bucket, I disabled Block all public access. After building the React project with npm run build, I uploaded the static files to the bucket via the AWS Console. I enabled static website hosting and set index.html as the index document, then updated the bucket policy to allow public read access by adding a policy for PublicReadGetObject.

For setting up a custom domain, I created a CNAME record in my domain hosting provider, pointing to the S3 bucket endpoint. Next, I deployed the React app to CloudFront by creating a new distribution, using the S3 bucket endpoint as the origin, enabling automatic object compression, and setting up custom domain names with HTTPS.

To use the root domain, I transferred the domain to Route 53, created a Route 53 public-hosted zone, updated the DNS name servers, and created DNS records for both the www and root domains using Aliases.

Finally, I set up a CI/CD pipeline using GitHub Actions. This automated the deployment process by installing dependencies, running unit tests, uploading files to S3, and invalidating the CloudFront cache. I created an IAM user with a policy granting the necessary permissions, added AWS credentials to GitHub secrets, and configured a GitHub Actions workflow to deploy the app whenever changes are pushed to the master branch.

name: Build and Deploy React App to CloudFront
on:
  push:
    branches: [ master ]
jobs:
  build-and-deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    env:
      BUCKET: <bucket-name>
      DIST: build
      REGION: us-east-1
      DIST_ID: <ID>

    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Configure AWS Credentials
      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: ${{ env.REGION }}

    # - uses: actions/setup-node@v2
    #   with:
    #     node-version: '14'

    - name: Install Dependencies
      run: |
        node --version
        npm ci --production

    - name: Build Static Website
      run: npm run build

    - name: Copy files to the production website with the AWS CLI
      run: |
        aws s3 sync --delete ${{ env.DIST }} s3://${{ env.BUCKET }}

    - name: Copy files to the production website with the AWS CLI
      run: |
        aws cloudfront create-invalidation \
          --distribution-id ${{ env.DIST_ID }} \
          --paths "/*"
Enter fullscreen mode Exit fullscreen mode

Connecting RetroHitCounter to DynamoDB Using AWS Lambda and API Gateway

To connect the RetroHitCounter to a DynamoDB database using an AWS Lambda function and API Gateway, start by setting up a DynamoDB table named 'HitCounter' with a primary key of type String. Then, create a Lambda function in Python to interact with the DynamoDB table, incrementing the hit count each time it is invoked. Ensure the Lambda function has the necessary IAM role permissions to read and write to the DynamoDB table. Next, create an API Gateway, setting up a new REST API with a resource (e.g., /hits) and a GET method that triggers the Lambda function. Enable CORS for the GET method to allow cross-origin requests. Finally, update your React application to fetch the hit counter value from the API Gateway endpoint and display it using RetroHitCounter. This involves fetching the hit count in a React component's useEffect hook and integrating the RetroHitCounter component to show the hit count.

Code Snippets

  1. Lambda Function in Python:

    import json
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('HitCounter')
    
    def lambda_handler(event, context):
        response = table.get_item(Key={'id': 'counterId'})
    
        if 'Item' in response:
            hits = response['Item']['hits']
        else:
            hits = 0
    
        hits += 1
    
        table.put_item(
            Item={
                'id': 'counterId',
                'hits': hits
            }
        )
    
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*' 
            },
            'body': json.dumps({'hits': hits})
        }
    
  2. IAM Role Policy:

    {
        "Effect": "Allow",
        "Action": [
            "dynamodb:GetItem",
            "dynamodb:PutItem"
        ],
        "Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/HitCounter"
    }
    
  3. React Component:

    import React, { useState, useEffect } from 'react';
    import RetroHitCounter from 'react-retro-hit-counter';
    import jelani from '../assets/img/jelaniprofilepic.jpeg';
    import { Link, animateScroll as scroll } from 'react-scroll';
    import { Navbar, Nav } from 'react-bootstrap';
    import { Container } from 'react-bootstrap';
    
    const Navigate = (props) => {
        const [hits, setHits] = useState(0);
    
        useEffect(() => {
            fetch('https://YOUR_API_GATEWAY_URL/hits')
                .then(response => response.json())
                .then(data => setHits(data.hits))
                .catch(error => console.error('Error fetching hits:', error));
        }, []);
    
        const titles = props.titles.map((title, index) => (
            <Container style={{ paddingRight: 0, marginRight: 0 }} className="nav-item justify-content-end" key={index}>
                <Nav.Link
                    as={Link}
                    eventKey={index}
                    activeClass="active"
                    style={{ cursor: "pointer" }}
                    className="nav-link"
                    spy={true}
                    to={props.hrefs[index]}
                    smooth={"easeInOutQuart"}
                    duration={1500}
                >
                    {title}
                </Nav.Link>
            </Container>
        ));
    
        return (
            <Navbar className="navbar navbar-expand-lg navbar-dark bg-primary fixed-top" id="sideNav" collapseOnSelect expand="lg" bg="dark" variant="dark">
                <button className="navbar-brand" onClick={() => scroll.scrollToTop()} style={{ background: 'none', border: 'none', padding: 0 }}>
                    <span className="d-none d-lg-block">
                        <img className="img-fluid img-profile rounded-circle mx-auto mb-2" style={{ cursor: "pointer" }} src={jelani} alt="" />
                    </span>
                </button>
                <Navbar.Toggle aria-controls="responsive-navbar-nav" />
                <Navbar.Collapse id="responsive-navbar-nav">
                    <Nav className="mr-auto">
                        {titles}
                        <RetroHitCounter
                            hits={hits}
                            withBorder={true}
                            withGlow={false}
                            minLength={4}
                            size={40}
                            padding={4}
                            digitSpacing={3}
                            segmentThickness={4}
                            segmentSpacing={0.5}
                            segmentActiveColor="#76FF03"
                            segmentInactiveColor="#315324"
                            backgroundColor="#222222"
                            borderThickness={7}
                            glowStrength={0.5}
                        />
                    </Nav>
                </Navbar.Collapse>
            </Navbar>
        );
    }
    
    export default Navigate;
    

Make sure to replace https://YOUR_API_GATEWAY_URL/hits with your actual API Gateway endpoint URL. This setup will fetch the hit counter value from DynamoDB through the Lambda function and display it using RetroHitCounter.

Step 3: Setting Up AWS Lambda Function with Terraform

Now, let's dive into deploying your backend Lambda function using Terraform. This approach allows you to define your infrastructure as code, ensuring consistency and reproducibility across environments.

Terraform Code for AWS Lambda

Create a new Terraform file, let's call it lambda.tf, and include the following code to define your Lambda function:

# lambda.tf

# Define the AWS Lambda function

resource "aws_lambda_function" "hitcounter" {
  filename         = "${path.module}/lambda/func.zip"
  function_name    = "myfunction"
  role             = aws_iam_role.lambda_exec_role.arn
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.12"
  memory_size      = 128
  timeout          = 3

  environment {
    variables = {
      ENV_VAR_KEY = "value"
    }
  }

  tracing_config {
    mode = "Active"
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Define IAM role for Lambda execution
resource "aws_iam_role" "lambda_exec_role" {
  name               = "lambda_exec_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# Define IAM policy for Lambda function permissions
resource "aws_iam_policy" "lambda_policy" {
  name        = "lambda_policy"
  description = "Policy for Lambda function"

  policy      = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = "logs:*"
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = [
          "dynamodb:GetItem",
          "dynamodb:PutItem"
        ]
        Resource = "arn:aws:dynamodb:us-west-2:123456789012:table/HitCounter"
      },
      {
        Effect   = "Allow"
        Action   = "lambda:*"
        Resource = "*"
      }
    ]
  })
}

# Attach IAM policy to Lambda execution role
resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
  role       = aws_iam_role.lambda_exec_role.name
  policy_arn = aws_iam_policy.lambda_policy.arn
}

# Archive Lambda function code
data "archive_file" "func" {
  type        = "zip"
  source_dir  = "${path.module}/lambda/"  # Replace with the directory containing your Lambda function code
  output_path = "${path.module}/lambda/func.zip"
}
Enter fullscreen mode Exit fullscreen mode

Deploying with Terraform

  • *Make sure Terraform is installed and configured with AWS credentials.
  • *Place your Lambda function code in the lambda/ directory relative to your Terraform configuration.
  • *Initialize Terraform (terraform init) and apply the configuration (terraform apply) to deploy your Lambda function.

Step 4: Setting Up CI/CD with GitHub Actions
To automate deployment and ensure continuous integration with your backend Lambda function, integrate GitHub Actions with your repository.

GitHub Actions Workflow
Update your existing GitHub Actions workflow (from Step 2) to include deployment steps for your Lambda function. Here's an example snippet to add to your workflow file:

# .github/workflows/main.yml

name: Build and Deploy Backend Lambda

on:
  push:
    branches:
      - master

jobs:
  build-and-deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    env:
      BUCKET: <bucket-name>
      DIST: build
      REGION: us-east-1
      DIST_ID: <CloudFront Distribution ID>
      LAMBDA_FUNCTION_NAME: "myfunction"

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS Credentials
        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: ${{ env.REGION }}

      - name: Install Dependencies
        run: |
          npm ci --production

      - name: Build Lambda Function ZIP
        run: |
          cd lambda/
          zip -r func.zip .

      - name: Deploy Lambda Function with Terraform
        run: |
          terraform init
          terraform apply -auto-approve

      - name: Invalidate CloudFront Cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ env.DIST_ID }} \
            --paths "/*"
Enter fullscreen mode Exit fullscreen mode

Conclusion
By using Terraform for infrastructure as code and GitHub Actions for CI/CD, you've automated the deployment of your backend Lambda function, ensuring efficient development and deployment workflows for your Cloud Resume Challenge project.

Top comments (0)