Skip to main content
In this guide you’ll manually deploy a Node.js Express application to AWS Lambda (Node.js 20.x). We will package the app, upload the ZIP to S3, and update an existing Lambda function. In a follow-up lesson we’ll automate this with Jenkins Pipelines. Below are the AWS objects created for this deployment and their purposes.
Resource TypeName / IdentifierPurpose
S3 Bucketsolar-system-lambda-bucketStore the deployment ZIP for Lambda
Lambda Functionsolar-system-functionThe existing function we will update
Function URL(Lambda Function URL)Public endpoint to verify UI and behavior
A screenshot of the AWS S3 console showing the "General purpose buckets" list with two buckets: "solar-system-jenkins-reports-bucket" and "solar-system-lambda-bucket" in the US East (Ohio) us-east-2 region. The page shows creation dates, IAM Access Analyzer links, and controls like "Create bucket."
We will create a ZIP archive of the minimal application artifacts, upload it to the S3 bucket above, and use aws lambda update-function-code to point the Lambda function to that S3 object. The Lambda function already exists; we will update it instead of creating a new function.
A screenshot of the AWS Lambda console showing a function's Code properties and Runtime settings, including package size (9.9 MB), SHA256 hash, and last modified timestamp. It lists the runtime as Node.js 20.x, handler app.handler, and architecture x86_64.
Key Lambda details:
  • Runtime: Node.js 20.x
  • Handler: app.handler (project uses app.js)
  • Current package size: ~10 MB
The packaged application contains server-side JavaScript plus static assets (HTML, CSS, images). Example static HTML snippet found in the repo:
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Solar System - Sidd</title>
    <link rel="icon" type="image/x-icon" href="https://gitlab.com/sidd-larth/solar-system/-/raw/main/images/favicon.ico">
    <style>
      #planetImage {
        background: url('https://gitlab.com/sidd-larth/solar-system/-/raw/main/images/solar-system.png');
        background-repeat: no-repeat;
        background-size: cover;
        position: static;
        animation: spin 20s linear infinite;
        width: 30vw;
      }
    </style>
  </head>
  <body>
  </body>
</html>
Important: the deployed Lambda currently has MongoDB credentials stored as plaintext environment variables.
A screenshot of the AWS Lambda console for a function named "solar-system-function," open to the Configuration → Environment variables tab showing keys like MONGO_PASSWORD, MONGO_URI, and MONGO_USERNAME with their values. The page also shows navigation tabs, action buttons (Throttle, Copy ARN, Actions), and the left-side configuration menu.
Do not store sensitive secrets in plain environment variables for production. Use a secrets manager and grant the Lambda execution role least privilege.
For production deployments, avoid hardcoding sensitive values. Use AWS Secrets Manager or AWS Systems Manager Parameter Store (SSM) and grant the Lambda role least privilege to retrieve secrets.
The Lambda function is exposed with a Function URL (you can view and test this in the Lambda console).
A screenshot of the AWS Lambda console on the Configuration > Function URL tab, showing a public function URL with Auth type "NONE", Invoke mode "BUFFERED", and CORS settings. The left sidebar lists other configuration sections like Triggers, Permissions, and Environment variables.
Preparing the local workspace and packaging the app
A dark-themed browser view of a code repository page (dasher-org/solar-system) showing the file list, recent commits, branches and repository stats. The page displays filenames like Dockerfile, app.js and README.md along with commit messages and timestamps.
Project details and Node.js serverless setup:
  • The app uses serverless-http to wrap an Express app for Lambda.
  • The handler export should be: module.exports.handler = serverless(app)
  • If running locally with app.listen, those lines must be commented/removed for Lambda.
Example package.json dependencies (serverless-http included):
{
  "nyc": {
    "check-coverage": true,
    "lines": 90
  },
  "dependencies": {
    "@babel/traverse": "^7.23.2",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "mocha-junit-reporter": "^2.2.1",
    "mongoose": "5.13.20",
    "nyc": "^15.1.0",
    "serverless-http": "^3.2.0"
  },
  "devDependencies": {
    "chai": "*",
    "chai-http": "*",
    "mocha": "*"
  }
}
Relevant excerpt from app.js showing serverless-http usage and MongoDB env var consumption:
const path = require('path');
const fs = require('fs');
const express = require('express');
const os = require('os');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const cors = require('cors');
const serverless = require('serverless-http');

const app = express();

app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, '/')));
app.use(cors());

mongoose.connect(process.env.MONGO_URI, {
  user: process.env.MONGO_USERNAME,
  pass: process.env.MONGO_PASSWORD,
  useNewUrlParser: true,
  useUnifiedTopology: true
}, function(err) {
  if (err) {
    console.error('MongoDB connection error:', err);
  }
});

// If running locally, the app might have these lines:
// app.listen(3000, () => { console.log("Server successfully running on port - " + 3000); })
// module.exports = app;

// For Lambda we must export the serverless handler:
module.exports.handler = serverless(app);
Deployment checklist (high-level)
  1. Clone the repository and install dependencies.
  2. Apply any app changes to verify deployment (e.g., bump a visible version string).
  3. Remove or comment local server start code (app.listen) and ensure the Lambda handler export is present.
  4. Zip the minimal files required by Lambda (app files, package.json, index.html, node_modules).
  5. Upload the ZIP to S3.
  6. Update the Lambda function using the S3 object via AWS CLI.
  7. Retrieve and test the Function URL.
Recommended shell commands (run from a workspace/sandbox):
# clone and enter repo (example)
git clone https://gitlab.com/dasher-org/solar-system.git
cd solar-system

# install dependencies (creates node_modules)
npm install

# update index.html version (example using sed)
# (This is just an example change to indicate a new version in the UI)
sed -i 's/SOLAR SYSTEM 3.0/SOLAR SYSTEM 4.0/' index.html

# Modify app.js for Lambda:
# Comment any local app.listen line and module.exports = app;
sed -i 's/^app\.listen(3000.*$/\/\/&/' app.js
sed -i 's/^module\.exports = app;/\/\/&/' app.js

# Ensure the serverless handler line is uncommented:
sed -i 's/^\/\/module\.exports\.handler/module.exports.handler/' app.js

# Verify the last 5 lines to confirm changes
tail -n 5 app.js

# Create the deployment ZIP with the needed files (app*, package*, index.html, node*)
zip -qr solar-system-lambda.zip app* package* index.html node*

# Upload the ZIP to S3
aws s3 cp solar-system-lambda.zip s3://solar-system-lambda-bucket/solar-system-lambda.zip

# Update the Lambda function from the S3 object
aws lambda update-function-code --function-name solar-system-function --s3-bucket solar-system-lambda-bucket --s3-key solar-system-lambda.zip

# Retrieve function URL config to get the FunctionUrl
aws lambda get-function-url-config --function-name solar-system-function
After uploading the ZIP, verify the S3 object exists in the console.
A screenshot of the AWS S3 console showing the bucket "solar-system-lambda-bucket" with a single object: "solar-system-lambda.zip" (9.9 MB) and its last modified timestamp. The S3 toolbar (Upload, Actions, Copy URL, etc.) and bucket tabs (Objects, Properties, Permissions) are also visible.
When the CLI update completes, it returns function metadata (JSON) including LastModified, CodeSize, and the environment variables currently configured. Example (truncated):
{
  "FunctionName": "solar-system-function",
  "Runtime": "nodejs20.x",
  "Handler": "app.handler",
  "CodeSize": 10332446,
  "LastModified": "2024-10-02T07:36:54.000+0000",
  "Version": "$LATEST",
  "Environment": {
    "Variables": {
      "MONGO_USERNAME": "superuser",
      "MONGO_PASSWORD": "SuperPassword",
      "MONGO_URI": "mongodb+srv://supercluster.d83jj.mongodb.net/superData"
    }
  }
}
To obtain the Function URL programmatically:
aws lambda get-function-url-config --function-name solar-system-function
Example response (truncated):
{
  "FunctionUrl": "https://jkg12znysg2qhkphl6766t2p5de0jocjm.lambda-url.us-east-2.on.aws/",
  "AuthType": "NONE",
  "Cors": {
    "AllowOrigins": ["*"]
  },
  "InvokeMode": "BUFFERED"
}
Open that Function URL (or refresh in the browser) to verify the deployment and any UI changes you made (the example uses “SOLAR SYSTEM 4.0”).
A web page titled "SOLAR SYSTEM 4.0" with a starry background showing a colorful, cartoon-style Sun and planets arranged in orbital rings on the right. On the left are a purple search/input panel and descriptive text about the solar system.
Concise summary of the manual deployment steps:
  • Ensure app exports a Lambda-compatible handler (module.exports.handler = serverless(app)) and remove local app.listen lines.
  • Install dependencies with npm install.
  • Create a ZIP containing only the files Lambda needs.
  • Upload the ZIP to an S3 bucket.
  • Update the Lambda function with aws lambda update-function-code referencing the S3 bucket and key.
  • Confirm deployment by retrieving the Function URL and testing the endpoint.
Links and references Next lesson: Automate these steps and make deployments repeatable and secure with Jenkins Pipelines.