├── CODE_OF_CONDUCT.md ├── .vscode └── settings.json ├── go.mod ├── LICENSE ├── connect ├── Makefile └── connect.go ├── publish ├── Makefile └── publish.go ├── disconnect ├── Makefile └── disconnect.go ├── .editorconfig ├── .gitignore ├── lib ├── logger │ └── instance.go ├── apigw │ ├── response.go │ ├── client.go │ └── ws │ │ └── envelop.go └── redis │ └── client.go ├── Makefile ├── README.md ├── CONTRIBUTING.md ├── go.sum └── template.yml /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "CNAME", 4 | "CPUs", 5 | "Datapoints", 6 | "Hostnames", 7 | "SADD", 8 | "SMEMBERS", 9 | "SREM", 10 | "aerr", 11 | "apigw", 12 | "cerr", 13 | "servicelookup", 14 | "wscat" 15 | ] 16 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module com.aws-samples/apigateway.websockets.golang 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.18.0 7 | github.com/aws/aws-sdk-go-v2 v0.24.0 8 | github.com/jmespath/go-jmespath v0.3.0 // indirect 9 | github.com/mediocregopher/radix/v3 v3.5.2 10 | github.com/pkg/errors v0.9.1 // indirect 11 | go.uber.org/zap v1.15.0 12 | ) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /connect/Makefile: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | .PHONY: clean build 24 | 25 | clean: 26 | rm -rfv bin 27 | 28 | build: 29 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o $(ARTIFACTS_DIR)/bootstrap 30 | -------------------------------------------------------------------------------- /publish/Makefile: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | .PHONY: clean build 24 | 25 | clean: 26 | rm -rfv bin 27 | 28 | build: 29 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o $(ARTIFACTS_DIR)/bootstrap 30 | -------------------------------------------------------------------------------- /disconnect/Makefile: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | .PHONY: clean build 24 | 25 | clean: 26 | rm -rfv bin 27 | 28 | build: 29 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o $(ARTIFACTS_DIR)/bootstrap 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | root = true 23 | 24 | [*] 25 | charset = utf-8 26 | indent_size = 2 27 | end_of_line = lf 28 | indent_style = space 29 | insert_final_newline = true 30 | trim_trailing_whitespace = true 31 | 32 | [*.go] 33 | indent_size = 4 34 | 35 | [Makefile] 36 | indent_size = 4 37 | indent_style = tab 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | *.exe 23 | *.exe~ 24 | *.dll 25 | *.so 26 | *.dylib 27 | 28 | # Test binary, build with `go test -c` and test data 29 | *.test 30 | **/_test 31 | 32 | # Output of the go coverage tool, specifically when used with LiteIDE 33 | *.out 34 | 35 | # SAM generated directories and files 36 | .aws-sam 37 | packaged.yml 38 | 39 | # AWS Lambda invoke response 40 | out.json 41 | 42 | # All build directories and log files 43 | **/bin 44 | **/*.log 45 | **/*.DS_Store 46 | -------------------------------------------------------------------------------- /lib/logger/instance.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Package logger provides a singleton logging instance which is used across the same AWS Lambda execution contexts. 24 | // Reusing the client across execution contexts provides some performance enhancements and ensures the logger 25 | // configuration is consistent across all handlers. 26 | package logger 27 | 28 | import "go.uber.org/zap" 29 | 30 | // Instance used across Lambda invocations for this execution context. 31 | var Instance *zap.Logger 32 | 33 | // initialize the logger used across Lambda invocations for the same execution context. 34 | func init() { 35 | Instance, _ = zap.NewProduction() 36 | } 37 | -------------------------------------------------------------------------------- /lib/apigw/response.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Package apigw provides common resources for working with Amazon API Gateway 24 | package apigw 25 | 26 | import ( 27 | "net/http" 28 | 29 | "github.com/aws/aws-lambda-go/events" 30 | ) 31 | 32 | // Response is a typedef for the response type provided by the SDK. 33 | type Response = events.APIGatewayProxyResponse 34 | 35 | // InternalServerErrorResponse returns an Amazon API Gateway Proxy Response configured with the correct HTTP status 36 | // code. 37 | func InternalServerErrorResponse() Response { 38 | return Response{StatusCode: http.StatusInternalServerError} 39 | } 40 | 41 | // BadRequestResponse returns an Amazon API Gateway Proxy Response configured with the correct HTTP status code. 42 | func BadRequestResponse() Response { 43 | return Response{StatusCode: http.StatusBadRequest} 44 | } 45 | 46 | // OkResponse returns an Amazon API Gateway Proxy Response configured with the correct HTTP status code. 47 | func OkResponse() Response { 48 | return Response{StatusCode: http.StatusOK} 49 | } 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | .PHONY: check clean test build deploy 24 | 25 | test: 26 | go test -v ./... 27 | 28 | clean: 29 | $(MAKE) -C publish clean 30 | $(MAKE) -C connect clean 31 | $(MAKE) -C disconnect clean 32 | 33 | build: clean 34 | @echo "building handlers for aws lambda" 35 | sam build 36 | 37 | build-ConnectFunction: 38 | @echo "building handler for aws lambda" 39 | $(MAKE) -C connect build 40 | 41 | build-DisconnectFunction: 42 | @echo "building handler for aws lambda" 43 | $(MAKE) -C disconnect build 44 | 45 | build-PublishFunction: 46 | @echo "building handler for aws lambda" 47 | $(MAKE) -C publish build 48 | 49 | deploy: check 50 | @echo "deploying infrastructure and code" 51 | sam package --output-template-file packaged.yml --s3-bucket "${bucket}" 52 | sam deploy --no-fail-on-empty-changeset \ 53 | --stack-name "${stack}" \ 54 | --template-file packaged.yml \ 55 | --capabilities CAPABILITY_IAM 56 | 57 | check: 58 | ifndef bucket 59 | $(error bucket was not provided) 60 | endif 61 | ifndef stack 62 | $(error stack was not provided) 63 | endif 64 | -------------------------------------------------------------------------------- /lib/apigw/client.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Package apigw provides common resources for working with Amazon API Gateway 24 | package apigw 25 | 26 | import ( 27 | "net/url" 28 | 29 | "github.com/aws/aws-sdk-go-v2/aws" 30 | "github.com/aws/aws-sdk-go-v2/service/apigatewaymanagementapi" 31 | ) 32 | 33 | // NewAPIGatewayManagementClient creates a new API Gateway Management Client instance from the provided parameters. The 34 | // new client will have a custom endpoint that resolves to the application's deployed API. 35 | func NewAPIGatewayManagementClient(cfg *aws.Config, domain, stage string) *apigatewaymanagementapi.Client { 36 | cp := cfg.Copy() 37 | cp.EndpointResolver = aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) { 38 | if service != "execute-api" { 39 | return cfg.EndpointResolver.ResolveEndpoint(service, region) 40 | } 41 | 42 | var endpoint url.URL 43 | endpoint.Path = stage 44 | endpoint.Host = domain 45 | endpoint.Scheme = "https" 46 | return aws.Endpoint{ 47 | SigningRegion: region, 48 | URL: endpoint.String(), 49 | }, nil 50 | }) 51 | 52 | return apigatewaymanagementapi.New(cp) 53 | } 54 | -------------------------------------------------------------------------------- /lib/apigw/ws/envelop.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Package ws provides common resources for working with Amazon API Gateway WebSockets 24 | package ws 25 | 26 | import "encoding/json" 27 | 28 | // InputEnvelop defines the expected structure for incoming messages sent over the WebSocket connection. The envelop 29 | // provides additional metadata in addition to the message data. 30 | type InputEnvelop struct { 31 | Echo bool `json:"echo"` 32 | Type int `json:"type"` 33 | Data json.RawMessage `json:"data"` 34 | } 35 | 36 | // Decode decodes and populates the InputEnvelop from the provided bytes. 37 | func (e *InputEnvelop) Decode(data []byte) (*InputEnvelop, error) { 38 | err := json.Unmarshal(data, e) 39 | return e, err 40 | } 41 | 42 | // OutputEnvelop defines the structure for messages sent over the WebSocket connection from the backend service. The 43 | // envelop provides additional metadata in addition to the message data. 44 | type OutputEnvelop struct { 45 | Type int `json:"type"` 46 | Data json.RawMessage `json:"data"` 47 | Received int64 `json:"received"` 48 | } 49 | 50 | // Encode encodes the OutputEnvelop as JSON. The output is suitable for sending over the wire. 51 | func (e *OutputEnvelop) Encode() ([]byte, error) { 52 | return json.Marshal(e) 53 | } 54 | -------------------------------------------------------------------------------- /lib/redis/client.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Package redis provides a singleton Redis client instance which is used across the same AWS Lambda execution contexts. 24 | // Reusing the client across execution contexts provides some performance enhancements due to reusing the underlying 25 | // connection to the Redis cluster. 26 | package redis 27 | 28 | import ( 29 | "fmt" 30 | "net" 31 | 32 | "com.aws-samples/apigateway.websockets.golang/lib/logger" 33 | "github.com/mediocregopher/radix/v3" 34 | "go.uber.org/zap" 35 | ) 36 | 37 | // Client is the single client instance shared across the same Lambda execution contexts. 38 | var Client *radix.Pool 39 | 40 | func init() { 41 | cname, servers, err := net.LookupSRV("redis", "tcp", "service.internal") 42 | if err != nil { 43 | logger.Instance.Panic("unable to resolve redis srv record", zap.Error(err)) 44 | } 45 | 46 | if len(servers) == 0 { 47 | logger.Instance.Panic("unable to resolve redis srv record") 48 | } 49 | 50 | logger.Instance.Info("redis srv record", 51 | zap.String("cname", cname), 52 | zap.Uint16("port", servers[0].Port), 53 | zap.String("target", servers[0].Target), 54 | zap.Uint16("weight", servers[0].Weight), 55 | zap.Uint16("priority", servers[0].Priority)) 56 | 57 | addr := net.JoinHostPort(servers[0].Target, fmt.Sprintf("%d", servers[0].Port)) 58 | Client, err = radix.NewPool("tcp", addr, 1) 59 | if err != nil { 60 | logger.Instance.Panic("unable to create redis connection pool", zap.Error(err)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /connect/connect.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "context" 27 | 28 | "com.aws-samples/apigateway.websockets.golang/lib/apigw" 29 | "com.aws-samples/apigateway.websockets.golang/lib/logger" 30 | "com.aws-samples/apigateway.websockets.golang/lib/redis" 31 | "github.com/aws/aws-lambda-go/events" 32 | 33 | "github.com/aws/aws-lambda-go/lambda" 34 | radix "github.com/mediocregopher/radix/v3" 35 | "go.uber.org/zap" 36 | ) 37 | 38 | func main() { 39 | lambda.Start(handler) 40 | } 41 | 42 | // handler receives a synchronous invocation from API Gateway when a new WebSocket connection is created for the 43 | // application's API. The connection details are cached in the application's Redis cache which makes the connection 44 | // available to the other application components. 45 | func handler(_ context.Context, req *events.APIGatewayWebsocketProxyRequest) (apigw.Response, error) { 46 | defer func() { 47 | _ = logger.Instance.Sync() 48 | }() 49 | 50 | logger.Instance.Info("websocket connect", 51 | zap.String("requestId", req.RequestContext.RequestID), 52 | zap.String("connectionId", req.RequestContext.ConnectionID)) 53 | 54 | var result string 55 | err := redis.Client.Do(radix.Cmd(&result, "SADD", "connections", req.RequestContext.ConnectionID)) 56 | if err != nil { 57 | logger.Instance.Error("failed to cache connection details", 58 | zap.String("requestId", req.RequestContext.RequestID), 59 | zap.String("connectionId", req.RequestContext.ConnectionID), 60 | zap.Error(err)) 61 | 62 | return apigw.InternalServerErrorResponse(), err 63 | } 64 | 65 | logger.Instance.Info("websocket connection cached", 66 | zap.String("result", result), 67 | zap.String("requestId", req.RequestContext.RequestID), 68 | zap.String("connectionId", req.RequestContext.ConnectionID)) 69 | 70 | return apigw.OkResponse(), nil 71 | } 72 | -------------------------------------------------------------------------------- /disconnect/disconnect.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "context" 27 | 28 | "com.aws-samples/apigateway.websockets.golang/lib/apigw" 29 | "com.aws-samples/apigateway.websockets.golang/lib/logger" 30 | "com.aws-samples/apigateway.websockets.golang/lib/redis" 31 | "github.com/aws/aws-lambda-go/events" 32 | 33 | "github.com/aws/aws-lambda-go/lambda" 34 | radix "github.com/mediocregopher/radix/v3" 35 | "go.uber.org/zap" 36 | ) 37 | 38 | func main() { 39 | lambda.Start(handler) 40 | } 41 | 42 | // handler receives a synchronous invocation from API Gateway when a new connection has been disconnected from the 43 | // application's API. The connection details are removed in the application's Redis cache which cleans up the connection 44 | // details. This handler is not guaranteed to be called when the WebSocket connection is closed. 45 | func handler(_ context.Context, req *events.APIGatewayWebsocketProxyRequest) (apigw.Response, error) { 46 | defer func() { 47 | _ = logger.Instance.Sync() 48 | }() 49 | 50 | logger.Instance.Info("websocket disconnect", 51 | zap.String("requestId", req.RequestContext.RequestID), 52 | zap.String("connectionId", req.RequestContext.ConnectionID)) 53 | 54 | var result string 55 | err := redis.Client.Do(radix.Cmd(&result, "SREM", "connections", req.RequestContext.ConnectionID)) 56 | if err != nil { 57 | logger.Instance.Error("failed to delete connection details from cache", 58 | zap.String("requestId", req.RequestContext.RequestID), 59 | zap.String("connectionId", req.RequestContext.ConnectionID), 60 | zap.Error(err)) 61 | 62 | return apigw.InternalServerErrorResponse(), err 63 | } 64 | 65 | logger.Instance.Info("websocket connection deleted from cache", 66 | zap.String("result", result), 67 | zap.String("requestId", req.RequestContext.RequestID), 68 | zap.String("connectionId", req.RequestContext.ConnectionID)) 69 | 70 | return apigw.OkResponse(), nil 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon API Gateway WebSockets with Amazon ElastiCache for Redis 2 | 3 | This project contains a reference implementation for using AWS VPC, Amazon API Gateway WebSockets, AWS Lambda, and Amazon ElastiCache for Redis. 4 | 5 | Three AWS Lambda handlers are included in the project: 6 | 7 | - **ConnectFunction**: Invoked by API Gateway when a new WebSocket connection is established. The connection information is cached in the ElastiCache for Redis instance. 8 | 9 | - **DisconnectFunction**: Invoked by API Gateway when a WebSocket connection is terminated. The connection information is removed from the ElastiCache for Redis instance. 10 | 11 | - **PublishFunction**: Invoked by API Gateway when data is sent from the client over the WebSocket connection. The data is "published" to all connected clients. 12 | 13 | ## Building and Deploying 14 | 15 | ### Compilation 16 | 17 | The AWS Lambda handlers are written in Go. See the following link for more information about the Go programming language including installation instructions. 18 | 19 | Go >= 1.13 is required. 20 | 21 | 22 | 23 | Compilation of the AWS Lambda handlers is managed with the included Makefile. The Makefile ensures the binaries are cross-compiled to run in AWS Lambda. Run the following command to build the binaries for deployment. 24 | 25 | ```bash 26 | make clean build 27 | ``` 28 | 29 | ### Deploying 30 | 31 | Deployments are managed using the AWS Serverless Application Model. See the following links for more information about AWS SAM including installation instructions. 32 | 33 | 34 | 35 | 36 | 37 | The included Makefile includes a target for deploying the infrastructure and code using AWS SAM. The following example demonstrates how to deploy the infrastructure and code. 38 | 39 | ```bash 40 | AWS_PROFILE={profile} AWS_DEFAULT_REGION={region} make bucket={bucket} stack={stack name} deploy 41 | ``` 42 | 43 | ## Using wscat for Testing 44 | 45 | 46 | 47 | Query the deployed WebSocket endpoint by running the following command: 48 | 49 | ```bash 50 | aws cloudformation describe-stacks --stack-name {stack name} --query "Stacks[0].Outputs[?OutputKey=='WebSocketEndpoint'].OutputValue" --output text 51 | ``` 52 | 53 | Now knowing the WebSocket endpoint, wscat can be used to test the deployment. Establish a WebSocket connection with the following command, and the endpoint value from the previous step: 54 | 55 | ```bash 56 | wscat -c {endpoint} 57 | ``` 58 | 59 | The "backend service" only accepts messages which adhere to the below schema. Use the following format to publish a message to all connected clients: 60 | 61 | ```json 62 | { "echo": false, "type": 99, "data": "data to publish" } 63 | ``` 64 | 65 | ## Security 66 | 67 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 68 | 69 | ## License 70 | 71 | This library is licensed under the MIT-0 License. See the LICENSE file. 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/aws/aws-lambda-go v1.18.0 h1:13AfxzFoPlFjOzXHbRnKuTbteCzHbu4YQgKONNhWcmo= 4 | github.com/aws/aws-lambda-go v1.18.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw= 5 | github.com/aws/aws-sdk-go-v2 v0.24.0 h1:R0lL0krk9EyTI1vmO1ycoeceGZotSzCKO51LbPGq3rU= 6 | github.com/aws/aws-sdk-go-v2 v0.24.0/go.mod h1:2LhT7UgHOXK3UXONKI5OMgIyoQL6zTAw/jwIeX6yqzw= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 12 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 13 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 15 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 16 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 17 | github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= 18 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 19 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 20 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 21 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 22 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 23 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 24 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 25 | github.com/mediocregopher/radix/v3 v3.5.2 h1:A9u3G7n4+fWmDZ2ZDHtlK+cZl4q55T+7RjKjR0/MAdk= 26 | github.com/mediocregopher/radix/v3 v3.5.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= 27 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 28 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 29 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 30 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 34 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 35 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 38 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 39 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 40 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 41 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 42 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 43 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 44 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 45 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 46 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 47 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 48 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 49 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 50 | go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= 51 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 52 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 53 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 54 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 55 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 56 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 57 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 58 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 68 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 69 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 70 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 71 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 74 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 76 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 79 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 81 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 82 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 83 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 84 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 85 | -------------------------------------------------------------------------------- /publish/publish.go: -------------------------------------------------------------------------------- 1 | // MIT No Attribution 2 | 3 | // Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "context" 27 | "errors" 28 | "runtime" 29 | "sync" 30 | "time" 31 | 32 | "com.aws-samples/apigateway.websockets.golang/lib/apigw" 33 | "com.aws-samples/apigateway.websockets.golang/lib/apigw/ws" 34 | "com.aws-samples/apigateway.websockets.golang/lib/logger" 35 | "com.aws-samples/apigateway.websockets.golang/lib/redis" 36 | "github.com/aws/aws-lambda-go/events" 37 | "github.com/aws/aws-sdk-go-v2/aws" 38 | "github.com/aws/aws-sdk-go-v2/aws/awserr" 39 | "github.com/aws/aws-sdk-go-v2/aws/external" 40 | 41 | "github.com/aws/aws-lambda-go/lambda" 42 | "github.com/aws/aws-sdk-go-v2/service/apigatewaymanagementapi" 43 | radix "github.com/mediocregopher/radix/v3" 44 | "go.uber.org/zap" 45 | ) 46 | 47 | // Stack is a simple thread-safe Pop only stack implementation which allows workers to pull work from the stack. 48 | type Stack struct { 49 | mu sync.Mutex 50 | elements []string 51 | } 52 | 53 | // Pop pops the next item from the stack and returns it or returns an error if the stack is empty. 54 | func (s *Stack) Pop() (string, error) { 55 | s.mu.Lock() 56 | defer s.mu.Unlock() 57 | 58 | c := len(s.elements) 59 | if c == 0 { 60 | return "", errors.New("no more elements") 61 | } 62 | 63 | v := s.elements[c-1] 64 | s.elements = s.elements[:c-1] 65 | return v, nil 66 | } 67 | 68 | // Len returns the length of the stack. 69 | func (s *Stack) Len() int { 70 | s.mu.Lock() 71 | defer s.mu.Unlock() 72 | return len(s.elements) 73 | } 74 | 75 | // cfg is the base or parent AWS configuration for this lambda. 76 | var cfg aws.Config 77 | 78 | // apiClient provides access to the Amazon API Gateway management functions. Once initialized, the instance is reused 79 | // across subsequent AWS Lambda invocations. This potentially amortizes the instance creation over multiple executions 80 | // of the AWS Lambda instance. 81 | var apiClient *apigatewaymanagementapi.Client 82 | 83 | // Use the SDK default configuration, loading additional config and credentials values from the environment variables, 84 | // shared credentials, and shared configuration files. 85 | func init() { 86 | var err error 87 | cfg, err = external.LoadDefaultAWSConfig() 88 | if err != nil { 89 | logger.Instance.Panic("unable to load SDK config", zap.Error(err)) 90 | } 91 | } 92 | 93 | func main() { 94 | lambda.Start(handler) 95 | } 96 | 97 | // handler is the hook AWS Lambda calls to invoke the function as an Amazon API Gateway Proxy. This handlers reads the 98 | // request and echos the request back out to all connected clients. This demonstrates looking up connected clients from 99 | // the Redis cache and calling the Amazon API Gateway Management API to send data to the connected clients. 100 | func handler(ctx context.Context, req *events.APIGatewayWebsocketProxyRequest) (apigw.Response, error) { 101 | defer func() { 102 | _ = logger.Instance.Sync() 103 | }() 104 | 105 | // Lazily initialize the API Gateway Management client. This enables setting the service's endpoint to our API 106 | // endpoint. These values are provided from the synchronous request, thus the client can only be created upon the 107 | // first invocation. 108 | if apiClient == nil { 109 | apiClient = apigw.NewAPIGatewayManagementClient(&cfg, req.RequestContext.DomainName, req.RequestContext.Stage) 110 | } 111 | 112 | logger.Instance.Info("websocket publish", 113 | zap.String("requestId", req.RequestContext.RequestID), 114 | zap.String("connectionId", req.RequestContext.ConnectionID)) 115 | 116 | input, err := new(ws.InputEnvelop).Decode([]byte(req.Body)) 117 | if err != nil { 118 | logger.Instance.Error("failed to parse client input", 119 | zap.String("requestId", req.RequestContext.RequestID), 120 | zap.String("connectionId", req.RequestContext.ConnectionID), 121 | zap.Error(err)) 122 | 123 | return apigw.BadRequestResponse(), err 124 | } 125 | 126 | output := &ws.OutputEnvelop{ 127 | Data: input.Data, 128 | Type: input.Type, 129 | Received: time.Now().Unix(), 130 | } 131 | 132 | data, err := output.Encode() 133 | if err != nil { 134 | logger.Instance.Error("failed to encode output", 135 | zap.String("requestId", req.RequestContext.RequestID), 136 | zap.String("connectionId", req.RequestContext.ConnectionID), 137 | zap.Error(err)) 138 | 139 | return apigw.InternalServerErrorResponse(), err 140 | } 141 | 142 | stack := new(Stack) 143 | err = redis.Client.Do(radix.Cmd(&(stack.elements), "SMEMBERS", "connections")) 144 | if err != nil { 145 | logger.Instance.Error("failed to read connections from cache", 146 | zap.String("requestId", req.RequestContext.RequestID), 147 | zap.String("connectionId", req.RequestContext.ConnectionID), 148 | zap.Error(err)) 149 | 150 | return apigw.InternalServerErrorResponse(), err 151 | } 152 | 153 | logger.Instance.Info("websocket connections read from cache", 154 | zap.Int("connections", stack.Len()), 155 | zap.String("requestId", req.RequestContext.RequestID), 156 | zap.String("connectionId", req.RequestContext.ConnectionID)) 157 | 158 | // Calculate how many go routines should be created to handle the work. Taking the number of logical CPUs times a 159 | // factor of 4 enables processing outgoing messages concurrently while limiting the amount of context switching. 160 | var wg sync.WaitGroup 161 | for i := 0; i < runtime.NumCPU()*4; i++ { 162 | wg.Add(1) 163 | 164 | // Run the go routine until the context is canceled or there is no more work to process. 165 | go func(sender string, echo bool) { 166 | defer wg.Done() 167 | for { 168 | select { 169 | case <-ctx.Done(): 170 | return 171 | default: 172 | // Pull the next connection id from the stack. If the stack is empty, a non-nil error is returned 173 | // and the go routine exits cleanly. Otherwise, a nil error and the next connection id is returned. 174 | id, err := stack.Pop() 175 | if err != nil { 176 | return 177 | } 178 | 179 | // Do not send data to the connection if the connection represents the sender and the message was 180 | // configured to not echo back the message. 181 | if id == sender && !echo { 182 | continue 183 | } 184 | 185 | // Publish the data to the connected client via Amazon API Gateway's Management API. If publishing 186 | // the data results in an error, the error is passed to a convenience function which attempts to 187 | // resolve the issue which caused the error. The convenience function may return the same error if 188 | // it can not be handled or may return a different error if attempting the resolution results in an 189 | // error. Regardless, if an error is returned the only course of action is to log it. 190 | err = handleError(publish(ctx, id, data), id) 191 | if err != nil { 192 | logger.Instance.Error("failed to publish to connection", 193 | zap.String("receiver", id), 194 | zap.String("requestId", req.RequestContext.RequestID), 195 | zap.String("sender", req.RequestContext.ConnectionID), 196 | zap.Error(err)) 197 | } 198 | } 199 | } 200 | }(req.RequestContext.ConnectionID, input.Echo) 201 | } 202 | 203 | wg.Wait() 204 | return apigw.OkResponse(), nil 205 | } 206 | 207 | // publish publishes the provided data to the provided Amazon API Gateway connection ID. A common failure scenario which 208 | // results in an error is if the connection ID is no longer valid. This can occur when a client disconnected from the 209 | // Amazon API Gateway endpoint but the disconnect AWS Lambda was not invoked as it is not guaranteed to be invoked when 210 | // clients disconnect. 211 | func publish(ctx context.Context, id string, data []byte) error { 212 | _, err := apiClient.PostToConnectionRequest(&apigatewaymanagementapi.PostToConnectionInput{ 213 | Data: data, 214 | ConnectionId: aws.String(id), 215 | }).Send(ctx) 216 | 217 | return err 218 | } 219 | 220 | // handleError is a convenience function for taking action for a given error value. The function handles nil errors as a 221 | // convenience to the caller. If a nil error is provided, the error is immediately returned. The function may return an 222 | // error from the handling action, such as deleting the id from the cache, if that action results in an error. 223 | func handleError(err error, id string) error { 224 | if err == nil { 225 | return err 226 | } 227 | 228 | // Casting to the awserr.Error type will allow you to inspect the error code returned by the service in code. The 229 | // error code can be used to switch on context specific functionality. 230 | if aerr, ok := err.(awserr.Error); ok { 231 | switch aerr.Code() { 232 | case aws.ErrCodeSerialization: 233 | logger.Instance.Info("delete stale connection details from cache", zap.String("connectionId", id)) 234 | return deleteConnectionId(id) 235 | case apigatewaymanagementapi.ErrCodeGoneException: 236 | logger.Instance.Info("delete stale connection details from cache", zap.String("connectionId", id)) 237 | return deleteConnectionId(id) 238 | default: 239 | return err 240 | } 241 | } 242 | 243 | return err 244 | } 245 | 246 | // deleteConnectionId deletes the connection id from the REDIS cache. The function logs both error and success cases. 247 | func deleteConnectionId(id string) error { 248 | var result string 249 | err := redis.Client.Do(radix.Cmd(&result, "SREM", "connections", id)) 250 | if err != nil { 251 | logger.Instance.Error("failed to delete connection details from cache", 252 | zap.String("connectionId", id), 253 | zap.Error(err)) 254 | 255 | return err 256 | } 257 | 258 | logger.Instance.Info("websocket connection deleted from cache", 259 | zap.String("result", result), 260 | zap.String("connectionId", id)) 261 | 262 | return err 263 | } 264 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | # Copyright 2020 Amazon.com, Inc. or its affiliates. 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | AWSTemplateFormatVersion: "2010-09-09" 23 | Transform: AWS::Serverless-2016-10-31 24 | Description: > 25 | 26 | Parameters: 27 | ApplicationName: 28 | Type: String 29 | Default: API Gateway WebSockets Golang Example 30 | Description: The application name used when tagging resources 31 | 32 | VpcCIDR: 33 | Type: String 34 | Default: 10.1.0.0/16 35 | Description: The IP range (CIDR notation) for this VPC 36 | AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(\\d|[1-2]\\d|3[0-2]))$" 37 | 38 | PrivateSubnet1CIDR: 39 | Type: String 40 | Default: 10.1.1.0/24 41 | Description: The IP range (CIDR notation) for the private subnet in the first AZ 42 | AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(\\d|[1-2]\\d|3[0-2]))$" 43 | 44 | PrivateSubnet2CIDR: 45 | Type: String 46 | Default: 10.1.2.0/24 47 | Description: The IP range (CIDR notation) for the private subnet in the second AZ 48 | AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(\\d|[1-2]\\d|3[0-2]))$" 49 | 50 | PublicSubnet1CIDR: 51 | Type: String 52 | Default: 10.1.3.0/24 53 | Description: The IP range (CIDR notation) for the public subnet in the first AZ 54 | AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(\\d|[1-2]\\d|3[0-2]))$" 55 | 56 | PublicSubnet2CIDR: 57 | Type: String 58 | Default: 10.1.4.0/24 59 | Description: The IP range (CIDR notation) for the public subnet in the second AZ 60 | AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(\\d|[1-2]\\d|3[0-2]))$" 61 | 62 | CacheNodeType: 63 | Type: String 64 | Default: cache.t2.small 65 | Description: EC2 Instance Type to use for the Redis cache 66 | AllowedValues: 67 | - cache.t2.micro 68 | - cache.t2.small 69 | - cache.t2.medium 70 | - cache.t3.micro 71 | - cache.t3.small 72 | - cache.t3.medium 73 | 74 | Globals: 75 | Function: 76 | CodeUri: . 77 | Timeout: 5 78 | Tracing: Active 79 | MemorySize: 512 80 | Runtime: provided.al2 81 | Handler: my.bootstrap.file 82 | VpcConfig: 83 | SubnetIds: 84 | - !Ref PrivateSubnet1 85 | - !Ref PrivateSubnet2 86 | SecurityGroupIds: 87 | - !Ref NoIngressSecurityGroup 88 | 89 | Resources: 90 | VPC: 91 | Type: AWS::EC2::VPC 92 | Properties: 93 | CidrBlock: !Ref VpcCIDR 94 | EnableDnsSupport: true 95 | EnableDnsHostnames: true 96 | Tags: 97 | - Key: Name 98 | Value: !Ref ApplicationName 99 | 100 | PrivateSubnet1: 101 | Type: AWS::EC2::Subnet 102 | Properties: 103 | VpcId: !Ref VPC 104 | CidrBlock: !Ref PrivateSubnet1CIDR 105 | AvailabilityZone: !Select 106 | - 0 107 | - Fn::GetAZs: !Ref "AWS::Region" 108 | Tags: 109 | - Key: Name 110 | Value: !Sub ${ApplicationName} Private Subnet (AZ1) 111 | 112 | PrivateSubnet2: 113 | Type: AWS::EC2::Subnet 114 | Properties: 115 | VpcId: !Ref VPC 116 | CidrBlock: !Ref PrivateSubnet2CIDR 117 | AvailabilityZone: !Select 118 | - 1 119 | - Fn::GetAZs: !Ref "AWS::Region" 120 | Tags: 121 | - Key: Name 122 | Value: !Sub ${ApplicationName} Private Subnet (AZ2) 123 | 124 | PublicSubnet1: 125 | Type: AWS::EC2::Subnet 126 | Properties: 127 | VpcId: !Ref VPC 128 | CidrBlock: !Ref PublicSubnet1CIDR 129 | AvailabilityZone: !Select 130 | - 0 131 | - Fn::GetAZs: !Ref "AWS::Region" 132 | Tags: 133 | - Key: Name 134 | Value: !Sub ${ApplicationName} Public Subnet (AZ1) 135 | 136 | PublicSubnet2: 137 | Type: AWS::EC2::Subnet 138 | Properties: 139 | VpcId: !Ref VPC 140 | CidrBlock: !Ref PublicSubnet2CIDR 141 | AvailabilityZone: !Select 142 | - 1 143 | - Fn::GetAZs: !Ref "AWS::Region" 144 | Tags: 145 | - Key: Name 146 | Value: !Sub ${ApplicationName} Public Subnet (AZ2) 147 | 148 | InternetGateway: 149 | Type: AWS::EC2::InternetGateway 150 | Properties: 151 | Tags: 152 | - Key: Name 153 | Value: !Sub ${ApplicationName} Internet Gateway 154 | 155 | InternetGatewayAttachment: 156 | Type: AWS::EC2::VPCGatewayAttachment 157 | Properties: 158 | VpcId: !Ref VPC 159 | InternetGatewayId: !Ref InternetGateway 160 | 161 | NatGateway1EIP: 162 | Type: AWS::EC2::EIP 163 | DependsOn: InternetGatewayAttachment 164 | Properties: 165 | Domain: vpc 166 | Tags: 167 | - Key: Name 168 | Value: !Sub ${ApplicationName} (1) 169 | 170 | NatGateway2EIP: 171 | Type: AWS::EC2::EIP 172 | DependsOn: InternetGatewayAttachment 173 | Properties: 174 | Domain: vpc 175 | Tags: 176 | - Key: Name 177 | Value: !Sub ${ApplicationName} (2) 178 | 179 | NatGateway1: 180 | Type: AWS::EC2::NatGateway 181 | Properties: 182 | SubnetId: !Ref PublicSubnet1 183 | AllocationId: !GetAtt NatGateway1EIP.AllocationId 184 | Tags: 185 | - Key: Name 186 | Value: !Sub ${ApplicationName} (1) 187 | 188 | NatGateway2: 189 | Type: AWS::EC2::NatGateway 190 | Properties: 191 | SubnetId: !Ref PublicSubnet2 192 | AllocationId: !GetAtt NatGateway2EIP.AllocationId 193 | Tags: 194 | - Key: Name 195 | Value: !Sub ${ApplicationName} (2) 196 | 197 | PublicRouteTable: 198 | Type: AWS::EC2::RouteTable 199 | Properties: 200 | VpcId: !Ref VPC 201 | Tags: 202 | - Key: Name 203 | Value: !Sub ${ApplicationName} Public Routes 204 | 205 | DefaultPublicRoute: 206 | Type: AWS::EC2::Route 207 | DependsOn: InternetGatewayAttachment 208 | Properties: 209 | GatewayId: !Ref InternetGateway 210 | DestinationCidrBlock: 0.0.0.0/0 211 | RouteTableId: !Ref PublicRouteTable 212 | 213 | PublicSubnet1RouteTableAssociation: 214 | Type: AWS::EC2::SubnetRouteTableAssociation 215 | Properties: 216 | SubnetId: !Ref PublicSubnet1 217 | RouteTableId: !Ref PublicRouteTable 218 | 219 | PublicSubnet2RouteTableAssociation: 220 | Type: AWS::EC2::SubnetRouteTableAssociation 221 | Properties: 222 | SubnetId: !Ref PublicSubnet2 223 | RouteTableId: !Ref PublicRouteTable 224 | 225 | PrivateRouteTable1: 226 | Type: AWS::EC2::RouteTable 227 | Properties: 228 | VpcId: !Ref VPC 229 | Tags: 230 | - Key: Name 231 | Value: !Sub ${ApplicationName} Private Routes (AZ1) 232 | 233 | DefaultPrivateRoute1: 234 | Type: AWS::EC2::Route 235 | Properties: 236 | NatGatewayId: !Ref NatGateway1 237 | DestinationCidrBlock: 0.0.0.0/0 238 | RouteTableId: !Ref PrivateRouteTable1 239 | 240 | PrivateSubnet1RouteTableAssociation: 241 | Type: AWS::EC2::SubnetRouteTableAssociation 242 | Properties: 243 | SubnetId: !Ref PrivateSubnet1 244 | RouteTableId: !Ref PrivateRouteTable1 245 | 246 | PrivateRouteTable2: 247 | Type: AWS::EC2::RouteTable 248 | Properties: 249 | VpcId: !Ref VPC 250 | Tags: 251 | - Key: Name 252 | Value: !Sub ${ApplicationName} Private Routes (AZ2) 253 | 254 | DefaultPrivateRoute2: 255 | Type: AWS::EC2::Route 256 | Properties: 257 | NatGatewayId: !Ref NatGateway2 258 | DestinationCidrBlock: 0.0.0.0/0 259 | RouteTableId: !Ref PrivateRouteTable2 260 | 261 | PrivateSubnet2RouteTableAssociation: 262 | Type: AWS::EC2::SubnetRouteTableAssociation 263 | Properties: 264 | SubnetId: !Ref PrivateSubnet2 265 | RouteTableId: !Ref PrivateRouteTable2 266 | 267 | PrivateHostedZone: 268 | Type: "AWS::Route53::HostedZone" 269 | Properties: 270 | Name: "service.internal" 271 | HostedZoneConfig: 272 | Comment: Private Hosted Zone 273 | VPCs: 274 | - VPCId: !Ref VPC 275 | VPCRegion: !Ref "AWS::Region" 276 | 277 | CacheEndpointRecordSetGroup: 278 | Type: AWS::Route53::RecordSetGroup 279 | Properties: 280 | HostedZoneId: !Ref PrivateHostedZone 281 | Comment: Record Set for the primary Redis endpoint 282 | RecordSets: 283 | - TTL: "900" 284 | Type: CNAME 285 | Name: "redis.service.internal" 286 | ResourceRecords: 287 | - !GetAtt RedisReplicationGroup.PrimaryEndPoint.Address 288 | - TTL: "900" 289 | Type: SRV 290 | Name: "_redis._tcp.service.internal" 291 | ResourceRecords: 292 | - !Sub "1 0 ${RedisReplicationGroup.PrimaryEndPoint.Port} redis.service.internal" 293 | 294 | NoIngressSecurityGroup: 295 | Type: AWS::EC2::SecurityGroup 296 | Properties: 297 | VpcId: !Ref VPC 298 | GroupDescription: !Sub ${ApplicationName} Security Group (No Ingress) 299 | Tags: 300 | - Key: Name 301 | Value: !Sub ${ApplicationName} Security Group (No Ingress) 302 | 303 | RedisSecurityGroup: 304 | Type: AWS::EC2::SecurityGroup 305 | Properties: 306 | VpcId: !Ref VPC 307 | GroupDescription: !Sub ${ApplicationName} Security Group (Redis) 308 | SecurityGroupIngress: 309 | - ToPort: 6379 310 | FromPort: 6379 311 | IpProtocol: tcp 312 | SourceSecurityGroupId: !Ref NoIngressSecurityGroup 313 | Description: Redis cluster connections from security group 314 | Tags: 315 | - Key: Name 316 | Value: !Sub ${ApplicationName} Security Group (Redis) 317 | 318 | RedisSubnetGroup: 319 | Type: AWS::ElastiCache::SubnetGroup 320 | Properties: 321 | Description: Redis cluster subnet group 322 | SubnetIds: 323 | - !Ref PrivateSubnet1 324 | - !Ref PrivateSubnet2 325 | 326 | RedisReplicationGroup: 327 | Type: AWS::ElastiCache::ReplicationGroup 328 | Properties: 329 | Port: 6379 330 | Engine: redis 331 | NumCacheClusters: 2 332 | EngineVersion: 5.0.0 333 | CacheNodeType: !Ref CacheNodeType 334 | AutomaticFailoverEnabled: true 335 | AtRestEncryptionEnabled: false 336 | TransitEncryptionEnabled: false 337 | CacheSubnetGroupName: !Ref RedisSubnetGroup 338 | PreferredMaintenanceWindow: sun:23:00-mon:01:30 339 | ReplicationGroupDescription: ElastiCache For Redis Replication Group 340 | SecurityGroupIds: 341 | - !Ref RedisSecurityGroup 342 | PreferredCacheClusterAZs: 343 | - !Select 344 | - 0 345 | - Fn::GetAZs: !Ref "AWS::Region" 346 | - !Select 347 | - 1 348 | - Fn::GetAZs: !Ref "AWS::Region" 349 | Tags: 350 | - Key: Name 351 | Value: !Sub ${ApplicationName} ElastiCache for Redis 352 | 353 | ConnectFunction: 354 | Type: AWS::Serverless::Function 355 | Metadata: 356 | BuildMethod: makefile 357 | Properties: 358 | Policies: 359 | - VPCAccessPolicy: {} 360 | 361 | DisconnectFunction: 362 | Metadata: 363 | BuildMethod: makefile 364 | Type: AWS::Serverless::Function 365 | Properties: 366 | Policies: 367 | - VPCAccessPolicy: {} 368 | 369 | PublishFunction: 370 | Metadata: 371 | BuildMethod: makefile 372 | Type: AWS::Serverless::Function 373 | Properties: 374 | Timeout: 15 375 | MemorySize: 2048 376 | Policies: 377 | - VPCAccessPolicy: {} 378 | - Statement: 379 | - Effect: Allow 380 | Action: 381 | - "execute-api:ManageConnections" 382 | Resource: 383 | - !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocket}/*" 384 | 385 | WebSocket: 386 | Type: AWS::ApiGatewayV2::Api 387 | Properties: 388 | Name: !Ref ApplicationName 389 | ProtocolType: WEBSOCKET 390 | RouteSelectionExpression: "$request.body.message" 391 | 392 | Deployment: 393 | Type: AWS::ApiGatewayV2::Deployment 394 | DependsOn: 395 | - PublishRoute 396 | - ConnectRoute 397 | - DisconnectRoute 398 | Properties: 399 | ApiId: !Ref WebSocket 400 | 401 | Stage: 402 | Type: AWS::ApiGatewayV2::Stage 403 | Properties: 404 | StageName: v1 405 | ApiId: !Ref WebSocket 406 | Description: TO DO 407 | DeploymentId: !Ref Deployment 408 | DefaultRouteSettings: 409 | LoggingLevel: INFO 410 | DataTraceEnabled: true 411 | DetailedMetricsEnabled: true 412 | 413 | ConnectFunctionPermission: 414 | Type: AWS::Lambda::Permission 415 | DependsOn: 416 | - WebSocket 417 | Properties: 418 | Action: lambda:InvokeFunction 419 | Principal: apigateway.amazonaws.com 420 | FunctionName: !Ref ConnectFunction 421 | 422 | DisconnectFunctionPermission: 423 | Type: AWS::Lambda::Permission 424 | DependsOn: 425 | - WebSocket 426 | Properties: 427 | Action: lambda:InvokeFunction 428 | Principal: apigateway.amazonaws.com 429 | FunctionName: !Ref DisconnectFunction 430 | 431 | PublishFunctionPermission: 432 | Type: AWS::Lambda::Permission 433 | DependsOn: 434 | - WebSocket 435 | Properties: 436 | Action: lambda:InvokeFunction 437 | Principal: apigateway.amazonaws.com 438 | FunctionName: !Ref PublishFunction 439 | 440 | ConnectFunctionLogGroup: 441 | Type: AWS::Logs::LogGroup 442 | DependsOn: 443 | - ConnectFunction 444 | Properties: 445 | RetentionInDays: 30 446 | LogGroupName: !Sub /aws/lambda/${ConnectFunction} 447 | 448 | DisconnectFunctionLogGroup: 449 | Type: AWS::Logs::LogGroup 450 | DependsOn: 451 | - DisconnectFunction 452 | Properties: 453 | RetentionInDays: 30 454 | LogGroupName: !Sub /aws/lambda/${DisconnectFunction} 455 | 456 | PublishFunctionLogGroup: 457 | Type: AWS::Logs::LogGroup 458 | DependsOn: 459 | - PublishFunction 460 | Properties: 461 | RetentionInDays: 30 462 | LogGroupName: !Sub /aws/lambda/${PublishFunction} 463 | 464 | ConnectRoute: 465 | Type: AWS::ApiGatewayV2::Route 466 | Properties: 467 | RouteKey: $connect 468 | ApiId: !Ref WebSocket 469 | AuthorizationType: NONE 470 | OperationName: ConnectRoute 471 | Target: !Join 472 | - "/" 473 | - - "integrations" 474 | - !Ref ConnectIntegration 475 | 476 | DisconnectRoute: 477 | Type: AWS::ApiGatewayV2::Route 478 | Properties: 479 | RouteKey: $disconnect 480 | ApiId: !Ref WebSocket 481 | AuthorizationType: NONE 482 | OperationName: DisconnectRoute 483 | Target: !Join 484 | - "/" 485 | - - "integrations" 486 | - !Ref DisconnectIntegration 487 | 488 | PublishRoute: 489 | Type: AWS::ApiGatewayV2::Route 490 | Properties: 491 | RouteKey: $default 492 | ApiId: !Ref WebSocket 493 | AuthorizationType: NONE 494 | OperationName: PublishRoute 495 | Target: !Join 496 | - "/" 497 | - - "integrations" 498 | - !Ref PublishIntegration 499 | 500 | ConnectIntegration: 501 | Type: AWS::ApiGatewayV2::Integration 502 | Properties: 503 | ApiId: !Ref WebSocket 504 | Description: TO DO 505 | IntegrationType: AWS_PROXY 506 | IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ConnectFunction.Arn}/invocations 507 | 508 | DisconnectIntegration: 509 | Type: AWS::ApiGatewayV2::Integration 510 | Properties: 511 | ApiId: !Ref WebSocket 512 | Description: TO DO 513 | IntegrationType: AWS_PROXY 514 | IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DisconnectFunction.Arn}/invocations 515 | 516 | PublishIntegration: 517 | Type: AWS::ApiGatewayV2::Integration 518 | Properties: 519 | ApiId: !Ref WebSocket 520 | Description: TO DO 521 | IntegrationType: AWS_PROXY 522 | IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PublishFunction.Arn}/invocations 523 | 524 | CacheNodeCpuUtilizationAlarm: 525 | Type: AWS::CloudWatch::Alarm 526 | Properties: 527 | Period: 900 528 | Threshold: 90 529 | Unit: Percent 530 | Statistic: Average 531 | EvaluationPeriods: 2 532 | MetricName: CPUUtilization 533 | Namespace: AWS/ElastiCache 534 | ComparisonOperator: GreaterThanThreshold 535 | AlarmName: Redis Cache Cluster CPU Utilization 536 | AlarmDescription: Alarm if average percentage of Nodes CPU utilization goes over 90% 537 | Dimensions: 538 | - Name: CacheClusterId 539 | Value: !Ref RedisReplicationGroup 540 | 541 | CacheEvictionsAlarm: 542 | Type: AWS::CloudWatch::Alarm 543 | Properties: 544 | Period: 60 545 | Threshold: 0 546 | Statistic: Sum 547 | EvaluationPeriods: 3 548 | MetricName: Evictions 549 | Namespace: AWS/ElastiCache 550 | TreatMissingData: notBreaching 551 | ComparisonOperator: GreaterThanThreshold 552 | AlarmName: Redis Cache Cluster Evictions 553 | AlarmDescription: Alarm if cache is evicting non-expired items 554 | Dimensions: 555 | - Name: CacheClusterId 556 | Value: !Ref RedisReplicationGroup 557 | 558 | CacheSwapUsageAlarm: 559 | Type: AWS::CloudWatch::Alarm 560 | Properties: 561 | Period: 300 562 | Statistic: Maximum 563 | Threshold: 50000000 564 | DatapointsToAlarm: 5 565 | EvaluationPeriods: 5 566 | MetricName: SwapUsage 567 | Namespace: AWS/ElastiCache 568 | ComparisonOperator: GreaterThanThreshold 569 | AlarmName: Redis Cache Cluster Swap Usage 570 | AlarmDescription: Alarm if cache swap usage exceeds 50MB 571 | Dimensions: 572 | - Name: CacheClusterId 573 | Value: !Ref RedisReplicationGroup 574 | 575 | Outputs: 576 | VPC: 577 | Value: !Ref VPC 578 | Description: The created VPC 579 | 580 | PublicSubnets: 581 | Description: A list of the public subnets 582 | Value: !Join [",", [!Ref PublicSubnet1, !Ref PublicSubnet2]] 583 | 584 | PrivateSubnets: 585 | Description: A list of the private subnets 586 | Value: !Join [",", [!Ref PrivateSubnet1, !Ref PrivateSubnet2]] 587 | 588 | PublicSubnet1: 589 | Value: !Ref PublicSubnet1 590 | Description: A reference to the public subnet in the 1st Availability Zone 591 | 592 | PublicSubnet2: 593 | Value: !Ref PublicSubnet2 594 | Description: A reference to the public subnet in the 2nd Availability Zone 595 | 596 | PrivateSubnet1: 597 | Value: !Ref PrivateSubnet1 598 | Description: A reference to the private subnet in the 1st Availability Zone 599 | 600 | PrivateSubnet2: 601 | Value: !Ref PrivateSubnet2 602 | Description: A reference to the private subnet in the 2nd Availability Zone 603 | 604 | NoIngressSecurityGroup: 605 | Value: !Ref NoIngressSecurityGroup 606 | Description: Security group with no ingress rule 607 | 608 | RedisSecurityGroup: 609 | Value: !Ref RedisSecurityGroup 610 | Description: Security group for ElastiCache Redis 611 | 612 | RedisPrimaryEndpointAddress: 613 | Value: !GetAtt RedisReplicationGroup.PrimaryEndPoint.Address 614 | Description: Primary Endpoint Address for the ElastiCache Redis Cluster 615 | 616 | RedisPrimaryEndpointPort: 617 | Value: !GetAtt RedisReplicationGroup.PrimaryEndPoint.Port 618 | Description: Primary Endpoint Port for the ElastiCache Redis Cluster 619 | 620 | WebSocketEndpoint: 621 | Description: URL for making WebSocket connections to the application's API 622 | Value: !Sub "wss://${WebSocket}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/" 623 | --------------------------------------------------------------------------------