Repository: serverless/serverless (JavaScript)

Issue:
sls deploy function -f command is detaching layer from existing Lambda
### Are you certain it's a bug?

- [X] Yes, it looks like a bug

### Is the issue caused by a plugin?

- [X] It is not a plugin issue

### Are you using the latest v3 release?

- [X] Yes, I'm using the latest v3 release

### Is there an existing issue for this?

- [X] I have searched existing issues, it hasn't been reported yet

### Issue description

When using `sls deploy function -f postsHandler` to update an existing function (with layer attached) the layer is being REMOVED from the Lambda. 

As a possible related side note, when I make changes to the postsHandler js file, sometimes it doesn't detect that there are changes and it won't deploy. 

### Service configuration (serverless.yml) content

```yaml
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs18.x
  stage: ${opt:stage, 'dev'}
  region: us-west-2
  vpc:
    securityGroupIds: !Split
      - ','
      - ${param:securityGroupIds, ""}
    subnetIds: !Split
      - ','
      - ${param:subnetIds, ""}
  # stackName: ${self:service}-${param:envName}
  environment:
    REGION: ${self:provider.region}
    STAGE: ${sls:stage}
    ENV_TYPE: ${param:envType}
    S3_BUCKET_NAME: ${param:mediaBucketName, ""}
    S3_URL_EXPIRE_IN_SECONDS: 3600
    SQS_QUEUE_URL: ${param:notificationQueueUrl, ""}
  layers:
    - ${param:servicesSharedUtilsLayerArn}
  httpApi:
    id: ${param:apiGatewayId, ""}
  iam:
    role:
      name: ${self:service}-${sls:stage}-lambda-role
      statements:
        - Effect: 'Allow'
          Action:
            - 'ssm:GetParameter'
            - 's3:GetObject'
            - 's3:GetObjectAcl'
            - 'sqs:SendMessage'
          Resource:
            - '*'

# params: *redacted*

functions:
  postsHandler:
    handler: ./controllers/handlerController.handler
    events:
      # indexPosts
      - httpApi:
          path: /v2/posts
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexActivismPosts
      - httpApi:
          path: /v2/posts/activism
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexMobilePosts
      - httpApi:
          path: /v2/posts/mobile
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexDraftPosts
      - httpApi:
          path: /v2/posts/drafts
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # show
      - httpApi:
          path: /v2/posts/{id}
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # showThread:
      - httpApi:
          path: /v2/posts/{id}/thread
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexPostLocations:
      - httpApi:
          path: /v2/posts/locations
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # create:
      - httpApi:
          path: /v2/posts
          method: POST
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # createUpdate:
      - httpApi:
          path: /v2/posts/{id}
          method: POST
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # update:
      - httpApi:
          path: /v2/posts/{id}
          method: PUT
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # delete:
      - httpApi:
          path: /v2/posts/{id}
          method: DELETE
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexBookmarks:
      - httpApi:
          path: /v2/posts/bookmarks
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # createBookmark:
      - httpApi:
          path: /v2/posts/bookmarks
          method: POST
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # deleteBookmark:
      - httpApi:
          path: /v2/posts/bookmarks/{id}
          method: DELETE
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexLocationsByUser:
      - httpApi:
          path: /v2/locations
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexStaff:
      - httpApi:
          path: /v2/posts/staff
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer
      # indexBookmarkLocations:
      - httpApi:
          path: /v2/posts/bookmarks/locations
          method: GET
          authorizer:
            type: jwt
            id: ${param:jwtAuthorizerId}
            name: cognitoAuthorizer

custom:
  stage: ${sls:stage}
  plugins:
    local:
      - serverless-offline
  serverless-offline:
    httpPort: 6000
    websocketPort: 6001
    lambdaPort: 6002
    ignoreJWTSignature: true
    noAuth: true

plugins: ${self:custom.plugins.${sls:stage}, ""}
```


### Command name and used flags

serverless deploy function -f 

### Command output

```shell
N/A
```


### Environment information

```shell
Framework Core: 3.30.1 (local) 3.30.1 (global)
Plugin: 6.2.3
SDK: 4.3.2
```



Discussion:
Can confirm that I too see this behaviour
Indeed, it's partially a regression introduced by https://github.com/serverless/serverless/pull/11849 (cc @Danwakeem), yet it's also an outcome of a bug which was in function deploy logic from the start.

When resolving configuration for function deployment, we do not take into account layers as configured at `provider.layers` (just those at `functions[].layers`), and with #11849 we reshaped logic to start with an implicitly set empty collection for `Layers` setting, which we remove only if we find that remotely there are also no layers configured (and that mix, removes layer as added via `provider.layers` setting).

Fix is to ensure support for `provider.layers` in [deploy function](https://github.com/serverless/serverless/blob/main/lib/plugins/aws/deploy-function.js) logic.

PR's welcome!
Thank you for the explanation @medikoo - I am going to give a try for PR this weekend.
This regression caused some nasty outages after a serverless upgrade for us. Do we know the version the bug was introduced in, then we can downgrade and pin?
@electric-al I've created a fix at https://github.com/serverless/serverless/pull/11972, it'll be published with next release



Patch:
diff --git a/lib/plugins/aws/deploy-function.js b/lib/plugins/aws/deploy-function.js
index aaf69e5de74..3640e586808 100644
--- a/lib/plugins/aws/deploy-function.js
+++ b/lib/plugins/aws/deploy-function.js
@@ -277,7 +277,7 @@ class AwsDeployFunction {
       // We need to initialize to an empty array so if a layer is removed
       // we will send an empty Layers array in the update call to remove any layers.
       // If there are no layers in the remove config this property will be set to undefined anyway.
-      params.Layers = functionObj.layers || [];
+      params.Layers = functionObj.layers || providerObj.layers || [];
 
       if (!remoteFunctionConfiguration.Layers) {
         remoteFunctionConfiguration.Layers = [];


Test patch:
diff --git a/test/unit/lib/plugins/aws/deploy-function.test.js b/test/unit/lib/plugins/aws/deploy-function.test.js
index 17f4ccde78d..5b9d30de01c 100644
--- a/test/unit/lib/plugins/aws/deploy-function.test.js
+++ b/test/unit/lib/plugins/aws/deploy-function.test.js
@@ -638,6 +638,78 @@ describe('test/unit/lib/plugins/aws/deployFunction.test.js', () => {
     });
   });
 
+  it('should recognize layers at `provider.layers`', async () => {
+    await runServerless({
+      fixture: 'function',
+      command: 'deploy function',
+      options: { function: 'basic' },
+      awsRequestStubMap: {
+        ...awsRequestStubMap,
+        Lambda: {
+          ...awsRequestStubMap.Lambda,
+          getFunction: {
+            Configuration: {
+              LastModified: '2020-05-20T15:34:16.494+0000',
+              PackageType: 'Zip',
+              State: 'Active',
+              LastUpdateStatus: 'Successful',
+            },
+          },
+        },
+      },
+      configExt: {
+        provider: {
+          environment: {
+            ANOTHERVAR: 'anothervalue',
+          },
+          layers: [layerArn, secondLayerArn],
+        },
+        functions: {
+          basic: {
+            kmsKeyArn,
+            description,
+            handler,
+            environment: {
+              VARIABLE: 'value',
+            },
+            name: functionName,
+            memorySize,
+            onError: onErrorHandler,
+            role,
+            timeout,
+            vpc: {
+              securityGroupIds: ['sg-111', 'sg-222'],
+              subnetIds: ['subnet-111', 'subnet-222'],
+            },
+          },
+        },
+      },
+    });
+    expect(updateFunctionConfigurationStub).to.be.calledWithExactly({
+      FunctionName: functionName,
+      KMSKeyArn: kmsKeyArn,
+      Description: description,
+      Handler: handler,
+      Environment: {
+        Variables: {
+          ANOTHERVAR: 'anothervalue',
+          VARIABLE: 'value',
+        },
+      },
+      MemorySize: memorySize,
+      Timeout: timeout,
+      DeadLetterConfig: {
+        TargetArn: onErrorHandler,
+      },
+      Role: role,
+      VpcConfig: {
+        SecurityGroupIds: ['sg-111', 'sg-222'],
+        SubnetIds: ['subnet-111', 'subnet-222'],
+      },
+      Layers: [layerArn, secondLayerArn],
+    });
+  });
+
   it('should update function configuration if configuration changed where all arn layers were removed', async () => {
     await runServerless({
       fixture: 'function',