UNPKG

serverless

Version:

Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more

760 lines (652 loc) • 27.2 kB
'use strict'; const AWS = require('aws-sdk'); const BbPromise = require('bluebird'); const crypto = require('crypto'); const fs = require('fs'); const _ = require('lodash'); const path = require('path'); const deepSortObjectByKey = require('../../../../utils/deepSortObjectByKey'); const getHashForFilePath = require('../lib/getHashForFilePath'); const memoizeeMethods = require('memoizee/methods'); const d = require('d'); class AwsCompileFunctions { constructor(serverless, options) { this.serverless = serverless; this.options = options; const servicePath = this.serverless.config.servicePath || ''; this.packagePath = this.serverless.service.package.path || path.join(servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); this.ensureTargetExecutionPermission = _.memoize(this.ensureTargetExecutionPermission); if ( this.serverless.service.provider.name === 'aws' && this.serverless.service.provider.versionFunctions == null ) { this.serverless.service.provider.versionFunctions = true; } this.hooks = { 'initialize': () => { if (_.get(this.serverless.service.serviceObject, 'awsKmsKeyArn')) { this.serverless._logDeprecation( 'AWS_KMS_KEY_ARN', 'Starting with next major version, ' + '"awsKmsKeyArn" service property will be replaced by "provider.kmsKeyArn"' ); } if ( Object.values(this.serverless.service.functions).some(({ awsKmsKeyArn }) => awsKmsKeyArn) ) { this.serverless._logDeprecation( 'AWS_KMS_KEY_ARN', 'Starting with next major version, ' + '"awsKmsKeyArn" function property will be replaced by "kmsKeyArn"' ); } if ( !this.serverless.service.provider.lambdaHashingVersion && (this.serverless.service.provider.versionFunctions || Object.values(this.serverless.service.functions).some( ({ versionFunction }) => versionFunction )) ) { this.serverless._logDeprecation( 'LAMBDA_HASHING_VERSION_V2', 'Starting with next major version, ' + 'default value of provider.lambdaHashingVersion will be equal to "20201221"' ); } }, 'package:compileFunctions': () => BbPromise.bind(this).then(this.downloadPackageArtifacts).then(this.compileFunctions), }; } compileRole(newFunction, role) { const compiledFunction = newFunction; if (typeof role === 'string') { if (role.startsWith('arn:aws')) { // role is a statically defined iam arn compiledFunction.Properties.Role = role; } else if (role === 'IamRoleLambdaExecution') { // role is the default role generated by the framework compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; } else { // role is a Logical Role Name compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(role); } } else if ('Fn::GetAtt' in role) { // role is an "Fn::GetAtt" object compiledFunction.Properties.Role = role; compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(role['Fn::GetAtt'][0]); } else { // role is an "Fn::ImportValue" object compiledFunction.Properties.Role = role; } } async downloadPackageArtifact(functionName) { const { region } = this.options; const S3 = new AWS.S3({ region }); const functionObject = this.serverless.service.getFunction(functionName); if (functionObject.image) return; const artifactFilePath = _.get(functionObject, 'package.artifact') || _.get(this, 'serverless.service.package.artifact'); const regex = new RegExp('s3\\.amazonaws\\.com/(.+)/(.+)'); const match = artifactFilePath.match(regex); if (!match) return; await new Promise((resolve, reject) => { const tmpDir = this.serverless.utils.getTmpDirPath(); const filePath = path.join(tmpDir, match[2]); const readStream = S3.getObject({ Bucket: match[1], Key: match[2], }).createReadStream(); const writeStream = fs.createWriteStream(filePath); readStream .pipe(writeStream) .on('error', reject) .on('close', () => { if (functionObject.package.artifact) { functionObject.package.artifact = filePath; } else { this.serverless.service.package.artifact = filePath; } return resolve(filePath); }); }); } async addFileToHash(filePath, hash) { const lambdaHashingVersion = this.serverless.service.provider.lambdaHashingVersion; if (lambdaHashingVersion) { const filePathHash = await getHashForFilePath(filePath); hash.write(filePathHash); } else { await addFileContentsToHashes(filePath, [hash]); } } async compileFunction(functionName) { const cfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = this.cfLambdaFunctionTemplate(); const functionObject = this.serverless.service.getFunction(functionName); functionObject.package = functionObject.package || {}; if (!functionObject.handler && !functionObject.image) { throw new this.serverless.classes.Error( `Either "handler" or "image" property needs to be set on function "${functionName}"` ); } if (functionObject.handler && functionObject.image) { throw new this.serverless.classes.Error( `Either "handler" or "image" property (not both) needs to be set on function "${functionName}".` ); } let functionImageUri; let functionImageSha; if (functionObject.image) { ({ functionImageUri, functionImageSha } = await this.resolveImageUriAndSha( functionObject.image )); } // publish these properties to the platform functionObject.memory = functionObject.memorySize || this.serverless.service.provider.memorySize || 1024; if (!functionObject.timeout) { functionObject.timeout = this.serverless.service.provider.timeout || 6; } let artifactFilePath; if (functionObject.handler) { const serviceArtifactFileName = this.provider.naming.getServiceArtifactName(); const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(functionName); artifactFilePath = functionObject.package.artifact || this.serverless.service.package.artifact; if ( !artifactFilePath || (this.serverless.service.artifact && !functionObject.package.artifact) ) { let artifactFileName = serviceArtifactFileName; if (this.serverless.service.package.individually || functionObject.package.individually) { artifactFileName = functionArtifactFileName; } artifactFilePath = path.join( this.serverless.config.servicePath, '.serverless', artifactFileName ); } functionObject.runtime = this.provider.getRuntime(functionObject.runtime); functionResource.Properties.Handler = functionObject.handler; functionResource.Properties.Code.S3Bucket = this.serverless.service.package.deploymentBucket ? this.serverless.service.package.deploymentBucket : { Ref: 'ServerlessDeploymentBucket' }; functionResource.Properties.Code.S3Key = `${ this.serverless.service.package.artifactDirectoryName }/${artifactFilePath.split(path.sep).pop()}`; functionResource.Properties.Runtime = functionObject.runtime; } else { functionResource.Properties.Code.ImageUri = functionImageUri; functionResource.Properties.PackageType = 'Image'; } functionResource.Properties.FunctionName = functionObject.name; functionResource.Properties.MemorySize = functionObject.memory; functionResource.Properties.Timeout = functionObject.timeout; if (functionObject.description) { functionResource.Properties.Description = functionObject.description; } if (functionObject.condition) { functionResource.Condition = functionObject.condition; } if (functionObject.dependsOn) { functionResource.DependsOn = (functionResource.DependsOn || []).concat( functionObject.dependsOn ); } if (functionObject.tags || this.serverless.service.provider.tags) { const tags = Object.assign({}, this.serverless.service.provider.tags, functionObject.tags); functionResource.Properties.Tags = []; Object.entries(tags).forEach(([Key, Value]) => { functionResource.Properties.Tags.push({ Key, Value }); }); } if (functionObject.onError) { const arn = functionObject.onError; if (typeof arn === 'string') { const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution; functionResource.Properties.DeadLetterConfig = { TargetArn: arn, }; // update the PolicyDocument statements (if default policy is used) if (iamRoleLambdaExecution) { iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push({ Effect: 'Allow', Action: ['sns:Publish'], Resource: [arn], }); } } else { functionResource.Properties.DeadLetterConfig = { TargetArn: arn, }; } } let kmsKeyArn; const serviceObj = this.serverless.service.serviceObject; if (serviceObj && serviceObj.awsKmsKeyArn) kmsKeyArn = serviceObj.awsKmsKeyArn; if (this.serverless.service.provider.kmsKeyArn) { kmsKeyArn = this.serverless.service.provider.kmsKeyArn; } if (functionObject.awsKmsKeyArn) kmsKeyArn = functionObject.awsKmsKeyArn; if (functionObject.kmsKeyArn) kmsKeyArn = functionObject.kmsKeyArn; if (kmsKeyArn) { if (typeof kmsKeyArn === 'string') { functionResource.Properties.KmsKeyArn = kmsKeyArn; // update the PolicyDocument statements (if default policy is used) const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution; if (iamRoleLambdaExecution) { iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith( iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement, [ { Effect: 'Allow', Action: ['kms:Decrypt'], Resource: [kmsKeyArn], }, ], _.isEqual ); } } else { functionResource.Properties.KmsKeyArn = kmsKeyArn; } } const tracing = functionObject.tracing || (this.serverless.service.provider.tracing && this.serverless.service.provider.tracing.lambda); if (tracing) { let mode = tracing; if (typeof tracing === 'boolean') { mode = 'Active'; } const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution; functionResource.Properties.TracingConfig = { Mode: mode, }; const stmt = { Effect: 'Allow', Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'], Resource: ['*'], }; // update the PolicyDocument statements (if default policy is used) if (iamRoleLambdaExecution) { iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith( iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement, [stmt], _.isEqual ); } } if (functionObject.environment || this.serverless.service.provider.environment) { functionResource.Properties.Environment = {}; functionResource.Properties.Environment.Variables = Object.assign( {}, this.serverless.service.provider.environment, functionObject.environment ); } this.compileRole( functionResource, functionObject.role || this.serverless.service.provider.role || 'IamRoleLambdaExecution' ); if (!functionObject.vpc) functionObject.vpc = {}; if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {}; functionResource.Properties.VpcConfig = { SecurityGroupIds: functionObject.vpc.securityGroupIds || this.serverless.service.provider.vpc.securityGroupIds, SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds, }; if ( !functionResource.Properties.VpcConfig.SecurityGroupIds || !functionResource.Properties.VpcConfig.SubnetIds ) { delete functionResource.Properties.VpcConfig; } const fileSystemConfig = functionObject.fileSystemConfig; if (fileSystemConfig) { if (!functionResource.Properties.VpcConfig) { const errorMessage = [ `Function "${functionName}": when using fileSystemConfig, `, 'ensure that function has vpc configured ', 'on function or provider level', ].join(''); throw new this.serverless.classes.Error( errorMessage, 'LAMBDA_FILE_SYSTEM_CONFIG_MISSING_VPC' ); } const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution; const stmt = { Effect: 'Allow', Action: ['elasticfilesystem:ClientMount', 'elasticfilesystem:ClientWrite'], Resource: [fileSystemConfig.arn], }; // update the PolicyDocument statements (if default policy is used) if (iamRoleLambdaExecution) { iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(stmt); } const cfFileSystemConfig = { Arn: fileSystemConfig.arn, LocalMountPath: fileSystemConfig.localMountPath, }; functionResource.Properties.FileSystemConfigs = [cfFileSystemConfig]; } if (functionObject.reservedConcurrency || functionObject.reservedConcurrency === 0) { functionResource.Properties.ReservedConcurrentExecutions = functionObject.reservedConcurrency; } if (!functionObject.disableLogs) { functionResource.DependsOn = [this.provider.naming.getLogGroupLogicalId(functionName)].concat( functionResource.DependsOn || [] ); } if (functionObject.layers) { functionResource.Properties.Layers = functionObject.layers; } else if (this.serverless.service.provider.layers) { functionResource.Properties.Layers = this.serverless.service.provider.layers; } const functionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const newFunctionObject = { [functionLogicalId]: functionResource, }; Object.assign(cfTemplate.Resources, newFunctionObject); const shouldVersionFunction = functionObject.versionFunction != null ? functionObject.versionFunction : this.serverless.service.provider.versionFunctions; if (shouldVersionFunction || functionObject.provisionedConcurrency) { // Create hashes for the artifact and the logical id of the version resource // The one for the version resource must include the function configuration // to make sure that a new version is created on configuration changes and // not only on source changes. const versionHash = crypto.createHash('sha256'); versionHash.setEncoding('base64'); const layerConfigurations = _.cloneDeep( extractLayerConfigurationsFromFunction(functionResource.Properties, cfTemplate) ); const versionResource = this.cfLambdaVersionTemplate(); if (functionImageSha) { versionResource.Properties.CodeSha256 = functionImageSha; } else { const fileHash = await getHashForFilePath(artifactFilePath); versionResource.Properties.CodeSha256 = fileHash; await this.addFileToHash(artifactFilePath, versionHash); } // Include all referenced layer code in the version id hash const layerArtifactPaths = []; layerConfigurations.forEach((layer) => { const layerArtifactPath = this.provider.resolveLayerArtifactName(layer.name); layerArtifactPaths.push(layerArtifactPath); }); for (const layerArtifactPath of layerArtifactPaths.sort()) { await this.addFileToHash(layerArtifactPath, versionHash); } // Include function and layer configuration details in the version id hash for (const layerConfig of layerConfigurations) { delete layerConfig.properties.Content.S3Key; } const functionProperties = _.cloneDeep(functionResource.Properties); // In `image` case, we assume it's path to ECR image digest if (!functionObject.image) delete functionProperties.Code; // Properties applied to function globally (not specific to version or alias) delete functionProperties.ReservedConcurrentExecutions; delete functionProperties.Tags; const lambdaHashingVersion = this.serverless.service.provider.lambdaHashingVersion; if (lambdaHashingVersion) { functionProperties.layerConfigurations = layerConfigurations; versionHash.write(JSON.stringify(deepSortObjectByKey(functionProperties))); } else { // sort the layer conifigurations for hash consistency const sortedLayerConfigurations = {}; const byKey = ([key1], [key2]) => key1.localeCompare(key2); for (const { name, properties: layerProperties } of layerConfigurations) { sortedLayerConfigurations[name] = _.fromPairs( Object.entries(layerProperties).sort(byKey) ); } functionProperties.layerConfigurations = sortedLayerConfigurations; const sortedFunctionProperties = _.fromPairs( Object.entries(functionProperties).sort(byKey) ); versionHash.write(JSON.stringify(sortedFunctionProperties)); } versionHash.end(); const versionDigest = versionHash.read(); versionResource.Properties.FunctionName = { Ref: functionLogicalId }; if (functionObject.description) { versionResource.Properties.Description = functionObject.description; } // use the version SHA in the logical resource ID of the version because // AWS::Lambda::Version resource will not support updates const versionLogicalId = this.provider.naming.getLambdaVersionLogicalId( functionName, versionDigest ); functionObject.versionLogicalId = versionLogicalId; const newVersionObject = { [versionLogicalId]: versionResource, }; Object.assign(cfTemplate.Resources, newVersionObject); // Add function versions to Outputs section const functionVersionOutputLogicalId = this.provider.naming.getLambdaVersionOutputLogicalId( functionName ); const newVersionOutput = this.cfOutputLatestVersionTemplate(); newVersionOutput.Value = { Ref: versionLogicalId }; Object.assign(cfTemplate.Outputs, { [functionVersionOutputLogicalId]: newVersionOutput, }); if (functionObject.provisionedConcurrency) { if (!shouldVersionFunction) delete versionResource.DeletionPolicy; const provisionedConcurrency = Number(functionObject.provisionedConcurrency); const aliasLogicalId = this.provider.naming.getLambdaProvisionedConcurrencyAliasLogicalId( functionName ); const aliasName = this.provider.naming.getLambdaProvisionedConcurrencyAliasName(); functionObject.targetAlias = { name: aliasName, logicalId: aliasLogicalId }; const aliasResource = { Type: 'AWS::Lambda::Alias', Properties: { FunctionName: { Ref: functionLogicalId }, FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] }, Name: aliasName, ProvisionedConcurrencyConfig: { ProvisionedConcurrentExecutions: provisionedConcurrency, }, }, DependsOn: functionLogicalId, }; cfTemplate.Resources[aliasLogicalId] = aliasResource; } } this.compileFunctionEventInvokeConfig(functionName); } compileFunctionEventInvokeConfig(functionName) { const functionObject = this.serverless.service.getFunction(functionName); const { destinations, maximumEventAge, maximumRetryAttempts } = functionObject; if (!destinations && !maximumEventAge && maximumRetryAttempts == null) { return; } const destinationConfig = {}; if (destinations) { const hasAccessPoliciesHandledExternally = Boolean( functionObject.role || this.serverless.service.provider.role ); if (destinations.onSuccess) { if (!hasAccessPoliciesHandledExternally) { this.ensureTargetExecutionPermission(destinations.onSuccess); } destinationConfig.OnSuccess = { Destination: destinations.onSuccess.startsWith('arn:') ? destinations.onSuccess : this.provider.resolveFunctionArn(destinations.onSuccess), }; } if (destinations.onFailure) { if (!hasAccessPoliciesHandledExternally) { this.ensureTargetExecutionPermission(destinations.onFailure); } destinationConfig.OnFailure = { Destination: destinations.onFailure.startsWith('arn:') ? destinations.onFailure : this.provider.resolveFunctionArn(destinations.onFailure), }; } } const cfResources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources; const functionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const resource = { Type: 'AWS::Lambda::EventInvokeConfig', Properties: { FunctionName: { Ref: functionLogicalId }, DestinationConfig: destinationConfig, Qualifier: functionObject.targetAlias ? functionObject.targetAlias.name : '$LATEST', }, }; if (maximumEventAge) { resource.Properties.MaximumEventAgeInSeconds = maximumEventAge; } if (maximumRetryAttempts != null) { resource.Properties.MaximumRetryAttempts = maximumRetryAttempts; } cfResources[this.provider.naming.getLambdaEventConfigLogicalId(functionName)] = resource; } // Memoized in a construtor ensureTargetExecutionPermission(functionAddress) { const iamPolicyStatements = this.serverless.service.provider.compiledCloudFormationTemplate .Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement; const action = (() => { if (!functionAddress.startsWith('arn:') || functionAddress.includes(':function:')) { return 'lambda:InvokeFunction'; } if (functionAddress.includes(':sqs:')) return 'sqs:SendMessage'; if (functionAddress.includes(':sns:')) return 'sns:Publish'; if (functionAddress.includes(':event-bus/')) return 'events:PutEvents'; throw new this.serverless.classes.Error(`Unsupported destination target ${functionAddress}`); })(); iamPolicyStatements.push({ Effect: 'Allow', Action: action, // Note: Cannot address function via { 'Fn::GetAtt': [targetLogicalId, 'Arn'] } // as same IAM settings are used for target function and that will introduce // circular dependency error. Relying on Fn::Sub as a workaround Resource: functionAddress.startsWith('arn:') ? functionAddress : { 'Fn::Sub': `arn:\${AWS::Partition}:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${ this.serverless.service.getFunction(functionAddress).name }`, }, }); } downloadPackageArtifacts() { const allFunctions = this.serverless.service.getAllFunctions(); return BbPromise.each(allFunctions, (functionName) => this.downloadPackageArtifact(functionName) ); } compileFunctions() { const allFunctions = this.serverless.service.getAllFunctions(); return Promise.all(allFunctions.map((functionName) => this.compileFunction(functionName))); } cfLambdaFunctionTemplate() { return { Type: 'AWS::Lambda::Function', Properties: { Code: {}, }, }; } cfLambdaVersionTemplate() { return { Type: 'AWS::Lambda::Version', // Retain old versions even though they will not be in future // CloudFormation stacks. On stack delete, these will be removed when // their associated function is removed. DeletionPolicy: 'Retain', Properties: { FunctionName: 'FunctionName', CodeSha256: 'CodeSha256', }, }; } cfOutputLatestVersionTemplate() { return { Description: 'Current Lambda function version', Value: 'Value', }; } } function addFileContentsToHashes(filePath, hashes) { return new Promise((resolve, reject) => { const readStream = fs.createReadStream(filePath); readStream .on('data', (chunk) => { hashes.forEach((hash) => { hash.write(chunk); }); }) .on('close', () => { resolve(); }) .on('error', (error) => { reject(new Error(`Could not add file content to hash: ${error}`)); }); }); } function extractLayerConfigurationsFromFunction(functionProperties, cfTemplate) { const layerConfigurations = []; if (!functionProperties.Layers) return layerConfigurations; functionProperties.Layers.forEach((potentialLocalLayerObject) => { if (potentialLocalLayerObject.Ref) { const configuration = cfTemplate.Resources[potentialLocalLayerObject.Ref]; layerConfigurations.push({ name: configuration._serverlessLayerName, ref: potentialLocalLayerObject.Ref, properties: configuration.Properties, }); } }); return layerConfigurations; } Object.defineProperties( AwsCompileFunctions.prototype, memoizeeMethods({ resolveImageUriAndSha: d( async function (image) { const providedImageSha = image.split('@')[1]; if (providedImageSha) { return { functionImageSha: providedImageSha.slice('sha256:'.length), functionImageUri: image, }; } const [repositoryName, imageTag] = image.split('/')[1].split(':'); const registryId = image.split('.')[0]; const describeImagesResponse = await this.provider.request('ECR', 'describeImages', { imageIds: [ { imageTag, }, ], repositoryName, registryId, }); const imageDigest = describeImagesResponse.imageDetails[0].imageDigest; const functionImageUri = `${image.split(':')[0]}@${imageDigest}`; return { functionImageUri, functionImageSha: imageDigest.slice('sha256:'.length), }; }, { promise: true } ), }) ); module.exports = AwsCompileFunctions;