Skip to content

Commit

Permalink
feat(AWS Lambda): Support 64-bit ARM architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
medikoo committed Oct 4, 2021
1 parent 01c4ae3 commit fe655d4
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 0 deletions.
21 changes: 21 additions & 0 deletions docs/providers/aws/guide/functions.md
Expand Up @@ -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-<service>-<stage>`. 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 functions 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:
Expand Down
3 changes: 3 additions & 0 deletions docs/providers/aws/guide/layers.md
Expand Up @@ -35,6 +35,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.
# - '*'
Expand Down
5 changes: 5 additions & 0 deletions docs/providers/aws/guide/serverless.yml.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
- '*'
Expand Down
4 changes: 4 additions & 0 deletions lib/plugins/aws/package/compile/functions.js
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions lib/plugins/aws/package/compile/layers.js
Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions lib/plugins/aws/provider.js
Expand Up @@ -496,6 +496,7 @@ class AwsProvider {
],
},
},
awsLambdaArchitecture: { enum: ['arm64', 'x86_64'] },
awsLambdaEnvironment: {
type: 'object',
patternProperties: {
Expand Down Expand Up @@ -737,6 +738,7 @@ class AwsProvider {
},
apiKeys: { $ref: '#/definitions/awsApiGatewayApiKeys' },
apiName: { type: 'string' },
architecture: { $ref: '#/definitions/awsLambdaArchitecture' },
cfnRole: { $ref: '#/definitions/awsArn' },
cloudFront: {
type: 'object',
Expand Down Expand Up @@ -1162,6 +1164,7 @@ class AwsProvider {
},
function: {
properties: {
architecture: { $ref: '#/definitions/awsLambdaArchitecture' },
awsKmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
condition: { $ref: '#/definitions/awsResourceCondition' },
dependsOn: { $ref: '#/definitions/awsResourceDependsOn' },
Expand Down Expand Up @@ -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' },
Expand Down
18 changes: 18 additions & 0 deletions test/integration/aws/function.test.js
Expand Up @@ -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',
},
Expand Down Expand Up @@ -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);
});
});
55 changes: 55 additions & 0 deletions test/unit/lib/plugins/aws/package/compile/functions.test.js
Expand Up @@ -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',
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions test/unit/lib/plugins/aws/package/compile/layers.test.js
Expand Up @@ -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'],
},
Expand Down Expand Up @@ -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];
Expand Down

0 comments on commit fe655d4

Please sign in to comment.