Building a Serverless Application Using Terraform

In today’s digital landscape, leveraging serverless architectures for application deployment has become increasingly popular due to its scalability, cost-effectiveness, and ease of maintenance.
Among the myriad of possibilities, one standout application is the creation of a QR Code Generator API. In this blog post, we’ll walk through building and deploying such an API using AWS Lambda and Terraform.

Introduction to Serverless Computing

Serverless computing allows developers to focus solely on writing code without the need to manage server infrastructure. AWS Lambda is a leading serverless computing platform that enables the execution of code in response to events without provisioning or managing servers.

Steps:

Step 1: Create a Serverless function.
Step 2: Write the code for your function. Your function should handle incoming HTTP requests and return a response.
Step 3: Deploy your function to the cloud.
Step 4: Test your API by sending HTTP requests to the API Gateway resource.
Step 5: Write IaC for your Serverless Function so that you can use code to deploy it. You can use any one of the following: Terraform
Step 6: HTML File for Simple FrontEnd
Step 7: Set up a CICD pipeline to deploy your Function
Step 8: Set up a GitHub repository where you will push your serverless function code and IaC code.
Step 9: Implement CI/CD Pipeline using Jenkins

GitHub Repository:
https://github.com/HARSHALJETHWA19/qr-code.git

Step 1: Create a Serverless Function

The first step is to create a Lambda function.
We’ll write code that processes incoming HTTP requests and generates QR Codes based on the provided URLs. Using the AWS Management Console or CLI, we can easily create a Lambda function and define its runtime environment (such as Python, Node.js, etc.).

Step 2: Write the Function Code

Next, we’ll write the code for our Lambda function. The function should handle incoming POST requests, extract the URL from the request body, generate a QR Code for the URL, save it to an S3 bucket, and return the S3 object URL in the response. T
his can be achieved using libraries such as boto3 for AWS integration and qrcode for QR Code generation.

lambda_function.py

import json
import boto3
import qrcode
import io
import base64

# Initialize a session using Amazon S3
s3 = boto3.client('s3')
def lambda_handler(event, context):
    # Parse the URL from the event
    body = json.loads(event['body'])
    url = body['url']

    # Generate QR code
    img = qrcode.make(url)
    img_bytes = io.BytesIO()
    img.save(img_bytes)
    img_bytes = img_bytes.getvalue()

    # Generate a unique filename
    filename = url.split("://")[1].replace("/", "_") + '.png'

    # Upload the QR code to the S3 bucket
    s3.put_object(Bucket='S3-BUCKET-NAME', Key=filename, Body=img_bytes, ContentType='image/png', ACL='public-read')

    # Generate the URL of the uploaded QR code
    location = s3.get_bucket_location(Bucket='S3-BUCKET-NAME')['LocationConstraint']
    region = '' if location is None else f'{location}'
    qr_code_url = f"https://{'S3-BUCKET-NAME'}.s3.amazonaws.com/{filename}"

    # Construct response with CORS headers
    response = {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Headers" : "Content-Type",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "OPTIONS,POST,GET"
        },
        'body': json.dumps(qr_code_url)
    }

    return response

Replace S3-BUCKET-NAME with your s3 bucket name.
To install dependency execute the below command

pip install qrcode

Step 3: Deploy the Function

Once the function code is ready, we deploy it to AWS Lambda. This can be done manually via the AWS Management Console or automated using infrastructure as code (IaC) tools like Terraform.

Step 4: Test the API

With the Lambda deployed, we can test it by sending HTTP POST requests containing URLs. Upon successful invocation, the API should return the URL of the generated QR Code stored in the S3 bucket.

To Test the Lambda function Goto -> Test -> Configure Event and add below to the Event Json

{
  "body": "{\"url\": \"https://example.com\"}"
}

Step 5: Implement Infrastructure as Code (IaC) using Terraform

To ensure consistency and repeatability in our deployments, we use Terraform to define our serverless infrastructure as code. This includes provisioning Lambda functions, API Gateway resources, and necessary IAM roles.

main.tf

provider "aws" {
  region = "us-east-1" # Change this to your desired region
}

# S3 bucket
resource "aws_s3_bucket" "my_bucket" {
  bucket = "qr-code-generator7321"
#   acl    = "public-read"
}

# Lambda function
resource "aws_lambda_function" "my_lambda" {
  function_name    = "qr-code-generator7321"
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.12"
  memory_size      = 128
  timeout          = 10
  source_code_hash = filebase64sha256("${path.module}/lambda_function.zip")
  s3_bucket        = aws_s3_bucket.my_bucket.id
  s3_key           = aws_s3_bucket_object.lambda_zip.key

  # Use an existing IAM role
  role = "arn:aws:iam::871740193993:role/service-role/trigger" # Change this to your existing IAM role ARN

  environment {
    variables = {
      key = "value"
    }
  }
}

# Upload ZIP file to S3
resource "aws_s3_bucket_object" "lambda_zip" {
  bucket       = aws_s3_bucket.my_bucket.id
  key          = "lambda_function.zip"
  source       = "${path.module}/lambda_function.zip"
  etag         = filemd5("${path.module}/lambda_function.zip")
  content_type = "application/zip"
#   acl          = "public-read"
}

resource "aws_api_gateway_rest_api" "api" {
  name        = "QRCodeAPI"
  description = "API for QR Code Generation"
}

resource "aws_api_gateway_resource" "qr_code_resource" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  path_part   = "qr-code"
}

resource "aws_api_gateway_method" "post_method" {
  rest_api_id   = aws_api_gateway_rest_api.api.id
  resource_id   = aws_api_gateway_resource.qr_code_resource.id
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda_integration" {
  rest_api_id             = aws_api_gateway_rest_api.api.id
  resource_id             = aws_api_gateway_resource.qr_code_resource.id
  http_method             = aws_api_gateway_method.post_method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.my_lambda.invoke_arn
}

resource "aws_lambda_permission" "apigateway_invoke" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.my_lambda.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "arn:aws:execute-api:us-east-1:871740193993:${aws_api_gateway_rest_api.api.id}/*/*"
}

resource "aws_api_gateway_method_response" "response_200" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  resource_id = aws_api_gateway_resource.qr_code_resource.id
  http_method = aws_api_gateway_method.post_method.http_method
  status_code = "200"

  response_models = {
    "application/json" = "Empty"
  }
  response_parameters = {
    "method.response.header.Access-Control-Allow-Origin" = true
  }
}

resource "aws_api_gateway_integration_response" "response_200" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  resource_id = aws_api_gateway_resource.qr_code_resource.id
  http_method = aws_api_gateway_method.post_method.http_method
  status_code = aws_api_gateway_method_response.response_200.status_code

  response_templates = {
    "application/json" = "#set($origin = $input.params().header.get('Origin'))\n#if($origin)\n  #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end\n{}"
  }
  response_parameters = {
    "method.response.header.Access-Control-Allow-Origin" = "'*'"
  }
  depends_on = [aws_api_gateway_integration.lambda_integration]
}



resource "aws_api_gateway_deployment" "deployment" {
  depends_on = [aws_api_gateway_integration.lambda_integration]

  rest_api_id = aws_api_gateway_rest_api.api.id
  stage_name  = "prod"
}

output "api_url" {
  value = aws_api_gateway_deployment.deployment.invoke_url
}

Step 6: HTML File for Simple FrontEnd

In this step, we’ll develop a basic HTML file to serve as a front-end interface for our QR Code Generator API. The HTML file will include a form where users can input a URL, and upon submission, it will make a POST request to our API endpoint. We’ll use HTML, along with some minimal JavaScript, to handle the form submission and display the generated QR Code to the user.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QR Code Generator</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f0f0f0;
        }

        #container {
            text-align: center;
        }

        #urlInput {
            width: 300px;
            padding: 10px;
            margin-bottom: 10px;
        }

        #generateBtn {
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
            font-size: 16px;
        }

        #qrCodeImage {
            margin-top: 20px;
            max-width: 300px;
        }
    </style>
</head>
<body>
    <div id="container">
        <input type="text" id="urlInput" placeholder="Enter URL">
        <button id="generateBtn">Generate QR Code</button>
        <div id="qrCodeContainer">
            <img id="qrCodeImage" src="" alt="QR Code">
        </div>
    </div>

    <script>
        document.getElementById('generateBtn').addEventListener('click', function() {
            var url = document.getElementById('urlInput').value.trim();
            if (url === '') {
                alert('Please enter a valid URL');
                return;
            }
            generateQRCode(url);
        });

        function generateQRCode(url) {
            fetch('YOUR_API_GATEWAY_ENDPOINT', {
                method: 'POST',
                body: JSON.stringify({ url: url }),
                headers: {
                    'Content-Type': 'application/json'
                }
            })
            .then(response => response.json())
            .then(data => {
                var qrCodeUrl = data.body;
                document.getElementById('qrCodeImage').src = qrCodeUrl;
                document.getElementById('qrCodeContainer').style.display = 'block';
            })
            .catch(error => {
                console.error('Error:', error);
            });
        }
    </script>
</body>
</html>

Step 7: Write CI/CD pipeline Code using Jenkins

In this step, we’ll leverage Jenkins to set up a Continuous Integration/Continuous Deployment (CI/CD) pipeline for our serverless application. Jenkins allows us to automate the build, test, and deployment processes, ensuring efficiency and reliability in our development workflow.

Jenkinsfile

pipeline {
    agent any

    environment {
        // Define AWS credentials for S3 bucket access
        AWS_ACCESS_KEY_ID = credentials('awscreds')
        AWS_SECRET_ACCESS_KEY = credentials('awscreds')
        GITLAB_CRED_ID = 'your-gitlab-credentials'

        // Define S3 bucket name and Terraform workspace name
        // S3_BUCKET = 'your-s3-bucket-name'
        // TF_WORKSPACE = 'your-terraform-workspace'
    }

    stages {
        stage('Checkout') {
            steps {
                // Checkout your code from the repository
                script {
                    // Set global git configuration to disable SSL verification
                    sh 'git config --global http.sslVerify false'

                    // Checkout code from GitLab repository using credentials
                    checkout([$class: 'GitSCM', branches: [[name: 'main']], userRemoteConfigs: [[url: 'https://gitlab.cdsys.local/jenkins-training/cicd.git', credentialsId: env.GITLAB_CRED_ID]]])
                }

            }
        }



        stage('Terraform init') {
            steps {
                script {

                    // Plan and apply Terraform changes
                    sh 'terraform init'

                }
            }
        }


        stage('Terraform plan') {
            steps {
                script {

                    // Plan and apply Terraform changes
                    sh 'terraform plan -out=tfplan'

                }
            }
        }

        stage('Terraform INIT') {
            steps {
                script {
                    sh 'terraform apply -auto-approve tfplan | tee terraform_apply_output.txt'
                    // sh 'api_url=$(grep -oP "(?<=api_url = ).*" terraform_apply_output.txt)'
                    // sh 'sed -i "s|API_GATEWAY_ENDPOINT|$api_url|g" index.html'
                }
            }
        }



        stage('Serve HTML') {
            steps {
                // Serve HTML file using Python's SimpleHTTPServer
                sh 'nohup python -m SimpleHTTPServer 5050 > /dev/null 2>&1 &'
            }
        }

Replace awscreds, your-gitlab-credentials with your GitHub and AWS credentials.

Step 8: Set up GitHub Repository

For version control and collaboration, we create a GitHub repository where we store our function code and Terraform configurations.
Add all the code and do the below steps to commit and add to VCS.

git status
git add .
git commit -m "first"
git push origin main

Step 9: Implement CI/CD Pipeline using Jenkins

Lastly, we set up a Continuous Integration/Continuous Deployment (CI/CD) pipeline using services like Jenkins. This automates the process of building, testing, and deploying our serverless application whenever changes are pushed to the GitHub repository.

NOTE: Add awscreds and your-gitlab-credentials to your Jenkins. Credentials

The Created Resources after the Pipeline successfully gets executed:

The Front-End of our Serverless Application:

GitHub Repository:
https://github.com/HARSHALJETHWA19/qr-code.git

In conclusion, building a QR Code Generator API using AWS Lambda and Terraform showcases the power and flexibility of serverless computing. By following the steps outlined above, developers can easily create and deploy scalable, cost-effective APIs with minimal infrastructure management overhead.

Follow me :

Linkedin: linkedin.com/in/harshaljethwa

GitHub: github.com/HARSHALJETHWA19

Twitter: twitter.com/harshaljethwaa

Medium: https://medium.com/@harshaljethwa19

Thank You!!!