github.com/mweagle/Sparta@v1.15.0/docs_source/content/reference/apiv2gateway/_index.md (about)

     1  ---
     2  date: 2016-03-09T19:56:50+01:00
     3  pre: "<b>API V2 Gateway</b>"
     4  alwaysopen: false
     5  weight: 100
     6  ---
     7  
     8  # API V2 Gateway
     9  
    10  The API V2 Gateway service provides a way to expose a WebSocket API that is 
    11  supported by a set of Lambda functions. The AWS [blog post](https://aws.amazon.com/blogs/compute/announcing-websocket-apis-in-amazon-api-gateway/) supplies an excellent overview of
    12  the pros and cons of this approach that enables a near real time, pushed-based
    13  application. This section will provide an overview of how to configure a WebSocket
    14  API using Sparta. It is based on the [SpartaWebSocket](https://github.com/mweagle/SpartaWebSocket) sample project.
    15  
    16  ## Payload
    17  
    18  Similar to the AWS blog post, our WebSocket API will transmit messages of the form
    19  
    20  ```json
    21  {
    22      "message":"sendmessage",
    23      "data":"hello world !"
    24  }
    25  ```
    26  
    27  We'll use [ws](https://github.com/hashrocket/ws) to test the API from the command line.
    28  
    29  ## Routes
    30  
    31  The Sparta service consists of three lambda functions:
    32  
    33  * `connectWorld(context.Context, awsEvents.APIGatewayWebsocketProxyRequest) (*wsResponse, error)`
    34  * `disconnectWorld(context.Context, awsEvents.APIGatewayWebsocketProxyRequest) (*wsResponse, error)`
    35  * `sendMessage(context.Context, awsEvents.APIGatewayWebsocketProxyRequest) (*wsResponse, error)`
    36  
    37  Our functions will use the [PROXY](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-create-api-as-simple-proxy) style integration and therefore accept an instance of the [APIGatewayWebsocketProxyRequest](https://godoc.org/github.com/aws/aws-lambda-gevents#APIGatewayWebsocketProxyRequest) type.
    38  
    39  Each function returns a `*wsResponse` instance that satisfies the __PROXY__ response:
    40  
    41  ```go
    42  type wsResponse struct {
    43    StatusCode int    `json:"statusCode"`
    44    Body       string `json:"body"`
    45  }
    46  ```
    47  
    48  ### connectWorld
    49  
    50  The `connectWorld` AWS Lambda function is responsible for saving the incoming _connectionID_ into a dynamically provisioned DynamoDB database so that subsequent _sendMessage_ requests can broadcast to all subscribed parties.
    51  
    52  The table name is advertised in the Lambda function via a user-defined environment variable. The specifics of how that table is provisioned will be addressed in a section below.
    53  
    54  ```go
    55  ...
    56  // Operation
    57  putItemInput := &dynamodb.PutItemInput{
    58    TableName: aws.String(os.Getenv(envKeyTableName)),
    59    Item: map[string]*dynamodb.AttributeValue{
    60      ddbAttributeConnectionID: &dynamodb.AttributeValue{
    61        S: aws.String(request.RequestContext.ConnectionID),
    62       },
    63    },
    64  }
    65  _, putItemErr := dynamoClient.PutItem(putItemInput)
    66  ...
    67  ```
    68  
    69  ### disconnectWorld
    70  
    71  The complement to `connectWorld` is `disconnectWorld` which is responsible for removing the _connectionID_ from the list of registered connections:
    72  
    73  ```go
    74    delItemInput := &dynamodb.DeleteItemInput{
    75      TableName: aws.String(os.Getenv(envKeyTableName)),
    76      Key: map[string]*dynamodb.AttributeValue{
    77        ddbAttributeConnectionID: &dynamodb.AttributeValue{
    78          S: aws.String(connectionID),
    79        },
    80      },
    81    }
    82    _, delItemErr := ddbService.DeleteItem(delItemInput)
    83  ```
    84  
    85  ### sendMessage
    86  
    87  With the `connectWorld` and `disconnectWorld` connection management functions created, the core of the WebSocket API is `sendMessage`. This function is responsible for scanning over the set of registered _connectionIDs_ and forwarding a request to [PostConnectionWithContext](https://godoc.org/github.com/aws/aws-sdk-go/service/apigatewaymanagementapi#ApiGatewayManagementApi.PostToConnectionWithContext). This function sends the message to the registered connections.
    88  
    89  The `sendMessage` function can be broken down into a few sections.
    90  
    91  #### Setup API Gateway Management Instance
    92  
    93  The first requirement is to setup the API Gateway Management service instance using the proper endpoint. The endpoint can be constructed from the incoming [APIGatewayWebsocketProxyRequestContext](https://godoc.org/github.com/aws/aws-lambda-go/events#APIGatewayWebsocketProxyRequestContext) member of the request.
    94  
    95  ```go
    96    endpointURL := fmt.Sprintf("%s/%s",
    97      request.RequestContext.DomainName,
    98      request.RequestContext.Stage)
    99    logger.WithField("Endpoint", endpointURL).Info("API Gateway Endpoint")
   100    dynamoClient := dynamodb.New(sess)
   101      apigwMgmtClient := apigwManagement.New(sess, aws.NewConfig().WithEndpoint(endpointURL))
   102  ```
   103  
   104  #### Validate Input 
   105  
   106  The new step is to unmarshal and validate the incoming JSON request body:
   107  
   108  ```go
   109    // Get the input request...
   110    var objMap map[string]*json.RawMessage
   111    unmarshalErr := json.Unmarshal([]byte(request.Body), &objMap)
   112    if unmarshalErr != nil || objMap["data"] == nil {
   113      return &wsResponse{
   114        StatusCode: 500,
   115        Body:       "Failed to unmarshal request: " + unmarshalErr.Error(),
   116      }, nil
   117    }
   118  ```
   119  
   120  Once we have verified that the input is valid, the final step is to notify all the subscribers.
   121  
   122  #### Scan and Publish
   123  
   124  Once the incoming `data` property is validated, the next step is to scan the DynamoDB table for the registered connections and post a message to each one. Note that the scan callback also attempts to cleanup connections that are no longer valid, but which haven't been cleanly removed.
   125  
   126  ```go
   127    scanCallback := func(output *dynamodb.ScanOutput, lastPage bool) bool {
   128      // Send the message to all the clients
   129      for _, eachItem := range output.Items {
   130        // Get the connectionID
   131        receiverConnection := ""
   132        if eachItem[ddbAttributeConnectionID].S != nil {
   133          receiverConnection = *eachItem[ddbAttributeConnectionID].S
   134        }
   135  
   136        // Post to this connectionID
   137        postConnectionInput := &apigwManagement.PostToConnectionInput{
   138          ConnectionId: aws.String(receiverConnection),
   139          Data:         *objMap["data"],
   140        }
   141        _, respErr := apigwMgmtClient.PostToConnectionWithContext(ctx, postConnectionInput)
   142        if respErr != nil {
   143          if receiverConnection != "" &&
   144            strings.Contains(respErr.Error(), apigwManagement.ErrCodeGoneException) {
   145            // Cleanup in case the connection is stale
   146            go deleteConnection(receiverConnection, dynamoClient)
   147          } else {
   148            logger.WithField("Error", respErr).Warn("Failed to post to connection")
   149          }
   150        }
   151        return true
   152      }
   153      return true
   154    }
   155  
   156    // Scan the connections table
   157    scanInput := &dynamodb.ScanInput{
   158      TableName: aws.String(os.Getenv(envKeyTableName)),
   159    }
   160    scanItemErr := dynamoClient.ScanPagesWithContext(ctx,
   161      scanInput,
   162      scanCallback)
   163    ...
   164  ```
   165  
   166  These three functions are the core of the WebSocket service.
   167  
   168  ## API V2 Gateway Decorator
   169  
   170  The next step is to create the API V2 API object which is comprised of:
   171  
   172  * [Stage](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-stage.html)
   173  * [API](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-api.html)
   174  * [Routes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-route.html)
   175  
   176  There is one Stage and one API per service, but a given service (including this one) may include multiple Routes.
   177  
   178  ```go
   179  // APIv2 Websockets
   180  stage, _ := sparta.NewAPIV2Stage("v1")
   181  stage.Description = "New deploy!"
   182  
   183  apiGateway, _ := sparta.NewAPIV2(sparta.Websocket,
   184    "sample",
   185    "$request.body.message",
   186     stage)
   187  ```
   188  
   189  The `NewAPIV2` creation function requires:
   190  
   191  * The protocol to use (`sparta.Websocket`)
   192  * The name of the API (`sample`)
   193  * The [route selection expression](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-selection-expressions.html#apigateway-websocket-api-route-selection-expressions) that represents a JSONPath selection expression to map input data to the corresponding lambda function.
   194  * The stage
   195  
   196  Once the API is defined, each route is associated with the API as in:
   197  
   198  ```go
   199  apiv2ConnectRoute, _ := apiGateway.NewAPIV2Route("$connect",
   200      lambdaConnect)
   201  apiv2ConnectRoute.OperationName = "ConnectRoute"
   202  ...
   203  apiv2SendRoute, _ := apiGateway.NewAPIV2Route("sendmessage",
   204      lambdaSend)
   205  apiv2SendRoute.OperationName = "SendRoute"
   206  ...
   207  ```
   208  
   209  The `$connect` routeKey is a special [route key value](https://aws.amazon.com/blogs/compute/announcing-websocket-apis-in-amazon-api-gateway/) that is sent when a client first connects to the WebSocket API. See the official [documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-route-keys-connect-disconnect.html) for more information.
   210  
   211  In comparison, the `sendmessage` routeKey value of `sendmessage` means that a payload of the form:
   212  
   213  ```json
   214  {
   215      "message":"sendmessage",
   216      "data":"hello world !"
   217  }
   218  ```
   219  
   220  will trigger the `lambdaSend` function given the parent API's route selection expression of `$request.body.message`.
   221  
   222  ### Additional Privileges
   223  
   224  Because the `lambdaSend` function also needs to invoke the API Gateway Management APIs to broadcast, an additional IAM Privilege must be enabled:
   225  
   226  ```go
   227    var apigwPermissions = []sparta.IAMRolePrivilege{
   228      {
   229        Actions: []string{"execute-api:ManageConnections"},
   230        Resource: gocf.Join("",
   231          gocf.String("arn:aws:execute-api:"),
   232          gocf.Ref("AWS::Region"),
   233          gocf.String(":"),
   234          gocf.Ref("AWS::AccountId"),
   235          gocf.String(":"),
   236          gocf.Ref(apiGateway.LogicalResourceName()),
   237          gocf.String("/*")),
   238      },
   239    }
   240    lambdaSend.RoleDefinition.Privileges = append(lambdaSend.RoleDefinition.Privileges, apigwPermissions...)
   241  ```
   242  
   243  ## Annotating Lambda Functions
   244  
   245  The final configuration step is to use the API gateway to create an instance of the `APIV2GatewayDecorator`. This decorator is responsible for:
   246  
   247  * Provisioning the DynamoDB table.
   248  * Ensuring DynamoDB CRUD permissions for all the AWS Lambda functions.
   249  * Publishing the table name into the Lambda function's Environment block.
   250  * Adding the WebSocket `wss://...` URL to the Stack's Outputs.
   251  
   252  The decorator is created by a call to `NewConnectionTableDecorator` which accepts:
   253  
   254  * The environment variable to populate with the dynamically assigned DynamoDB table
   255  * The DynamoDB attribute name to use to store the connectionID
   256  * The read capacity units
   257  * The write capacity units
   258  
   259  For instance:
   260  
   261  ```go
   262    decorator, _ := apiGateway.NewConnectionTableDecorator(envKeyTableName,
   263        ddbAttributeConnectionID,
   264        5,
   265        5)
   266  
   267    var lambdaFunctions []*sparta.LambdaAWSInfo
   268    lambdaFunctions = append(lambdaFunctions,
   269        lambdaConnect,
   270        lambdaDisconnect,
   271        lambdaSend)
   272    decorator.AnnotateLambdas(lambdaFunctions)
   273  ```
   274  
   275  ## Provision
   276  
   277  With everything defined, provide the API V2 Decorator as a Workflow hook as in:
   278  
   279  ```go
   280    // Set everything up and run it...
   281    workflowHooks := &sparta.WorkflowHooks{
   282      ServiceDecorators: []sparta.ServiceDecoratorHookHandler{decorator},
   283    }
   284    err := sparta.MainEx(awsName,
   285      "Sparta application that demonstrates API v2 Websocket support",
   286      lambdaFunctions,
   287      apiGateway,
   288      nil,
   289      workflowHooks,
   290      false)
   291  ```
   292  
   293  and then provision the application:
   294  
   295  ```bash
   296  go run main.go provision --s3Bucket $S3_BUCKET --noop
   297  INFO[0000] ════════════════════════════════════════════════
   298  INFO[0000] ╔═╗╔═╗╔═╗╦═╗╔╦╗╔═╗   Version : 1.9.4
   299  INFO[0000] ╚═╗╠═╝╠═╣╠╦╝ ║ ╠═╣   SHA     : cfd44e2
   300  INFO[0000] ╚═╝╩  ╩ ╩╩╚═ ╩ ╩ ╩   Go      : go1.12.6
   301  INFO[0000] ════════════════════════════════════════════════
   302  INFO[0000] Service: SpartaWebSocket-123412341234         LinkFlags= Option=provision UTC="2019-07-25T05:26:57Z"
   303  INFO[0000] ════════════════════════════════════════════════
   304  INFO[0000] Using `git` SHA for StampedBuildID            Command="git rev-parse HEAD" SHA=6b26f8e645e9d58c1b678e46576e19bbc29886c0
   305  INFO[0000] Provisioning service                          BuildID=6b26f8e645e9d58c1b678e46576e19bbc29886c0 CodePipelineTrigger= InPlaceUpdates=false NOOP=false Tags=
   306  INFO[0000] Verifying IAM Lambda execution roles
   307  INFO[0000] IAM roles verified                            Count=3
   308  INFO[0000] Checking S3 versioning                        Bucket=weagle VersioningEnabled=true
   309  INFO[0000] Checking S3 region                            Bucket=weagle Region=us-west-2
   310  INFO[0000] Running `go generate`
   311  INFO[0000] Compiling binary                              Name=Sparta.lambda.amd64
   312  INFO[0002] Creating code ZIP archive for upload          TempName=./.sparta/SpartaWebSocket_123412341234-code.zip
   313  INFO[0002] Lambda code archive size                      Size="23 MB"
   314  INFO[0002] Uploading local file to S3                    Bucket=weagle Key=SpartaWebSocket-123412341234/SpartaWebSocket_123412341234-code.zip Path=./.sparta/SpartaWebSocket_123412341234-code.zip Size="23 MB"
   315  INFO[0011] Calling WorkflowHook                          ServiceDecoratorHook= WorkflowHookContext="map[]"
   316  INFO[0011] Uploading local file to S3                    Bucket=weagle Key=SpartaWebSocket-123412341234/SpartaWebSocket_123412341234-cftemplate.json Path=./.sparta/SpartaWebSocket_123412341234-cftemplate.json Size="14 kB"
   317  INFO[0011] Creating stack                                StackID="arn:aws:cloudformation:us-west-2:123412341234:stack/SpartaWebSocket-123412341234/d8a405b0-ae9c-11e9-a05a-0a1528792fce"
   318  INFO[0122] CloudFormation Metrics ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
   319  ...
   320  INFO[0122] Stack Outputs ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
   321  INFO[0122]     APIGatewayURL                             Description="API Gateway Websocket URL" Value="wss://gu4vmnia27.execute-api.us-west-2.amazonaws.com/v1"
   322  INFO[0122] Stack provisioned                             CreationTime="2019-07-25 05:27:08.687 +0000 UTC" StackId="arn:aws:cloudformation:us-west-2:123412341234:stack/SpartaWebSocket-123412341234/d8a405b0-ae9c-11e9-a05a-0a1528792fce" StackName=SpartaWebSocket-123412341234
   323  INFO[0122] ════════════════════════════════════════════════
   324  INFO[0122] SpartaWebSocket-123412341234 Summary
   325  INFO[0122] ════════════════════════════════════════════════
   326  INFO[0122] Verifying IAM roles                           Duration (s)=0
   327  INFO[0122] Verifying AWS preconditions                   Duration (s)=0
   328  INFO[0122] Creating code bundle                          Duration (s)=1
   329  INFO[0122] Uploading code                                Duration (s)=9
   330  INFO[0122] Ensuring CloudFormation stack                 Duration (s)=112
   331  INFO[0122] Total elapsed time                            Duration (s)=122
   332  ```
   333  
   334  ## Test
   335  
   336  With the API Gateway deployed, the last step is to test it. Download and install the [ws](go get -u github.com/hashrocket/ws
   337  ) tool:
   338  
   339  ```bash
   340  go get -u github.com/hashrocket/ws
   341  ```
   342  
   343  then connect to your new API and send a message as in:
   344  
   345  ```bash
   346  22:31 $ ws wss://gu4vmnia27.execute-api.us-west-2.amazonaws.com/v1
   347  > {"message":"sendmessage", "data":"hello world !"}
   348  < "hello world !"
   349  ```
   350  
   351  You can also send messages with [Firecamp](https://chrome.google.com/webstore/detail/firecamp-a-campsite-for-d/eajaahbjpnhghjcdaclbkeamlkepinbl?hl=en), a Chrome extension, and send messages between your `ws` session and the web (or vice versa).
   352  
   353  ## Conclusion
   354  
   355  While a production ready application would likely need to include authentication and authorization, this is the beginnings of a full featured WebSocket service in fewer than 200 lines of application code:
   356  
   357  ```bash
   358  -------------------------------------------------------------------------------
   359  Language                     files          blank        comment           code
   360  -------------------------------------------------------------------------------
   361  Go                               1             21             52            183
   362  Markdown                         1              0              2              0
   363  -------------------------------------------------------------------------------
   364  TOTAL                            2             21             54            183
   365  -------------------------------------------------------------------------------
   366  ```
   367  
   368  Remember to terminate the stack when you're done to avoid any unintentional costs!
   369  
   370  ## References
   371  
   372  * The SpartaWebSocket application is modeled after the [https://github.com/aws-samples/simple-websockets-chat-app](https://github.com/aws-samples/simple-websockets-chat-app) sample.