UNPKG

lambda-live-debugger

Version:

Debug Lambda functions locally like it is running in the cloud

537 lines (536 loc) 20 kB
import { DeleteLayerVersionCommand, LambdaClient, ListLayerVersionsCommand, PublishLayerVersionCommand, UpdateFunctionConfigurationCommand, GetFunctionCommand, ListLayersCommand, } from '@aws-sdk/client-lambda'; import { IAMClient, GetRolePolicyCommand, PutRolePolicyCommand, DeleteRolePolicyCommand, } from '@aws-sdk/client-iam'; import { getVersion } from './version.mjs'; import fs from 'fs/promises'; import * as path from 'path'; import { Configuration } from './configuration.mjs'; import { AwsCredentials } from './awsCredentials.mjs'; import { getModuleDirname } from './getDirname.mjs'; import { Logger } from './logger.mjs'; let lambdaClient; let iamClient; const inlinePolicyName = 'LambdaLiveDebuggerPolicy'; const layerName = 'LambdaLiveDebugger'; /** * Policy document to attach to the Lambda role */ const policyDocument = { Version: '2012-10-17', Statement: [ { Action: 'iot:*', Resource: '*', Effect: 'Allow', }, ], }; /** * Get the Lambda client * @returns */ function getLambdaClient() { if (!lambdaClient) { lambdaClient = new LambdaClient({ region: Configuration.config.region, credentials: AwsCredentials.getCredentialsProvider({ region: Configuration.config.region, profile: Configuration.config.profile, role: Configuration.config.role, }), }); } return lambdaClient; } /** * Get the IAM client * @returns */ function getIAMClient() { if (!iamClient) { iamClient = new IAMClient({ region: Configuration.config.region, credentials: AwsCredentials.getCredentialsProvider({ region: Configuration.config.region, profile: Configuration.config.profile, role: Configuration.config.role, }), }); } return iamClient; } /** * Find an existing layer * @param layerName * @param description * @returns */ async function findExistingLayerVersion(layerName, description) { let nextMarker; do { const listLayerVersionsCommand = new ListLayerVersionsCommand({ LayerName: layerName, Marker: nextMarker, }); const response = await getLambdaClient().send(listLayerVersionsCommand); if (response.LayerVersions && response.LayerVersions.length > 0) { const matchingLayer = response.LayerVersions.find((layer) => layer.Description === description); if (matchingLayer) { Logger.verbose(`Matching layer version: ${matchingLayer.Version}, description: ${matchingLayer.Description}`); return matchingLayer; } } nextMarker = response.NextMarker; } while (nextMarker); Logger.verbose('No existing layer found.'); return undefined; } /** * Deploy the Lambda Layer * @returns */ async function deployLayer() { const layerDescription = `Lambda Live Debugger Layer version ${await getVersion()}`; // Check if the layer already exists const existingLayer = await findExistingLayerVersion(layerName, layerDescription); if (existingLayer && existingLayer.LayerVersionArn && existingLayer.Description === layerDescription // check if the layer version is already deployed ) { // delete existing layer when developing if ((await getVersion()) === '0.0.1') { Logger.verbose('Deleting existing layer version, because it is a development mode.'); const deleteLayerVersionCommand = new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber: existingLayer.Version, }); await getLambdaClient().send(deleteLayerVersionCommand); } else { Logger.verbose(`${layerDescription} already deployed.`); return existingLayer.LayerVersionArn; } } // check the ZIP let layerZipPathFullPath = path.resolve(path.join(getModuleDirname(), './extension/extension.zip')); // get the full path to the ZIP file try { await fs.access(layerZipPathFullPath); } catch { // if I am debugging const layerZipPathFullPath2 = path.join(getModuleDirname(), '../dist/extension/extension.zip'); try { await fs.access(layerZipPathFullPath2); layerZipPathFullPath = layerZipPathFullPath2; } catch { throw new Error(`File for the layer not found: ${layerZipPathFullPath}`); } } Logger.verbose(`Layer ZIP path: ${layerZipPathFullPath}`); // Read the ZIP file containing your layer code const layerContent = await fs.readFile(layerZipPathFullPath); Logger.verbose(`Deploying ${layerDescription}`); // Create the command for publishing a new layer version const publishLayerVersionCommand = new PublishLayerVersionCommand({ LayerName: layerName, Description: layerDescription, Content: { ZipFile: layerContent, }, CompatibleArchitectures: ['x86_64', 'arm64'], CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x'], }); const response = await getLambdaClient().send(publishLayerVersionCommand); if (!response.LayerVersionArn) { throw new Error('Failed to retrieve the layer version ARN'); } Logger.verbose(`Deployed ${response.Description} ARN: ${response.LayerVersionArn}`); return response.LayerVersionArn; } /** * Delete the Lambda Layer */ async function deleteLayer() { let nextMarker; do { const layers = await getLambdaClient().send(new ListLayersCommand({ Marker: nextMarker, MaxItems: 10, })); // Filter layers by name const targetLayers = layers.Layers?.filter((layer) => layer.LayerName === layerName) || []; for (const layer of targetLayers) { await deleteAllVersionsOfLayer(layer.LayerArn); } nextMarker = layers.NextMarker; } while (nextMarker); } /** * Delete all versions of a layer * @param layerArn */ async function deleteAllVersionsOfLayer(layerArn) { let nextMarker; do { const versions = await getLambdaClient().send(new ListLayerVersionsCommand({ LayerName: layerArn, Marker: nextMarker, //MaxItems: 5, })); for (const version of versions.LayerVersions || []) { await deleteLayerVersion(layerArn, version.Version); } nextMarker = versions.NextMarker; } while (nextMarker); } /** * Delete a specific version of a layer * @param layerArn * @param versionNumber */ async function deleteLayerVersion(layerArn, versionNumber) { try { Logger.verbose(`Deleting version ${versionNumber} of layer ${layerArn}`); await getLambdaClient().send(new DeleteLayerVersionCommand({ LayerName: layerArn, VersionNumber: versionNumber, })); } catch (error) { Logger.error(`Error deleting version ${versionNumber} of layer ${layerArn}:`, error); throw error; } } /** * Remove the layer from the Lambda function * @param functionName */ async function removeLayerFromLambda(functionName) { try { let needToUpdate = false; const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout, } = await getLambdaCongfiguration(functionName); if (ddlLayerArns.length > 0) { needToUpdate = true; Logger.verbose(`Detaching layer from the function ${functionName}`); } else { Logger.verbose(`Skipping detaching layer from the function ${functionName}, no layer attached`); } const ddlEnvironmentVariables = getEnvironmentVarablesForDebugger('xxx', 0); // check if environment variables are set for each property for (const [key] of Object.entries(ddlEnvironmentVariables)) { if (environmentVariables && environmentVariables[key]) { needToUpdate = true; break; } } if (needToUpdate) { Logger.verbose(`Updating function configuration for ${functionName} to remove layer and reset environment variables`); Logger.verbose('Existing environment variables', JSON.stringify(environmentVariables, null, 2)); //remove environment variables for (const [key] of Object.entries(ddlEnvironmentVariables)) { if (environmentVariables && environmentVariables[key]) { delete environmentVariables[key]; } } Logger.verbose('New environment variables', JSON.stringify(environmentVariables, null, 2)); const updateFunctionConfigurationCommand = new UpdateFunctionConfigurationCommand({ FunctionName: functionName, Layers: otherLayerArns, Environment: { Variables: { ...environmentVariables, }, }, Timeout: initialTimeout, }); await getLambdaClient().send(updateFunctionConfigurationCommand); Logger.verbose(`Function configuration cleared ${functionName}`); } else { Logger.verbose(`Function ${functionName} configuration already cleared.`); } } catch (error) { throw new Error(`Failed to remove layer from lambda ${functionName}: ${error.message}`, { cause: error }); } } /** * Get the Lambda configuration * @param functionName * @returns */ async function getLambdaCongfiguration(functionName) { try { const getFunctionResponse = await getLambdaClient().send(new GetFunctionCommand({ FunctionName: functionName, })); const timeout = getFunctionResponse.Configuration?.Timeout; // get all layers this fuction has by name const layers = getFunctionResponse.Configuration?.Layers || []; const layerArns = layers.map((l) => l.Arn).filter((arn) => arn); const ddlLayerArns = layerArns.filter((arn) => arn?.includes(`:layer:${layerName}:`)); const otherLayerArns = layerArns.filter((arn) => !arn?.includes(`:layer:${layerName}:`)); const environmentVariables = getFunctionResponse.Configuration?.Environment?.Variables ?? {}; let initialTimeout; const initialTimeoutStr = environmentVariables?.LLD_INITIAL_TIMEOUT; if (!initialTimeoutStr || isNaN(Number(initialTimeoutStr))) { initialTimeout = timeout; } else { initialTimeout = Number(initialTimeoutStr); } return { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout, }; } catch (error) { throw new Error(`Failed to get lambda configuration ${functionName}: ${error.message}`, { cause: error }); } } /** * Attach the layer to the Lambda function * @param functionName * @param functionId * @param layerArn */ async function attachLayerToLambda(functionName, functionId, layerArn) { let needToUpdate = false; const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout } = await getLambdaCongfiguration(functionName); // check if layer is already attached if (!ddlLayerArns?.find((arn) => arn === layerArn)) { needToUpdate = true; Logger.verbose(`[Function ${functionName}] Layer not attached to the function`); } else { Logger.verbose(`[Function ${functionName}] Layer already attached to the function`); } // check if layers with the wrong version are attached if (!needToUpdate && ddlLayerArns.find((arn) => arn !== layerArn)) { needToUpdate = true; Logger.verbose('Layer with the wrong version attached to the function'); } const ddlEnvironmentVariables = getEnvironmentVarablesForDebugger(functionId, initialTimeout); // check if environment variables are already set for each property for (const [key, value] of Object.entries(ddlEnvironmentVariables)) { if (!environmentVariables || environmentVariables[key] !== value) { needToUpdate = true; break; } } if (needToUpdate) { try { const updateFunctionConfigurationCommand = new UpdateFunctionConfigurationCommand({ FunctionName: functionName, Layers: [layerArn, ...otherLayerArns], Environment: { Variables: { ...environmentVariables, ...ddlEnvironmentVariables, }, }, //Timeout: LlDebugger.argOptions.observable ? undefined : 300, // Increase the timeout to 5 minutes Timeout: Math.max(initialTimeout, 300), // Increase the timeout to min. 5 minutes }); await getLambdaClient().send(updateFunctionConfigurationCommand); Logger.verbose(`[Function ${functionName}] Lambda layer and environment variables updated`); } catch (error) { throw new Error(`Failed to update Lambda ${functionName}: ${error.message}`, { cause: error }); } } else { Logger.verbose(`[Function ${functionName}] Lambda layer and environment already up to date`); } } /** * Add the policy to the Lambda role * @param functionName */ async function addPolicyToLambdaRole(functionName) { // Retrieve the Lambda function's execution role ARN const getFunctionResponse = await getLambdaClient().send(new GetFunctionCommand({ FunctionName: functionName, })); const roleArn = getFunctionResponse.Configuration?.Role; if (!roleArn) { throw new Error(`Failed to retrieve the role ARN for Lambda ${functionName}`); } // Extract the role name from the role ARN const roleName = roleArn.split('/').pop(); if (!roleName) { throw new Error(`Failed to extract role name from role ARN: ${roleArn} for lambda ${functionName}`); } const existingPolicy = getPolicyDocument(roleName); let addPolicy = true; // compare existing policy with the new one if (existingPolicy) { if (JSON.stringify(existingPolicy) === JSON.stringify(policyDocument)) { Logger.verbose(`[Function ${functionName}] Policy already attached to the role ${roleName}`); addPolicy = false; } } if (addPolicy) { // add inline policy to the role using PutRolePolicyCommand Logger.verbose(`[Function ${functionName}] Attaching policy to the role ${roleName}`); await getIAMClient().send(new PutRolePolicyCommand({ RoleName: roleName, PolicyName: inlinePolicyName, PolicyDocument: JSON.stringify(policyDocument), })); } } /** * Get the environment variables for the Lambda function * @param functionId * @param timeout * @returns */ function getEnvironmentVarablesForDebugger(functionId, timeout) { return { LLD_FUNCTION_ID: functionId, AWS_LAMBDA_EXEC_WRAPPER: '/opt/lld-wrapper', NODE_OPTIONS: '--enable-source-maps', LLD_DEBUGGER_ID: Configuration.config.debuggerId, LLD_INITIAL_TIMEOUT: timeout ? timeout.toString() : '-1', // should never be negative LLD_OBSERVABLE_MODE: Configuration.config.observable ? 'true' : 'false', LLD_OBSERVABLE_INTERVAL: Configuration.config.interval.toString(), }; } /** * Remove the policy from the Lambda role * @param functionName * @returns */ async function removePolicyFromLambdaRole(functionName) { try { // Retrieve the Lambda function's execution role ARN const getFunctionResponse = await getLambdaClient().send(new GetFunctionCommand({ FunctionName: functionName, })); const roleArn = getFunctionResponse.Configuration?.Role; if (!roleArn) { throw new Error(`Failed to retrieve the role ARN for lambda ${functionName}`); } // Extract the role name from the role ARN const roleName = roleArn.split('/').pop(); if (!roleName) { Logger.error(`Failed to extract role name from role ARN: ${roleArn} for Lambda ${functionName}`); return; } const existingPolicy = await getPolicyDocument(roleName); if (existingPolicy) { try { Logger.verbose(`[Function ${functionName}] Removing policy from the role ${roleName}`); await getIAMClient().send(new DeleteRolePolicyCommand({ RoleName: roleName, PolicyName: inlinePolicyName, })); } catch (error) { Logger.error(`Failed to delete inline policy ${inlinePolicyName} from role ${roleName} for Lambda ${functionName}:`, error); } } else { Logger.verbose(`[Function ${functionName}] No need to remove policy from the role ${roleName}, policy not found`); } } catch (error) { throw new Error(`Failed to remove policy from the role for Lambda ${functionName}: ${error.message}`, { cause: error }); } } /** * Get the policy document needed to attach to the Lambda role needed for the Lambda Live Debugger * @param roleName * @returns */ async function getPolicyDocument(roleName) { try { const policy = await getIAMClient().send(new GetRolePolicyCommand({ RoleName: roleName, PolicyName: inlinePolicyName, })); if (policy.PolicyDocument) { const policyDocument = JSON.parse(decodeURIComponent(policy.PolicyDocument)); return policyDocument; } else { return undefined; } } catch (error) { if (error.name === 'NoSuchEntityException') { return undefined; } else { throw error; } } } /** * Deploy the infrastructure */ async function deployInfrastructure() { const layerVersionArn = await deployLayer(); const promises = []; for (const func of Configuration.getLambdas()) { const p = attachLayerToLambda(func.functionName, func.functionId, layerVersionArn); if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { await p; } else { promises.push(p); } } const p = (async () => { // do not do it in parallel, because Lambdas could share the same role for (const func of Configuration.getLambdas()) { await addPolicyToLambdaRole(func.functionName); } })(); // creates one promise if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { await p; } else { promises.push(p); } await Promise.all(promises); } /** * Remove the infrastructure */ async function removeInfrastructure() { Logger.verbose('Removing Lambda Live Debugger infrastructure.'); const promises = []; for (const func of Configuration.getLambdas()) { const p = removeLayerFromLambda(func.functionName); if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { await p; } else { promises.push(p); } } const p = (async () => { // do not do it in parallel, because Lambdas could share the same role for (const func of Configuration.getLambdas()) { await removePolicyFromLambdaRole(func.functionName); } })(); // creates one promise if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { await p; } else { promises.push(p); } await Promise.all(promises); } export const InfraDeploy = { deployInfrastructure, removeInfrastructure, deleteLayer, };