Lambda function, CloudFront automated deployment solution
Usage Scenario
Automatically update Lambda functions in CI, and update the Distribution configuration under Cloudfront, everything is automated.
Operating Principle
In this process, the user (U) executes a script called deploy:lambda. The script first requests the latest Lambda function version and the remote code hash from AWS. It then calculates the local code hash and compares it with the remote hash. If code changes are detected, the script publishes the updated Lambda function code and waits for the deployment to stabilize. Finally, the script updates the CloudFront distribution configuration. If no code changes are detected, the script informs the user that no deployment is needed and ends the process.
Github Action
name: release-lambda
env:
NODE_OPTIONS: '--max_old_space_size=6144'
on:
push:
branches:
- master
jobs:
release-lambda:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip actions]')"
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: master
token: ${{ secrets.GIT_HUB_TOKEN }}
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Initialize
run: pnpm install // or npm install, yarn install
- name: Build
run: npm run build
- name: 🔑 配置 AWS 凭证
uses: aws-actions/configure-aws-credentials@master
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Deploy Lambda
run: |
export AWS_LAMBDA_DISTRIBUTION_IDS=${{ vars.AWS_LAMBDA_DISTRIBUTION_IDS }}
export AWS_LAMBDA_FUNCTION_NAME=${{ vars.AWS_LAMBDA_FUNCTION_NAME }}
npm run deploy:lambda
"scripts": {
"deploy:lambda": "zx --quiet scripts/deploy-lambda.mjs"
}
- scripts/deploy-lambda.mjs
#!/usr/bin/env zx
import { join } from 'path';
import { cwd } from 'process';
const { writeJsonSync } = require('fs-extra'); // Ensure fs-extra is installed
const fs = require('fs');
require('dotenv').config({ path: join(cwd(), '.env.local') });
class LambdaCloudFrontDeployer {
constructor(distributionIds, functionName) {
this.distributionIds = distributionIds.split(',').map(id => id.trim()).filter(Boolean);
this.functionName = functionName;
this.functionZip = join(cwd(), 'blocklets/cloudfront-shield/dist/function.zip');
}
async getLatestVersion() {
const latestVersion = await $`aws lambda list-versions-by-function --function-name ${this.functionName} --query 'Versions[-1].Version' --output text`;
return latestVersion;
}
async shouldDeploy() {
const localFunctionCodeHash = await this.getLocalFunctionCodeHash();
const remoteFunctionCodeHash = await this.getRemoteFunctionCodeHash();
console.log({
remoteFunctionCodeHash,
localFunctionCodeHash
});
return remoteFunctionCodeHash !== localFunctionCodeHash;
}
async getRemoteFunctionCodeHash() {
const codeSha256 = await $`aws lambda list-versions-by-function --function-name ${this.functionName} --query 'Versions[-1].CodeSize' --output text`;
const remoteHash = parseInt(codeSha256.stdout.trim());
return remoteHash;
}
async getLocalFunctionCodeHash() {
const stat = fs.statSync(this.functionZip);
const localHash = stat.size; // Return the new code's hash value
return localHash;
}
async deploy() {
if (!(await this.shouldDeploy())) {
console.log("No changes detected in the function code. Skipping function deploy.");
return; // No code changes, return directly
}
await this.publish();
await this.updateDistributions();
}
async publish() {
console.log("Publish...");
// Update Lambda function code
await $`aws lambda update-function-code --function-name ${this.functionName} --zip-file fileb://${this.functionZip}`;
await new Promise(resolve => setTimeout(resolve, 30000)); // Wait for 30 seconds
console.log("Get latest version...");
// Publish new version and get version number
const { stdout: latestVersion } = await $`aws lambda publish-version --function-name ${this.functionName}`;
const latestFunctionVersion = JSON.parse(latestVersion).Version;
console.log(`Latest version: ${latestFunctionVersion}`);
// Update alias
await $`aws lambda update-alias --function-name ${this.functionName} --name "production" --function-version ${latestFunctionVersion}`;
console.log("Deployment complete!");
}
async updateDistributions() {
await Promise.all(this.distributionIds.map(id => this.updateDistribution(id)));
}
async updateDistribution(distributionId) {
const latestVersion = await this.getLatestVersion();
const output = await $`aws cloudfront get-distribution-config --id ${distributionId}`;
console.log('updateDistribution', distributionId, output.exitCode);
const data = JSON.parse(output.stdout);
const etag = data.ETag;
const config = data.DistributionConfig;
config.CacheBehaviors.Items.forEach((item) => {
item.LambdaFunctionAssociations.Items?.forEach((item) => {
if (item.LambdaFunctionARN && item.LambdaFunctionARN.includes(this.functionName)) {
console.log(item.LambdaFunctionARN);
item.LambdaFunctionARN = `${this.functionName}:${latestVersion}`.trim();
}
});
});
const configPath = join(cwd(), `${distributionId}.config.json`);
writeJsonSync(configPath, config);
await $`
aws cloudfront update-distribution --id ${distributionId} --if-match ${etag} --distribution-config file://${configPath}`;
console.log(`updateDistribution(${distributionId}) successfully`);
}
}
const distributionIds = process.env.AWS_LAMBDA_DISTRIBUTION_IDS;
const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
const deployer = new LambdaCloudFrontDeployer(distributionIds, functionName);
await deployer.deploy();
console.log('done');
How to test in local
- env.local in your project root folder
AWS_LAMBDA_DISTRIBUTION_IDS=xxx
AWS_LAMBDA_FUNCTION_NAME=arn:aws:lambda:us-east-1:xxx:function:verify
AWS_PROFILE=
npm run deploy:lambda