Repository: serverless/serverless (JavaScript)
Date: 2019-06-28 18:03:22+00:00
Task type: Bug Fix

Issue:
ALB event Target Group names are not unique across services
# This is a Bug Report

## Description

You cannot define function with alb event in different services using the same function name. If you try to do it, the second service deployment will fail saying the a Target Group with that name already exists.

To reproduce the bug, follow these steps:
- Deploy `test` service below. 
- Update `test2` service by replacing `<listenerArn>` with the `ListenerArn` output from `test` service deploy.
- Deploy `test2` service, which should fail.

```javascript
module.exports.hello = async () => ({
  isBase64Encoded: false,
  statusCode: 200,
  statusDescription: "200 OK",
  headers: { "Content-Type": "application/json" },
  body: "Hello from Lambda"
});
```

```yaml
service: test

provider:
  name: aws
  runtime: nodejs10.x
  versionFunctions: false

package:
  excludeDevDependencies: false
  exclude:
  - '**/*'
  include:
  - ./hello.js

functions:
  hello:
    handler: hello.hello
    events:
    - alb:
        listenerArn: { Ref: HTTPListener }
        priority: 1
        conditions:
          path: /hello

custom:
  defaultRegion: us-east-1
  defaultStage: dev
  region: ${opt:region, self:custom.defaultRegion}
  stage: ${opt:stage, self:custom.defaultStage}
  objectPrefix: '${self:service}-${self:custom.stage}'

resources:
  Outputs:
    LoadBalancerDNSName:
      Value: { 'Fn::GetAtt': [ LoadBalancer, 'DNSName' ] }
      Export: { Name: '${self:custom.objectPrefix}-LoadBalancerDNSName' }
    ListenerArn:
      Value: { 'Ref': HTTPListener }
  Resources:
    VPC:
      Type: 'AWS::EC2::VPC'
      Properties:
        CidrBlock: 172.31.0.0/16
        EnableDnsHostnames: true
    InternetGateway:
      Type: 'AWS::EC2::InternetGateway'
    VPCGatewayAttachment:
      Type: 'AWS::EC2::VPCGatewayAttachment'
      Properties:
        VpcId: { Ref: VPC }
        InternetGatewayId: { Ref: InternetGateway }
    RouteTable:
      Type: 'AWS::EC2::RouteTable'
      Properties:
        VpcId: { Ref: VPC }
    InternetRoute:
      Type: 'AWS::EC2::Route'
      DependsOn: VPCGatewayAttachment
      Properties:
        DestinationCidrBlock: 0.0.0.0/0
        GatewayId: { Ref: InternetGateway }
        RouteTableId: { Ref: RouteTable }
    SubnetA:
      Type: 'AWS::EC2::Subnet'
      Properties:
        AvailabilityZone: '${self:custom.region}a'
        CidrBlock: 172.31.0.0/20
        MapPublicIpOnLaunch: false
        VpcId: { Ref: VPC }
    SubnetB:
      Type: 'AWS::EC2::Subnet'
      Properties:
        AvailabilityZone: '${self:custom.region}b'
        CidrBlock: 172.31.16.0/20
        MapPublicIpOnLaunch: false
        VpcId: { Ref: VPC }
    SubnetARouteTableAssociation:
      Type: 'AWS::EC2::SubnetRouteTableAssociation'
      Properties:
        SubnetId: { Ref: SubnetA }
        RouteTableId: { Ref: RouteTable }
    SubnetBRouteTableAssociation:
      Type: 'AWS::EC2::SubnetRouteTableAssociation'
      Properties:
        SubnetId: { Ref: SubnetB }
        RouteTableId: { Ref: RouteTable }
    SecurityGroup:
      Type: 'AWS::EC2::SecurityGroup'
      Properties:
        GroupName: 'http-https'
        GroupDescription: 'HTTPS / HTTPS inbound; Nothing outbound'
        VpcId: { Ref: VPC }
        SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
        -
          IpProtocol: tcp
          FromPort: '443'
          ToPort: '443'
          CidrIp: 0.0.0.0/0
        SecurityGroupEgress:
        -
          IpProtocol: -1
          FromPort: '1'
          ToPort: '1'
          CidrIp: 127.0.0.1/32
    LoadBalancer:
      Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
      Properties:
        Type: 'application'
        Name: '${self:custom.objectPrefix}'
        IpAddressType: 'ipv4'
        Scheme: 'internet-facing'
        LoadBalancerAttributes:
        - { Key: 'deletion_protection.enabled', Value: false }
        - { Key: 'routing.http2.enabled', Value: false }
        - { Key: 'access_logs.s3.enabled', Value: false }
        SecurityGroups:
        - { Ref: SecurityGroup }
        Subnets:
        - { Ref: SubnetA }
        - { Ref: SubnetB }
    HTTPListener:
      Type: 'AWS::ElasticLoadBalancingV2::Listener'
      Properties:
        LoadBalancerArn: { Ref: LoadBalancer }
        Port: 80
        Protocol: 'HTTP'
        DefaultActions:
        -
          Type: 'fixed-response'
          Order: 1
          FixedResponseConfig:
            StatusCode: 404
            ContentType: 'application/json'
            MessageBody: '{ "not": "found" }'
```

```yaml
service: test2

provider:
  name: aws
  runtime: nodejs10.x
  versionFunctions: false

package:
  excludeDevDependencies: false
  exclude:
  - '**/*'
  include:
  - ./hello.js

functions:
  hello:
    handler: hello.hello
    events:
    - alb:
        listenerArn: <listenerArn>
        priority: 2
        conditions:
          path: /hello

custom:
  defaultRegion: us-east-1
  defaultStage: dev
  region: ${opt:region, self:custom.defaultRegion}
  stage: ${opt:stage, self:custom.defaultStage}
``

## Additional Data


Issue discussion:

Solution patch:
diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js
index d77336cd916..3ef221a4892 100644
--- a/lib/plugins/aws/lib/naming.js
+++ b/lib/plugins/aws/lib/naming.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const _ = require('lodash');
+const crypto = require('crypto');
 
 /**
  * A centralized naming object that provides naming standards for the AWS provider plugin.  The
@@ -381,6 +382,17 @@ module.exports = {
   getAlbTargetGroupLogicalId(functionName) {
     return `${this.getNormalizedFunctionName(functionName)}AlbTargetGroup`;
   },
+  getAlbTargetGroupNameTagValue(functionName) {
+    return `${
+      this.provider.serverless.service.service
+    }-${functionName}-${this.provider.getStage()}`;
+  },
+  getAlbTargetGroupName(functionName) {
+    return crypto
+      .createHash('md5')
+      .update(this.getAlbTargetGroupNameTagValue(functionName))
+      .digest('hex');
+  },
   getAlbListenerRuleLogicalId(functionName, idx) {
     return `${this.getNormalizedFunctionName(functionName)}AlbListenerRule${idx}`;
   },
diff --git a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js
index ea6ca220736..9d9e8176382 100644
--- a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js
+++ b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js
@@ -23,7 +23,13 @@ module.exports = {
                 },
               },
             ],
-            Name: `${functionName}-${this.provider.getStage()}`,
+            Name: this.provider.naming.getAlbTargetGroupName(functionName),
+            Tags: [
+              {
+                Key: 'Name',
+                Value: this.provider.naming.getAlbTargetGroupNameTagValue(functionName),
+              },
+            ],
           },
           DependsOn: [lambdaPermissionLogicalId],
         },


Test patch:
diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js
index 58b9e6d01e4..87507e43ead 100644
--- a/lib/plugins/aws/lib/naming.test.js
+++ b/lib/plugins/aws/lib/naming.test.js
@@ -703,4 +703,22 @@ describe('#naming()', () => {
       );
     });
   });
+
+  describe('#getAlbTargetGroupName()', () => {
+    it('should return a unique identifier based on the service name, function name and stage', () => {
+      serverless.service.service = 'myService';
+      expect(sdk.naming.getAlbTargetGroupName('functionName')).to.equal(
+        '1ea0b85512f1943fbae9f40a0451d718'
+      );
+    });
+  });
+
+  describe('#getAlbTargetGroupNameTagValue()', () => {
+    it('should return the composition of service name, function name and stage', () => {
+      serverless.service.service = 'myService';
+      expect(sdk.naming.getAlbTargetGroupNameTagValue('functionName')).to.equal(
+        `${serverless.service.service}-functionName-${sdk.naming.provider.getStage()}`
+      );
+    });
+  });
 });
diff --git a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js
index 3280c1757da..0f97c75cea8 100644
--- a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js
+++ b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js
@@ -54,7 +54,7 @@ describe('#compileTargetGroups()', () => {
     expect(resources.FirstAlbTargetGroup).to.deep.equal({
       Type: 'AWS::ElasticLoadBalancingV2::TargetGroup',
       Properties: {
-        Name: 'first-dev',
+        Name: 'd370cffdc0b94175c8239183d0b344a4',
         TargetType: 'lambda',
         Targets: [
           {
@@ -63,13 +63,19 @@ describe('#compileTargetGroups()', () => {
             },
           },
         ],
+        Tags: [
+          {
+            Key: 'Name',
+            Value: 'some-service-first-dev',
+          },
+        ],
       },
       DependsOn: ['FirstLambdaPermissionAlb'],
     });
     expect(resources.SecondAlbTargetGroup).to.deep.equal({
       Type: 'AWS::ElasticLoadBalancingV2::TargetGroup',
       Properties: {
-        Name: 'second-dev',
+        Name: '33af5fa54e565b8aca63f904de895aab',
         TargetType: 'lambda',
         Targets: [
           {
@@ -78,6 +84,12 @@ describe('#compileTargetGroups()', () => {
             },
           },
         ],
+        Tags: [
+          {
+            Key: 'Name',
+            Value: 'some-service-second-dev',
+          },
+        ],
       },
       DependsOn: ['SecondLambdaPermissionAlb'],
     });