How to Deploy a Typescript App on AWS Lambda Using AWS-CLI for Beginners

How to Deploy a Typescript App on AWS Lambda Using AWS-CLI for Beginners

Serverless computing lets you create and launch apps and services without worrying about the underlying infrastructure. It's especially handy for building APIs and backend servers, allowing you to concentrate more on your code and less on the operational details.

Creating a basic node.js app in Typescript

Creating a basic TypeScript Node.js app for AWS Lambda functions involves several steps.

Step 1: Set Up Your Project Folder

Create a new directory for your project and navigate into it:

mkdir my-node-js-app
cd my-node-js-app

Step 2: Initialize npm

In your project directory, initialize a new Node.js project by creating a package.json file. This file manages project dependencies and scripts.

npm init -y

Step 3: Install Dependencies

You need to install TypeScript, along with type definitions for Node.js and AWS Lambda:

npm install --save-dev typescript @types/node @types/aws-lambda

Step 4: Create tsconfig.json

Create a TypeScript configuration file called tsconfig.json in your project root. This file specifies the root files and the compiler options required to compile the project.

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "CommonJS",
        "outDir": "./build",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}

This configuration does the following:

  • Compiles the TypeScript files from ES2020 to CommonJS modules.

  • Outputs the compiled JavaScript files into the build directory.

  • Only includes files from the src directory.

Step 5: Configure npm Scripts

Modify your package.json to include a build script:

"scripts": {
    "build": "tsc"
}

This script runs the TypeScript compiler (tsc) which reads the configuration from tsconfig.json.

Step 6: Create Source Files

Create a src folder, and within it, create an index.ts file:

mkdir src
touch src/index.ts

Add the following AWS Lambda handler function to your index.ts:

import { Context, APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';

export const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
    console.log(`Event: ${JSON.stringify(event, null, 2)}`);
    console.log(`Context: ${JSON.stringify(context, null, 2)}`);
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'hello world',
        }),
    };
};

Step 7: Build the Project

Now, you can build your TypeScript project by running:

npm run build

This command turns your TypeScript code into JavaScript, storing it in the build folder based on the settings in your tsconfig.json.

Now that we've got our basic TypeScript app ready, let's look at how to prepare the app for deployment to AWS Lambda.

To deploy the function, we need to make a deployment package, which could be a zip file or a container image. For this guide, we'll focus on the container image because it's more versatile. This way, we could even deploy the function as a container to ECS if needed.

We'll create a container using Docker. If you're new to Docker, don't worry! You can find all the help you need in the official documentation here, which includes step-by-step guides on how to get Docker up and running on both Mac and Windows.

Create a Dockerfile

The Dockerfile will have instructions for compiling the TypeScript code, making sure the image is perfectly set up for deployment on AWS.

Go ahead and create a file named "Dockerfile", then copy and paste the following code into it.

# We are using a multi-staged build here for optimisation. 

# Stage 1 - transpile typescript to javascript

# Using the base image provided by AWS
# base images ensure that the environment and the operating system is properly setup
# so that we don't have to manually configure it

FROM public.ecr.aws/lambda/nodejs:18 as builder
# Sets the working directly of the container, any other command is executed relative to the specified directory.
WORKDIR /usr/app
COPY package.json  ./
COPY tsconfig.json ./
COPY src ./
RUN npm install
RUN npm run build


# Stage 2 -  Created a container image that only contains javascript files and production dependencies
FROM public.ecr.aws/lambda/nodejs:18
WORKDIR ${LAMBDA_TASK_ROOT}
COPY --from=builder /usr/app/build/ ./
CMD ["index.handler"]

Now run the following command to build the image.

docker build --platform linux/amd64 -t my-node-app:latest .

Let us analyze the command:
docker build: This command builds the image based on instructions from the Dockerfile.
--platform linux/amd64: This specifies that the image will be built for the amd64 architecture, which is supported by AWS Lambda.

-t docker-image:test: This option tags the image.

Upload the created image to AWS ECR (Elastic Container Registry)

ECR is a container registry where you can upload, share, and download your images easily.
Assuming you've already set up AWS CLI (if not, please check out this guide to get it done AWS CLI), just run the get-login-password AWS CLI command to connect your Docker CLI to your Amazon ECR registry.

aws ecr get-login-password --region ap-southeast-2 | docker login --username AWS --password-stdin 111122223333.dkr.ecr.ap-southeast-2.amazonaws.com

Let's analyze the CLI command in detail:

aws ecr get-login-password: Retrieves your login password from AWS.

  • --region ap-southeast-2: Sets the region of the ECR. The example uses ap-southeast-2 because it's based in Australia.

  • | (pipe): A shell operator that passes the output from the previous command to the next command. It's used here to avoid displaying or manually copying the password.

  • docker login --username AWS --password-stdin: Logs into Docker with AWS as the username. The --password-stdin option means the password is taken from the standard input, which is the output from the previous command.

  • 111122223333.dkr.ecr.ap-southeast-2.amazonaws.com: The URL of the ECR registry. Replace "111122223333" with your own AWS account ID, which you can find on your AWS dashboard.

    After running the command you should receive a "Login Succeeded" message indicating that you have authenticated docker to ECR.

  • Create a repository in ECR

    Create a repository in ECR by running this command

aws ecr create-repository --repository-name hello-world --region ap-southeast-2 --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE

You should see a JSON object like the one below, which means you've successfully created your registry. You can double-check this by visiting the AWS dashboard.

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-southeast-2:111122223333:repository/my-node-js-app-repository",
        "registryId": "79136265922",
        "repositoryName": "my-node-js-app-repository",
        "repositoryUri": "111122223333.dkr.ecr.ap-southeast-2.amazonaws.com/my-node-js-app-repository",
        "createdAt": "2024-04-24T19:54:41.051000+10:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": true
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

Tag the image

The next step is to tag the local image in the Amazon ECR repository as the latest version. Just add :latest at the end of the repositoryUrl to make sure your image is tagged as the latest version.

docker tag my-node-js-app:latest 111122223333.dkr.ecr.ap-southeast-2.amazonaws.com/my-node-js-app-repository:latest

Why do we need to tag the image?

  1. Repository management: This lets us define where the image is stored. Here, the repositoryUri comes from the previous step.

  2. Version control: We're labeling the version as latest.

Deploying the local image to the ECR registry
Execute the following command to push the image to the registry.

docker push 111122223333.dkr.ecr.ap-southeast-2.amazonaws.com/my-node-js-app-repository:latest

You should see an output similar to this.

The push refers to repository [111122223333.dkr.ecr.ap-southeast-2.amazonaws.com/my-node-js-app-repository]
9dc66a3df2af: Pushed 
c8de204b13ef: Pushed 
b1c8f1724be1: Pushed 
5f70bf18a086: Pushed 
e87362072f2a: Pushed 
bc47dfeb665a: Pushed 
fbbd8c1e2ec1: Pushed 
25139268e1cb: Pushed 
34c58d325578: Pushed 
97a787951169: Pushed 
latest: digest: sha256:072340e0f190c9ada4bd914f7636fc521b2060dcaaba31f5c09cec6f2c88be27 size: 2417

We're nearly ready to deploy the function! But first, if you don't already have one, we need to create an execution role.

What are execution roles?
The execution role gives your function permission to access AWS resources.
You can create an execution role by running the command,

aws iam create-role --role-name lambda-ex --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'

running the command above should print out the following output

{
    "Role": {
        "Path": "/",
        "RoleName": "lambda-ex",
        "RoleId": "AROA97CRFYFFYXIDPNLI6",
        "Arn": "arn:aws:iam::1111984768:role/lambda-ex",
        "CreateDate": "2024-04-24T10:06:41+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

You can also expand the execution role by adding more permissions.
For instance, if you want your Lambda function to write logs to AWS CloudWatch, you can run the following command:

 aws iam attach-role-policy --role-name lambda-ex --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Deploying the Lambda function

You're ready to deploy your Lambda function! We can use the AWS Lambda create-function command to get it up and running.

aws lambda create-function \
  --function-name my-node-js-app \
  --package-type Image \
  --code ImageUri=111122223333.dkr.ecr.ap-southeast-2.amazonaws.com/my-node-js-app-repository:latest \
  --role arn:aws:iam::111122223333:role/lambda-ex

--function-name: This is where you name your function. Feel free to choose any name you like.
--package-type: We've set this to Image because we're deploying a Docker image.
--code ImageUri: Copy the Repository Uri from the steps above. You can create the imageUri using <repositoryUri>:<version>. (It's the same as the tag)

After running the command, you'll get a long JSON object that shows your function is on its way to being created.

{
    "FunctionName": "my-node-js-app",
    "FunctionArn": "arn:aws:lambda:ap-southeast-2:111122223333:function:my-node-js-app",
    "Role": "arn:aws:iam::111122223333:role/lambda-ex",
    "CodeSize": 0,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2024-04-24T10:07:45.163+0000",
    "CodeSha256": "072340e0f190c9ada4bd94f7636fc521b2069gcaaba31f5c09cec6f2c88be27",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "18a28750-705a-48be-877f-89a5ff62401e",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Image",
    "Architectures": [
        "x86_64"
{
    "FunctionName": "my-node-js-app",
    "FunctionArn": "arn:aws:lambda:ap-southeast-2:111122223333:function:my-node-js-app",
    "Role": "arn:aws:iam::111122223333:role/lambda-ex",
    "CodeSize": 0,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2024-04-24T10:07:45.163+0000",
    "CodeSha256": "072340e0f190c9ada4bd914f7636fc521b2060dcaaba31f5c09cec6f2c88be27",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "18a28750-705a-48be-877f-89a5ff62401e",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Image",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    },
    "SnapStart": {
        "ApplyOn": "None",
        "OptimizationStatus": "Off"
    },
    "LoggingConfig": {
        "LogFormat": "Text",
        "LogGroup": "/aws/lambda/my-node-js-app"
    }
}

You should be able to see the function in your AWS console under the lambda section!

Thank you and happy coding!

Did you find this article valuable?

Support Alwin Sunny by becoming a sponsor. Any amount is appreciated!