From 2811a104f64b89d90b46909c83eda228b836cf7d Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Mon, 4 Oct 2021 16:24:24 +0200 Subject: [PATCH] feat(AWS Lambda): Support 64-bit ARM architecture --- docs/providers/aws/guide/functions.md | 21 +++++++ docs/providers/aws/guide/serverless.yml.md | 5 ++ lib/plugins/aws/package/compile/functions.js | 4 ++ lib/plugins/aws/package/compile/layers.js | 3 + lib/plugins/aws/provider.js | 8 +++ test/integration/aws/function.test.js | 18 ++++++ .../aws/package/compile/functions.test.js | 55 +++++++++++++++++++ .../aws/package/compile/layers.test.js | 8 +++ 8 files changed, 122 insertions(+) diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md index 554c1f6682e9..e15b24665e20 100644 --- a/docs/providers/aws/guide/functions.md +++ b/docs/providers/aws/guide/functions.md @@ -284,6 +284,27 @@ functions: During the first deployment when locally built images are used, Framework will automatically create a dedicated ECR repository to store these images, with name `serverless--`. Currently, the Framework will not remove older versions of images uploaded to ECR as they still might be in use by versioned functions. During `sls remove`, the created ECR repository will be removed. During deployment, Framework will attempt to `docker login` to ECR if needed. Depending on your local configuration, docker authorization token might be stored unencrypted. Please refer to documentation for more details: https://docs.docker.com/engine/reference/commandline/login/#credentials-store +## Instruction set architecture + +By default, Lambda functions are run by 64-bit x86 architecture CPUs. However, [using arm64 architecture](https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html) (AWS Graviton2 processor) may result in better pricing and performance. + +To switch all lambdas to AWS Graviton2 processor, configure `architecture` at `provider` level as follows: + +```yml +provider: + ... + architecture: arm64 +``` + +To toggle instruction set architecture per function individually, set it directly at `functions[]` context: + +```yaml +functions: + hello: + ... + architecture: arm64 +``` + ## VPC Configuration You can add VPC configuration to a specific function in `serverless.yml` by adding a `vpc` object property in the function configuration. This object should contain the `securityGroupIds` and `subnetIds` array properties needed to construct VPC for this function. Here's an example configuration: diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 09c503475435..a8f1018dc190 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -101,6 +101,7 @@ provider: QueryStrings: - not-cached-query-string versionFunctions: false # Optional function versioning + architecture: x86_64 # Default instruction set architecture for Lambda functions (for ARM, AWS Graviton2 processor based, set it to 'arm64') environment: # Service wide environment variables serviceEnvVar: 123456789 endpointType: regional # Optional endpoint configuration for API Gateway REST API. Default is Edge. @@ -308,6 +309,7 @@ functions: usersCreate: # A Function handler: users.create # The file and module for this specific function. Cannot be used when `image` is defined. image: baseimage # Image to be used by function, cannot be used when `handler` is defined. It can be configured as concrete uri of Docker image in ECR or as a reference to image defined in `provider.ecr.images` + architecture: x86_64 # Instruction set architecture of a Lambda function (for ARM, AWS Graviton2 processor based, set it to 'arm64') name: ${sls:stage}-lambdaName # optional, Deployed Lambda name description: My function # The description of your function. memorySize: 512 # memorySize for this specific function. @@ -597,6 +599,9 @@ layers: description: Description of what the lambda layer does # optional, Description to publish to AWS compatibleRuntimes: # optional, a list of runtimes this layer is compatible with - python3.8 + compatibleArchitectures: # optional, a list of architectures this layer is compatible with + - x86_64 + - arm64 licenseInfo: GPLv3 # optional, a string specifying license information allowedAccounts: # optional, a list of AWS account IDs allowed to access this layer. - '*' diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index 960ba46d0851..c84c796534da 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -245,6 +245,10 @@ class AwsCompileFunctions { functionResource.Properties.MemorySize = functionObject.memory; functionResource.Properties.Timeout = functionObject.timeout; + const functionArchitecture = + functionObject.architecture || this.serverless.service.provider.architecture; + if (functionArchitecture) functionResource.Properties.Architectures = [functionArchitecture]; + if (functionObject.description) { functionResource.Properties.Description = functionObject.description; } diff --git a/lib/plugins/aws/package/compile/layers.js b/lib/plugins/aws/package/compile/layers.js index 10a3d4d0416e..766c3adf9409 100644 --- a/lib/plugins/aws/package/compile/layers.js +++ b/lib/plugins/aws/package/compile/layers.js @@ -48,6 +48,9 @@ class AwsCompileLayers { if (layerObject.compatibleRuntimes) { newLayer.Properties.CompatibleRuntimes = layerObject.compatibleRuntimes; } + if (layerObject.compatibleArchitectures) { + newLayer.Properties.CompatibleArchitectures = layerObject.compatibleArchitectures; + } let layerLogicalId = this.provider.naming.getLambdaLayerLogicalId(layerName); const layerArtifactPath = getLambdaLayerArtifactPath( diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 7546fe5485b6..dff500afaa88 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -496,6 +496,7 @@ class AwsProvider { ], }, }, + awsLambdaArchitecture: { enum: ['arm64', 'x86_64'] }, awsLambdaEnvironment: { type: 'object', patternProperties: { @@ -737,6 +738,7 @@ class AwsProvider { }, apiKeys: { $ref: '#/definitions/awsApiGatewayApiKeys' }, apiName: { type: 'string' }, + architecture: { $ref: '#/definitions/awsLambdaArchitecture' }, cfnRole: { $ref: '#/definitions/awsArn' }, cloudFront: { type: 'object', @@ -1162,6 +1164,7 @@ class AwsProvider { }, function: { properties: { + architecture: { $ref: '#/definitions/awsLambdaArchitecture' }, awsKmsKeyArn: { $ref: '#/definitions/awsKmsArn' }, condition: { $ref: '#/definitions/awsResourceCondition' }, dependsOn: { $ref: '#/definitions/awsResourceDependsOn' }, @@ -1285,6 +1288,11 @@ class AwsProvider { pattern: '^(\\d{12}|\\*|arn:(aws[a-zA-Z-]*):iam::\\d{12}:root)$', }, }, + compatibleArchitectures: { + type: 'array', + items: { $ref: '#/definitions/awsLambdaArchitecture' }, + maxItems: 2, + }, compatibleRuntimes: { type: 'array', items: { $ref: '#/definitions/awsLambdaRuntime' }, diff --git a/test/integration/aws/function.test.js b/test/integration/aws/function.test.js index bbe94e711646..f3c109897897 100644 --- a/test/integration/aws/function.test.js +++ b/test/integration/aws/function.test.js @@ -17,6 +17,10 @@ describe('test/integration/aws/function.test.js', function () { const serviceData = await fixtures.setup('function', { configExt: { functions: { + arch: { + handler: 'basic.handler', + architecture: 'arm64', + }, target: { handler: 'target.handler', }, @@ -51,4 +55,18 @@ describe('test/integration/aws/function.test.js', function () { ); expect(events.length > 0).to.equal(true); }); + + it('should run lambda in `arm64` architecture', async () => { + const events = await confirmCloudWatchLogs( + `/aws/lambda/${stackName}-arch`, + async () => { + await awsRequest('Lambda', 'invoke', { + FunctionName: `${stackName}-arch`, + InvocationType: 'Event', + }); + }, + { checkIsComplete: (soFarEvents) => soFarEvents.length } + ); + expect(events.length > 0).to.equal(true); + }); }); diff --git a/test/unit/lib/plugins/aws/package/compile/functions.test.js b/test/unit/lib/plugins/aws/package/compile/functions.test.js index a804082080f9..dc5ee8dbafd6 100644 --- a/test/unit/lib/plugins/aws/package/compile/functions.test.js +++ b/test/unit/lib/plugins/aws/package/compile/functions.test.js @@ -1356,6 +1356,8 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { let iamRolePolicyStatements; before(async () => { + const imageSha = '6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38'; + const imageWithSha = `000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:${imageSha}`; const { awsNaming, cfTemplate, fixtureData } = await runServerless({ fixture: 'packageArtifact', command: 'package', @@ -1393,6 +1395,7 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { versionFunctions: false, }, functions: { + fnImage: { image: imageWithSha }, foo: { vpc: { subnetIds: ['subnet-02020202'], @@ -1629,6 +1632,35 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { }); }); + it('should support `provider.architecture`', async () => { + const imageSha = '6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38'; + const imageWithSha = `000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:${imageSha}`; + const { + awsNaming: localNaming, + cfTemplate: { Resources: localResources }, + } = await runServerless({ + fixture: 'function', + command: 'package', + configExt: { + functions: { fnImage: { image: imageWithSha } }, + provider: { architecture: 'arm64' }, + }, + }); + + expect( + localResources[localNaming.getLambdaLogicalId('basic')].Properties.Architectures + ).to.deep.equal(['arm64']); + expect( + localResources[localNaming.getLambdaLogicalId('fnImage')].Properties.Architectures + ).to.deep.equal(['arm64']); + expect(cfResources[naming.getLambdaLogicalId('fnImage')].Properties).to.not.have.property( + 'Architectures' + ); + expect(cfResources[naming.getLambdaLogicalId('foo')].Properties).to.not.have.property( + 'Architectures' + ); + }); + it('should support `vpc` defined with `Fn::Split`', async () => { const { awsNaming, cfTemplate, fixtureData } = await runServerless({ fixture: 'function', @@ -1831,6 +1863,10 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { handler: 'trigger.handler', destinations: { onSuccess: 'target' }, }, + fnArch: { + handler: 'target.handler', + architecture: 'arm64', + }, fnTargetFailure: { handler: 'target.handler', }, @@ -1859,6 +1895,10 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { fnImage: { image: imageWithSha, }, + fnImageArch: { + image: imageWithSha, + architecture: 'arm64', + }, fnImageWithConfig: { image: { uri: imageWithSha, @@ -2085,6 +2125,21 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { // https://github.com/serverless/serverless/blob/d8527d8b57e7e5f0b94ba704d9f53adb34298d99/lib/plugins/aws/package/compile/functions/index.test.js#L2381-L2397 }); + it('should support `functions[].architecture`', () => { + expect( + cfResources[naming.getLambdaLogicalId('fnArch')].Properties.Architectures + ).to.deep.equal(['arm64']); + expect( + cfResources[naming.getLambdaLogicalId('fnImageArch')].Properties.Architectures + ).to.deep.equal(['arm64']); + expect(cfResources[naming.getLambdaLogicalId('fnImage')].Properties).to.not.have.property( + 'Architectures' + ); + expect(cfResources[naming.getLambdaLogicalId('target')].Properties).to.not.have.property( + 'Architectures' + ); + }); + it('should support `functions[].destinations.onSuccess` referencing function in same stack', () => { const destinationConfig = cfResources[naming.getLambdaEventConfigLogicalId('trigger')].Properties.DestinationConfig; diff --git a/test/unit/lib/plugins/aws/package/compile/layers.test.js b/test/unit/lib/plugins/aws/package/compile/layers.test.js index 0c9091eeba63..909439ac355f 100644 --- a/test/unit/lib/plugins/aws/package/compile/layers.test.js +++ b/test/unit/lib/plugins/aws/package/compile/layers.test.js @@ -49,6 +49,7 @@ describe('lib/plugins/aws/package/compile/layers/index.test.js', () => { description: 'Layer two example', path: 'layer', compatibleRuntimes: ['nodejs12.x'], + compatibleArchitectures: ['arm64'], licenseInfo: 'GPL', allowedAccounts: ['123456789012', '123456789013'], }, @@ -188,6 +189,13 @@ describe('lib/plugins/aws/package/compile/layers/index.test.js', () => { expect(layerOne.Properties.CompatibleRuntimes).to.deep.equals(['nodejs12.x']); }); + it('should support `layers[].compatibleArchitectures`', () => { + const layerResourceName = naming.getLambdaLayerLogicalId('LayerTwo'); + const layerOne = cfResources[layerResourceName]; + + expect(layerOne.Properties.CompatibleArchitectures).to.deep.equals(['arm64']); + }); + it('should support `layers[].licenseInfo`', () => { const layerResourceName = naming.getLambdaLayerLogicalId('LayerTwo'); const layerOne = cfResources[layerResourceName];