AWS Lambda User Guide

Introduction

AWS Lambda is a leading "Serverless" computing service from Amazon Web Services (AWS). It allows you to run your code without having to manage or provision any servers.

Simply put, instead of renting a virtual computer (like EC2), installing an OS, and maintaining it, you just upload your code. Lambda handles everything else—from activating resources and executing the code to shutting everything down once the job is done.

Key Advantages of AWS Lambda

  • No Server Management (Serverless): You don't need to worry about OS updates, security patches, or hardware maintenance. AWS handles all the heavy lifting of infrastructure operations.
  • Auto-scaling: Lambda reacts instantly to the number of requests. If there is 1 request, it runs once; if there are 10,000 simultaneous requests, it automatically scales up to handle them in parallel without any extra configuration.
  • Cost Optimization (Pay-as-you-go): This is the best part. You only pay for the time your code is actually running (measured in milliseconds). If the code isn't running, you don't pay a cent. This is a huge shift from traditional servers where you pay even when the server is just sitting idle.
  • Event-driven Architecture: Lambda is extremely flexible. It can be triggered by various AWS services, such as uploading a photo to S3, adding new data to DynamoDB, or through a user's API request.
  • Multi-language Support: You can write code in popular languages like Python, Node.js, Java, Go, C#, Ruby, and PowerShell.

In short: AWS Lambda is the perfect solution for developers who want to focus entirely on their product logic while keeping operational costs to a minimum.


Real-world Example: On-the-fly Image Resizing

In this guide, I will show you how to use Lambda combined with S3 and CloudFront to resize images instantly when accessing an image URL. This is very useful when you upload high-resolution images but need to display smaller thumbnails to save bandwidth and improve performance.


Prerequisites

You should have a basic understanding of S3 Buckets and CloudFront. Ensure the AWS profile you are using has sufficient permissions for these services.


Detailed Implementation

First, create a file named resize-lambda.ts:

import { APIGatewayProxyHandler } from "aws-lambda"
import * as AWS from "aws-sdk"
import { Jimp } from "jimp"
import * as path from "path"
import * as mime from "mime-types"

const s3 = new AWS.S3()
const BUCKET_NAME = process.env.BUCKET_NAME!
const CF_DOMAIN = process.env.CF_DOMAIN!

export const handler: APIGatewayProxyHandler = async (event) => {
  const pathName = event.path
  const parts = pathName.split("/")

  const dimension = parts[1]
  const fullKey = parts.slice(2).join("/")
  const ext = path.extname(fullKey).toLowerCase()
  const ContentType = mime.lookup(ext) || "image/jpeg"

  const [widthStr, heightStr] = dimension.split("x")
  const width = parseInt(widthStr)
  const height = parseInt(heightStr)

  try {
    const originalImage = await s3
      .getObject({ Bucket: BUCKET_NAME, Key: fullKey })
      .promise()
    const image = await Jimp.read(originalImage.Body as Buffer)

    if (width < image.width || height < image.height) {
      image.scaleToFit({ w: width, h: height })
    }
    const resizedBuffer = await image.getBuffer(ContentType as any)

    await s3
      .putObject({
        Bucket: BUCKET_NAME,
        Key: `${dimension}/${fullKey}`,
        Body: resizedBuffer,
        ContentType,
        CacheControl: "max-age=31536000",
      })
      .promise()

    const redirectUrl = `https://${CF_DOMAIN}/${dimension}/${fullKey}`
    return {
      statusCode: 302,
      headers: {
        Location: redirectUrl,
        "Cache-Control": "no-cache",
      },
      body: "",
    }
  } catch (error) {
    console.error("Error:", error)
    return {
      statusCode: 404,
      body: JSON.stringify({
        message: "Image not found or processing error",
        error: (error as Error).message,
      }),
    }
  }
}

Explanation:

  • This code triggers when a user visits a URL like: {cloudfront}/{width}x{height}/{image-name}
  • s3.putObject: Saves the resized image back to the bucket with a cache duration of 1 year.
  • Redirect: Finally, it redirects the user to the newly created CloudFront image link.


Next, create the infrastructure stack in lib/image-resize-stack.ts:

import * as cdk from "aws-cdk-lib"
import * as s3 from "aws-cdk-lib/aws-s3"
import * as cloudfront from "aws-cdk-lib/aws-cloudfront"
import * as origins from "aws-cdk-lib/aws-cloudfront-origins"
import * as lambda from "aws-cdk-lib/aws-lambda"
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs"
import * as apigw from "aws-cdk-lib/aws-apigateway"
import * as iam from "aws-cdk-lib/aws-iam"
import { Construct } from "constructs"
import * as path from "path"

export class ImageResizerStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const CUSTOM_HEADER_NAME = "Referer"
    const CUSTOM_HEADER_VALUE = "<CUSTOM_HEADER_VALUE>"

    const imageBucket = new s3.Bucket(this, "ImageBucket", {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      blockPublicAccess: new s3.BlockPublicAccess({
        blockPublicAcls: true,
        ignorePublicAcls: true,
        blockPublicPolicy: false,
        restrictPublicBuckets: false,
      }),
      encryption: s3.BucketEncryption.S3_MANAGED,
    })

    const distribution = new cloudfront.Distribution(this, "ImageDist", {
      defaultBehavior: {
        origin: new origins.HttpOrigin(imageBucket.bucketWebsiteDomainName, {
          protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
          customHeaders: {
            [CUSTOM_HEADER_NAME]: CUSTOM_HEADER_VALUE,
          },
        }),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
      },
    })

    const resizerLambda = new nodejs.NodejsFunction(this, "ResizeHandler", {
      runtime: lambda.Runtime.NODEJS_LATEST,
      entry: path.join(__dirname, "resize-lambda.ts"),
      environment: {
        BUCKET_NAME: imageBucket.bucketName,
        CF_DOMAIN: distribution.domainName,
      },
      bundling: { minify: true },
      timeout: cdk.Duration.seconds(30),
      memorySize: 512,
    })

    imageBucket.grantReadWrite(resizerLambda)

    const api = new apigw.LambdaRestApi(this, "ResizerApi", {
      handler: resizerLambda,
      proxy: true,
    })

    const apiHostName = `${api.restApiId}.execute-api.${this.region}.${this.urlSuffix}`

    const cfnBucket = imageBucket.node.defaultChild as s3.CfnBucket
    cfnBucket.websiteConfiguration = {
      indexDocument: "index.html",
      routingRules: [
        {
          routingRuleCondition: { httpErrorCodeReturnedEquals: "403" },
          redirectRule: {
            hostName: apiHostName,
            httpRedirectCode: "307",
            protocol: "https",
            replaceKeyPrefixWith: "prod/",
          },
        },
        {
          routingRuleCondition: { httpErrorCodeReturnedEquals: "404" },
          redirectRule: {
            hostName: apiHostName,
            httpRedirectCode: "307",
            protocol: "https",
            replaceKeyPrefixWith: "prod/",
          },
        },
      ],
    }

    imageBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        sid: "AllowCloudFrontWithSecret",
        effect: iam.Effect.ALLOW,
        principals: [new iam.AnyPrincipal()],
        actions: ["s3:GetObject"],
        resources: [imageBucket.arnForObjects("*")],
        conditions: {
          StringEquals: {
            "aws:Referer": CUSTOM_HEADER_VALUE,
          },
        },
      }),
    )

    new cdk.CfnOutput(this, "InputBucketName", {
      value: imageBucket.bucketName,
      description: "Bucket to upload image",
    })
    new cdk.CfnOutput(this, "CloudFrontURL", {
      value: distribution.domainName,
    })
    new cdk.CfnOutput(this, "ApiEndpoint", { value: api.url })
  }
}

Explanation:

  • imageBucket: This creates the bucket. You’ll notice that public access isn't completely blocked like in my previous S3 and CloudFront tutorials. This is because we can't use an internal connection (S3 REST API Endpoint) here. Instead, we use the S3 Website Endpoint (HTTP) so that when someone tries to access an image that isn't there yet, S3 can redirect them to Lambda to create it, rather than just throwing a 403 or 404 error.
  • distribution: This creates the CloudFront setup. I’ve added CUSTOM_HEADER_NAME and CUSTOM_HEADER_VALUE as a security measure. Since the bucket isn't fully public, I use this "secret header" for communication between CloudFront and the bucket. Only requests with this specific header can access the files, which prevents random users from bypassing CloudFront to access your bucket directly.
  • resizerLambda: This is the Lambda function itself. Note that I've passed BUCKET_NAME and CF_DOMAIN into the environment variables so the code in resize-lambda.ts knows which bucket to use and where to redirect.
  • LambdaRestApi: This is the API Gateway that triggers the Lambda code to resize the image if the bucket doesn't have it.
  • websiteConfiguration: This is the bucket setting that handles the magic—it tells S3 to redirect to our Lambda API whenever an image isn't found.


Step-by-Step Workflow

  1. Upload the original image to the S3 Bucket.
  2. Access the image via the CloudFront URL. If CloudFront has it cached, it serves it immediately. If not, it checks the Bucket.
  3. Check Bucket: If the Bucket has the image (the specific size requested), it returns it to CloudFront. If it’s missing, S3 redirects the request to API Gateway to run the Lambda code.
  4. Process & Redirect: The Lambda code creates the resized image based on your URL parameters and saves it back to the Bucket. Finally, it redirects you back to the CloudFront link to view the newly created image.


Update your bin/aws-cdk.ts:

#!/usr/bin/env node
import * as cdk from "aws-cdk-lib/core";
import { ImageResizerStack } from "../lib/image-resize/image-resize-stack";

const app = new cdk.App();
new ImageResizerStack(app, "ImageResizerStack");


Successful Deployment Results

 ImageResizerStack
 Deployment time: 153.15s

Outputs:
ImageResizerStack.ApiEndpoint = https://kldalwpp0g.execute-api.ap-southeast-1.amazonaws.com/prod/
ImageResizerStack.CloudFrontURL = dingdvx97wac4.cloudfront.net
ImageResizerStack.InputBucketName = imageresizerstack-imagebucket97210811-0qkuup1zbr8v
ImageResizerStack.ResizerApiEndpointD1F38764 = https://kldalwpp0g.execute-api.ap-southeast-1.amazonaws.com/prod/

 Total time: 176.79s

Use the values provided, such as CloudFrontURL and InputBucketName, in your NestJS project.


Next, here is the code for the NestJS s3.service.ts file:

import {
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'
import {fromIni} from '@aws-sdk/credential-providers'
import {getSignedUrl} from '@aws-sdk/s3-request-presigner'
import {Injectable} from '@nestjs/common'
import {ConfigService} from '@nestjs/config'
import * as mime from 'mime-types'

@Injectable()
export class S3Service {
  private s3Client: S3Client
  private bucket: string
  private cfDomain: string

  constructor(private configService: ConfigService) {
    const region = this.configService.get<string>('REGION') || ''
    const profile = this.configService.get<string>('PROFILE') || ''
    this.bucket = this.configService.get<string>('BUCKET') || ''
    this.cfDomain = this.configService.get<string>('CLOUDFRONT') || ''
    this.s3Client = new S3Client({
      region,
      credentials: fromIni({profile}),
    })
  }

  async createPresignedUrl(fileName: string) {
    const ContentType = mime.lookup(fileName) || 'application/octet-stream'
    const command = new PutObjectCommand({
      Bucket: this.bucket,
      Key: fileName,
      ContentType,
    })
    const url = await getSignedUrl(this.s3Client, command, {expiresIn: 3600})
    return {url}
  }

  async listFiles() {
    const command = new ListObjectsV2Command({
      Bucket: this.bucket,
    })
    const {Contents} = await this.s3Client.send(command)
    if (!Contents) return {originals: [], resized: []}

    const images: any = {
      originals: [],
      resized: [],
    }
    Contents.forEach(object => {
      const key = object.Key
      const url = `${this.cfDomain}/${key}`
      const isResized = /^(\d+x\d+)\//.test(key || '')
      if (isResized) {
        images.resized.push({
          key,
          url,
          size: key?.split('/')[0],
        })
      } else {
        images.originals.push({
          key,
          url,
        })
      }
    })
    return images
  }
}

Explanation:

  • createPresignedUrl: I am still using a pre-signed URL to upload images. The mime.lookup(fileName) part is used to get the correct content type for the AWS file based on its file extension.
  • listFiles: This is used to retrieve both the original images and the resized ones. The resized images will have a file key that includes the dimensions (for example: 300x300).


After that, try uploading an image and accessing the resize URL to check the results.





The images uploaded to the Bucket will look like this:


Happy coding!

See more articles here.

Comments

Popular posts from this blog

All practice series

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Kubernetes Deployment for Zero Downtime

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

NodeJS Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine