import _ from 'lodash' import path from 'path' import crypto from 'crypto' import fse from 'fs-extra' import utils from '@serverlessinc/sf-core/src/utils.js' import generateZip from './generate-zip.js' import ServerlessError from '../../../serverless-error.js' const { log } = utils const prepareCustomResourcePackage = _.memoize(async (zipFilePath) => { log.info('Generating custom CloudFormation resources') return Promise.all([generateZip(), fse.mkdirs(path.dirname(zipFilePath))]) .then(([cachedZipFilePath]) => fse.copy(cachedZipFilePath, zipFilePath)) .then(() => path.basename(zipFilePath)) }) async function addCustomResourceToService( awsProvider, resourceName, iamRoleStatements, ) { let functionName let absoluteFunctionName let Handler let customResourceFunctionLogicalId const { serverless } = awsProvider const providerConfig = serverless.service.provider // write custom resource lambda logs by default const shouldWriteLogs = (providerConfig.logs && providerConfig.logs.frameworkLambda) ?? true const { Resources } = providerConfig.compiledCloudFormationTemplate const customResourcesRoleLogicalId = awsProvider.naming.getCustomResourcesRoleLogicalId() const zipFilePath = path.join( serverless.serviceDir, '.serverless', awsProvider.naming.getCustomResourcesArtifactName(), ) const funcPrefix = `${serverless.service.service}-${awsProvider.getStage()}` // check which custom resource should be used if (resourceName === 's3') { functionName = awsProvider.naming.getCustomResourceS3HandlerFunctionName() Handler = 's3/handler.handler' customResourceFunctionLogicalId = awsProvider.naming.getCustomResourceS3HandlerFunctionLogicalId() } else if (resourceName === 'cognitoUserPool') { functionName = awsProvider.naming.getCustomResourceCognitoUserPoolHandlerFunctionName() Handler = 'cognito-user-pool/handler.handler' customResourceFunctionLogicalId = awsProvider.naming.getCustomResourceCognitoUserPoolHandlerFunctionLogicalId() } else if (resourceName === 'eventBridge') { functionName = awsProvider.naming.getCustomResourceEventBridgeHandlerFunctionName() Handler = 'event-bridge/handler.handler' customResourceFunctionLogicalId = awsProvider.naming.getCustomResourceEventBridgeHandlerFunctionLogicalId() } else if (resourceName === 'apiGatewayCloudWatchRole') { functionName = awsProvider.naming.getCustomResourceApiGatewayAccountCloudWatchRoleHandlerFunctionName() Handler = 'api-gateway-cloud-watch-role/handler.handler' customResourceFunctionLogicalId = awsProvider.naming.getCustomResourceApiGatewayAccountCloudWatchRoleHandlerFunctionLogicalId() } else { throw new ServerlessError( `No implementation found for Custom Resource "${resourceName}"`, 'MISSING_CUSTOM_RESOURCE_IMPLEMENTATION', ) } absoluteFunctionName = `${funcPrefix}-${functionName}` if (absoluteFunctionName.length > 64) { // Function names cannot be longer than 64. // Temporary solution until we have https://github.com/serverless/serverless/issues/6598 // (which doesn't change names of already deployed functions) absoluteFunctionName = `${absoluteFunctionName.slice(0, 32)}${crypto .createHash('md5') .update(absoluteFunctionName) .digest('hex')}` } const zipFileBasename = await prepareCustomResourcePackage(zipFilePath) let S3Bucket = { Ref: awsProvider.naming.getDeploymentBucketLogicalId(), } if (serverless.service.package.deploymentBucket) { S3Bucket = serverless.service.package.deploymentBucket } const s3Folder = serverless.service.package.artifactDirectoryName const s3FileName = zipFileBasename const S3Key = `${s3Folder}/${s3FileName}` const customDeploymentRole = awsProvider.getCustomDeploymentRole() if (!customDeploymentRole) { let customResourceRole = Resources[customResourcesRoleLogicalId] if (!customResourceRole) { customResourceRole = { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { Service: ['lambda.amazonaws.com'], }, Action: ['sts:AssumeRole'], }, ], }, Policies: [ { PolicyName: { 'Fn::Join': [ '-', [ awsProvider.getStage(), awsProvider.serverless.service.service, 'custom-resources-lambda', ], ], }, PolicyDocument: { Version: '2012-10-17', Statement: [], }, }, ], }, } Resources[customResourcesRoleLogicalId] = customResourceRole if (shouldWriteLogs) { const logGroupsPrefix = awsProvider.naming.getLogGroupName(funcPrefix) customResourceRole.Properties.Policies[0].PolicyDocument.Statement.push( { Effect: 'Allow', Action: [ 'logs:CreateLogStream', 'logs:CreateLogGroup', 'logs:TagResource', ], Resource: [ { 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + `:log-group:${logGroupsPrefix}*:*`, }, ], }, { Effect: 'Allow', Action: ['logs:PutLogEvents'], Resource: [ { 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + `:log-group:${logGroupsPrefix}*:*:*`, }, ], }, ) } } const { Statement } = customResourceRole.Properties.Policies[0].PolicyDocument iamRoleStatements.forEach((newStmt) => { if ( !Statement.some( (existingStmt) => existingStmt.Resource === newStmt.Resource, ) ) { Statement.push(newStmt) } }) } const customResourceFunction = { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket, S3Key, }, FunctionName: absoluteFunctionName, Handler, MemorySize: 1024, Runtime: 'nodejs20.x', Timeout: 180, }, DependsOn: [], } Resources[customResourceFunctionLogicalId] = customResourceFunction if (customDeploymentRole) { customResourceFunction.Properties.Role = customDeploymentRole } else { customResourceFunction.Properties.Role = { 'Fn::GetAtt': [customResourcesRoleLogicalId, 'Arn'], } customResourceFunction.DependsOn.push(customResourcesRoleLogicalId) } if (shouldWriteLogs) { const customResourceLogGroupLogicalId = awsProvider.naming.getLogGroupLogicalId(functionName) customResourceFunction.DependsOn.push(customResourceLogGroupLogicalId) Object.assign(Resources, { [customResourceLogGroupLogicalId]: { Type: 'AWS::Logs::LogGroup', Properties: { LogGroupName: awsProvider.naming.getLogGroupName(absoluteFunctionName), RetentionInDays: awsProvider.getLogRetentionInDays(), DataProtectionPolicy: awsProvider.getLogDataProtectionPolicy(), }, }, }) } } export { addCustomResourceToService }