Skip to content

Commit 79e31c5

Browse files
committed
Merge branch 'serverless/main'
2 parents 350fd1d + fbdbc91 commit 79e31c5

27 files changed

Lines changed: 1112 additions & 28 deletions
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# API WebSocket to SNS with request validation
2+
3+
This pattern creates a WebSocket API to send notification via SNS topic with request validation.
4+
5+
Learn more about this pattern at Serverless Land Patterns:
6+
7+
Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.
8+
9+
## Requirements
10+
11+
* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
12+
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
13+
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
14+
* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) installed
15+
16+
## Deployment Instructions
17+
18+
1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
19+
```
20+
git clone https://github.com/aws-samples/serverless-patterns
21+
```
22+
2. Change directory to the pattern directory:
23+
```
24+
cd serverless-patterns/apigw-websocket-api-sns-terraform
25+
```
26+
3. From the command line, initialize terraform to download and install the providers defined in the configuration:
27+
```
28+
terraform init
29+
```
30+
4. From the command line, apply the configuration in the main.tf file:
31+
```
32+
terraform apply
33+
```
34+
5. During the prompts:
35+
* Enter yes
36+
6. Note the outputs from the deployment process, these contain the resource names and/or ARNs which are used for testing.
37+
38+
## How it works
39+
40+
This sample project demonstrates how to use WebSocket API to integrate with Amazon Simple Notification service (SNS) to send notifications. This pattern also implements data validation in WebSocket API using model in API Gateway. This template does not implement Authentication in WebSocket API to keep it simple. However, it is recommended to implement Authentication on WebSocket API.
41+
This pattern is utilizing native AWS Integration between WebSocket API Gateway and SNS. Request template is used in WebSocket integration to map the input to SNS payload.
42+
This pattern is also a workaround to invoke AWS services in WebSocket API which requires Content-Type header to be application/x-www-form-urlencoded. By default, WebSocket APIs do not support overriding headers from AWS console and supports application/json in Content-Type header.
43+
44+
## Testing
45+
46+
Once the application is deployed, retrieve the `websocket_URI` value from outputs from terraform apply. To test the Websocket API, you can use [wscat](https://github.com/websockets/wscat) which is an open-source command line tool.
47+
48+
1. wscat -c < YOUR WEBSOCKET URL >
49+
50+
2. Send a payload to the API in the correct format of the Model, otherwise you will receive a 'Forbidden' exception. The model in the template expects the request in below format:
51+
```
52+
{"action":"sendOrder","Name":"Jon","SirName":"Doe","Number":"123"}
53+
```
54+
3. A successful invocation to Amazon SNS would return the message ID like below:
55+
```
56+
{"PublishResponse":{"PublishResult":{"MessageId":"10e10111-a2bf-3a33-b44b-5b55dbde5555","SequenceNumber":null},"ResponseMetadata":{"RequestId":"bd11111e-2222-3c33-4444-055c5550c55e"}}}
57+
```
58+
59+
60+
## Cleanup
61+
62+
1. Change directory to the pattern directory:
63+
```
64+
cd serverless-patterns/apigw-websocket-api-sns-terraform
65+
```
66+
2. Delete the resource
67+
```
68+
terraform destroy -target aws_apigatewayv2_route.send_order
69+
terraform destroy
70+
```
71+
3. During the prompts:
72+
* Enter yes
73+
74+
4. Delete all the resources
75+
```
76+
terraform destroy
77+
```
78+
During the prompts:
79+
* Enter yes
80+
81+
5. Confirm all created resources has been deleted
82+
```
83+
terraform show
84+
```
85+
86+
87+
----
88+
Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
89+
90+
SPDX-License-Identifier: MIT-0
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"title": "API WebSocket to SNS with request validation",
3+
"description": "Create a WebSocket API to send notification via SNS topic with request validation",
4+
"language": "",
5+
"level": "200",
6+
"framework": "Terraform",
7+
"introBox": {
8+
"headline": "How it works",
9+
"text": [
10+
"This sample project demonstrates how to use WebSocket API to integrate with Amazon Simple Notification service (SNS) to send notifications. This pattern also implements data validation in WebSocket API using model in API Gateway.",
11+
"This pattern is utilizing native AWS Integration between WebSocket API Gateway and SNS. Request template is used in WebSocket integration to map the input to SNS payload.",
12+
"This pattern is also a workaround to invoke AWS services in WebSocket API which requires Content-Type header to be application/x-www-form-urlencoded. By default, WebSocket APIs do not support overriding headers from AWS console by default",
13+
"This pattern deploys one API Gateway and one SNS topic."
14+
]
15+
},
16+
"gitHub": {
17+
"template": {
18+
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-websocket-api-sns-terraform",
19+
"templateURL": "serverless-patterns/apigw-websocket-api-sns-terraform",
20+
"projectFolder": "apigw-websocket-api-sns-terraform",
21+
"templateFile": "apigw-websocket-api-sns-terraform/main.tf"
22+
}
23+
},
24+
"resources": {
25+
"bullets": [
26+
{
27+
"text": "Request validation in WebSocket API",
28+
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-request-validation.html"
29+
},
30+
{
31+
"text": "Mapping template in WebSocket API",
32+
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-data-transformations.html"
33+
}
34+
]
35+
},
36+
"deploy": {
37+
"text": [
38+
"terraform init",
39+
"terraform apply"
40+
]
41+
},
42+
"testing": {
43+
"text": [
44+
"See the GitHub repo for detailed testing instructions."
45+
]
46+
},
47+
"cleanup": {
48+
"text": [
49+
"terraform destroy -target aws_apigatewayv2_route.send_order",
50+
"terraform destroy",
51+
"terraform show"
52+
]
53+
},
54+
"authors": [
55+
{
56+
"name": "Dushyant Pal",
57+
"image": "https://drive.google.com/file/d/1nHLKNYGM4UaYa9sewd9lTQLEJpoZfDem/view",
58+
"bio": "Cloud Engineer, Serverless team at AWS Sydney, Australia",
59+
"linkedIn": "https://www.linkedin.com/in/dushyant-pal-b331996"
60+
}
61+
]
62+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
terraform {
2+
required_providers {
3+
aws = {
4+
source = "hashicorp/aws"
5+
version = "~> 4.0.0"
6+
}
7+
random = {
8+
source = "hashicorp/random"
9+
version = "~> 3.1.0"
10+
}
11+
archive = {
12+
source = "hashicorp/archive"
13+
version = "~> 2.2.0"
14+
}
15+
}
16+
17+
required_version = ">= 0.14.9"
18+
}
19+
20+
provider "aws" {
21+
region = "eu-west-1"
22+
}
23+
data "aws_region" "current" {}
24+
25+
locals {
26+
stack_name = "template"
27+
}
28+
29+
variable "api_stage_name" {
30+
description = "Name of WebSocket API stage"
31+
type = string
32+
default = "production"
33+
}
34+
35+
resource "aws_sns_topic" "sns_topic" {
36+
display_name = "${local.stack_name}-SNSTopic"
37+
}
38+
39+
resource "aws_apigatewayv2_api" "web_socket_api" {
40+
name = "${local.stack_name}-WebSocketApi"
41+
protocol_type = "WEBSOCKET"
42+
route_selection_expression = "$request.body.action"
43+
}
44+
45+
46+
resource "aws_apigatewayv2_stage" "MyApiGatewayStage" {
47+
api_id = aws_apigatewayv2_api.web_socket_api.id
48+
name = var.api_stage_name
49+
auto_deploy = true
50+
}
51+
52+
resource "aws_apigatewayv2_route" "connect_route" {
53+
api_id = aws_apigatewayv2_api.web_socket_api.id
54+
route_key = "$connect"
55+
authorization_type = "NONE"
56+
route_response_selection_expression = "$default"
57+
operation_name = "ConnectRoute"
58+
target = join("/", ["integrations", aws_apigatewayv2_integration.connect_route_integration.id])
59+
}
60+
61+
resource "aws_apigatewayv2_integration" "connect_route_integration" {
62+
api_id = aws_apigatewayv2_api.web_socket_api.id
63+
integration_type = "MOCK"
64+
request_templates = {
65+
200 = "{\"statusCode\":200}"
66+
}
67+
template_selection_expression = "200"
68+
passthrough_behavior = "WHEN_NO_MATCH"
69+
}
70+
71+
resource "aws_apigatewayv2_route_response" "connect_route_response" {
72+
route_id = aws_apigatewayv2_route.connect_route.id
73+
api_id = aws_apigatewayv2_api.web_socket_api.id
74+
route_response_key = "$default"
75+
}
76+
77+
resource "aws_apigatewayv2_integration_response" "connect_route_integration_response" {
78+
api_id = aws_apigatewayv2_api.web_socket_api.id
79+
integration_id = aws_apigatewayv2_integration.connect_route_integration.id
80+
integration_response_key = "/200/"
81+
template_selection_expression = "\\$default"
82+
response_templates = {
83+
200 = "{\"statusCode\":, \"message\":\"order initiated\"}"
84+
}
85+
}
86+
87+
resource "aws_iam_role" "apigw_sns_demo_functionrole" {
88+
#name = "apigw_sns_demo_functionrole"
89+
assume_role_policy = <<EOF
90+
{
91+
"Version": "2012-10-17",
92+
"Statement": [
93+
{
94+
"Action": "sts:AssumeRole",
95+
"Principal": {
96+
"Service": "apigateway.amazonaws.com"
97+
},
98+
"Effect": "Allow",
99+
"Sid": ""
100+
}
101+
]
102+
}
103+
EOF
104+
}
105+
106+
# Custom policy
107+
resource "aws_iam_policy" "apigw_sns_demo_policy" {
108+
name = "apigw-sns-demo-policy"
109+
path = "/"
110+
description = "Policy for APIGW to SNS demo"
111+
policy = <<EOF
112+
{
113+
"Version" : "2012-10-17",
114+
"Statement" : [
115+
{
116+
"Effect": "Allow",
117+
"Action": [
118+
"sns:Publish"
119+
],
120+
"Resource": "${aws_sns_topic.sns_topic.arn}"
121+
}
122+
]
123+
}
124+
EOF
125+
}
126+
127+
resource "aws_iam_role_policy_attachment" "apigw_policy_attachment" {
128+
role = aws_iam_role.apigw_sns_demo_functionrole.name
129+
policy_arn = aws_iam_policy.apigw_sns_demo_policy.arn
130+
}
131+
132+
133+
resource "aws_apigatewayv2_route" "disconnect_route" {
134+
api_id = aws_apigatewayv2_api.web_socket_api.id
135+
route_key = "$disconnect"
136+
operation_name = "DisconnectRoute"
137+
target = join("/", ["integrations", aws_apigatewayv2_integration.disconnect_route_integration.id])
138+
}
139+
140+
resource "aws_apigatewayv2_integration" "disconnect_route_integration" {
141+
api_id = aws_apigatewayv2_api.web_socket_api.id
142+
integration_type = "MOCK"
143+
request_templates = {
144+
200 = "{\"statusCode\":200}"
145+
}
146+
template_selection_expression = "200"
147+
passthrough_behavior = "WHEN_NO_MATCH"
148+
}
149+
150+
resource "aws_apigatewayv2_integration_response" "disconnect_route_integration_response" {
151+
api_id = aws_apigatewayv2_api.web_socket_api.id
152+
integration_id = aws_apigatewayv2_integration.disconnect_route_integration.id
153+
integration_response_key = "/200/"
154+
template_selection_expression = "\\$default"
155+
}
156+
157+
158+
resource "aws_apigatewayv2_integration" "send_order_route_integration" {
159+
api_id = aws_apigatewayv2_api.web_socket_api.id
160+
integration_type = "AWS"
161+
integration_uri = "arn:aws:apigateway:${data.aws_region.current.name}:sns:action/Publish"
162+
credentials_arn = aws_iam_role.apigw_sns_demo_functionrole.arn
163+
integration_method = "POST"
164+
content_handling_strategy = "CONVERT_TO_TEXT"
165+
request_parameters = {
166+
"integration.request.header.Content-Type" = "'application/x-www-form-urlencoded'"
167+
}
168+
request_templates = {
169+
"$default" = "Action=Publish&TopicArn=$util.urlEncode(\"${aws_sns_topic.sns_topic.id}\")&Message=$input.json('$')"
170+
}
171+
template_selection_expression = "$request.body.action"
172+
}
173+
174+
resource "aws_apigatewayv2_model" "food_order_model" {
175+
api_id = aws_apigatewayv2_api.web_socket_api.id
176+
content_type = "application/json"
177+
description = "this model helps us to ensure that all the required parameters are passed on to the SNS topic"
178+
name = "orderModel"
179+
schema = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"orderInputModel\",\"type\":\"object\",\"properties\":{\"Number\":{\"type\":\"string\"},\"SirName\":{\"type\":\"string\"},\"Name\":{\"type\":\"string\"}},\"required\":[\"Name\",\"SirName\",\"Number\"]}"
180+
}
181+
182+
resource "aws_apigatewayv2_route" "send_order" {
183+
api_id = aws_apigatewayv2_api.web_socket_api.id
184+
route_key = "sendOrder"
185+
authorization_type = "NONE"
186+
model_selection_expression = "$request.body.action"
187+
request_models = {
188+
sendOrder = "orderModel"
189+
}
190+
target = join("/", ["integrations", aws_apigatewayv2_integration.send_order_route_integration.id])
191+
}
192+
193+
resource "aws_apigatewayv2_route_response" "send_order_route_response" {
194+
route_id = aws_apigatewayv2_route.send_order.id
195+
api_id = aws_apigatewayv2_api.web_socket_api.id
196+
route_response_key = "$default"
197+
}
198+
199+
resource "aws_apigatewayv2_integration_response" "send_order_route_integration_response" {
200+
api_id = aws_apigatewayv2_api.web_socket_api.id
201+
integration_id = aws_apigatewayv2_integration.send_order_route_integration.id
202+
integration_response_key = "$default"
203+
}
204+
205+
206+
207+
output "api_endpoint" {
208+
description = "API Gateway endpoint URL"
209+
value = "wss://${aws_apigatewayv2_api.web_socket_api.id}.execute-api.${data.aws_region.current.name}.amazonaws.com/${var.api_stage_name}"
210+
}

0 commit comments

Comments
 (0)