diff --git a/appsync-private-api-sam/Ouzoegwu.jpeg b/appsync-private-api-sam/Ouzoegwu.jpeg new file mode 100644 index 000000000..379c05ee6 Binary files /dev/null and b/appsync-private-api-sam/Ouzoegwu.jpeg differ diff --git a/appsync-private-api-sam/README.md b/appsync-private-api-sam/README.md new file mode 100644 index 000000000..b3cff75b7 --- /dev/null +++ b/appsync-private-api-sam/README.md @@ -0,0 +1,97 @@ +# AWS AppSync Private API + +This pattern shows how you can deploy an AWS AppSync Private API which can only be invoked by resources within your private network. AWS AppSync Private APIs helps customers to restrict access to GraphQL APIs to API consumers within a private network, such as Amazon Virtual Private Cloud (VPC) or hybrid environment. To deploy the provided SAM template, provide the VPC ID and Subnet ID as parameters which will be where the AppSync Interface VPC endpoint will be deployed. It is recommended to provide 2 or more subnet IDs for high availability. This implementation will support all GraphQL `queries`, `mutations` and `subscriptions` defined in the AppSync API GraphQL schema. To demonstrate this pattern, the template will deploy a simple Restaurant API with Amazon DynamoDB as a data source. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigateway-appsync-dynamodb-sam + +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. + +## Requirements + +- [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. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed +- [Create a VPC and Subnets](https://docs.aws.amazon.com/vpc/latest/userguide/create-vpc.html) to deploy the AppSync Interface VPC Endpoint or to test out the pattern, you can use the default VPC in each region. Use commands below to identify the default VPC ID and Subnet IDs + +``` +aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query "Vpcs[0].VpcId" --output text +``` + +``` +aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-xxxxxxxxxxx" --query "Subnets[*].SubnetId" --output json +``` + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ``` + cd appsync-private-api-same + ``` +3. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam deploy --guided + ``` +4. During the prompts: + + - Enter a stack name + - Enter the desired AWS Region + - Enter the VpcID where to deploy the Private AppSync API + - Enter a comma-separated list of SubnetIds in the VPC to deploy theA AppSync API Interface Endpoint + - Allow SAM CLI to create IAM roles with the required permissions. + + Once you have run `sam deploy --guided` mode once and saved arguments to a configuration file (samconfig.toml), you can use `sam deploy` in future to use these defaults. + +5. Note the outputs from the SAM deployment process. Two of the outputs `AppSync GraphQL API URL` and `AppSync VPC Endpoint DNS` will be used to test this pattern. + +## How it works + +This patterns creates and AppSync Interface VPC Endpoint and a sample AppSync Private API backed with a DynamoDB data source. Requests to AppSync Private APIs will go through AWS’s private network without going over the internet. GraphQL requests from your application are routed via the interface VPC endpoint to AppSync Private API. Interface VPC endpoint is powered by [AWS PrivateLink](https://aws.amazon.com/privatelink/), a highly available, scalable technology that enables you to privately connect your VPC to AWS services like AWS AppSync as if the services were in your VPC. + +API Key is used as the authorization mode for the AppSync API. However it is not recommended to use API Key for production application, please refer to other authorization modes supported by AppSync in the [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html) + +## Testing + +You can test this pattern using any command prompt that supports the `curl` command. Refer to the outputs `AppSyncApiUrl`, `AppSyncApiKey` and `AppSyncVPCEndpointDNS` from deploying the SAM application which will be used for testing. + +1. Create a resource (for example EC2 instance) within your private network to invoke the AppSync API +2. Open your command prompt where you can run a `curl` commands +3. To add a new restaurant entry to the Restaurant API, run the `curl` command below by pasting it in your command prompt. Remember to replace the values for `{AppSyncGraphQLAPIURL}`, `{AppSyncApiKey}` and `{AppSyncVPCEndpointDNS}` which are part of the output generated after deploying the SAM template. + +Note: You can either use the `AppSync GraphQL API URL` or `AppSync VPC Interface Endpoint DNS` to invoke the API as show below. You can refer to the blog [Architecture Patterns for AWS AppSync Private APIs]() for further guidance + +-- Using AppSync GraphQL API URL (enabled by Private DNS settings = Yes) + +```curl {AppSyncGraphQLAPIURL} \ + -H "Content-Type:application/graphql" \ + -H "x-api-key:da2-{AppSyncApiKey}" \ + -d '{"query": "query MyQuery {listRestaurants {items {name state restaurantId zip cuisine }}}","variables":"{}"}' +``` + +-- Using AppSync VPC Interface Endpoint DNS (you will need to pass the`AppSyncGraphQLAPIURL` in the host header, remember to remove prefix `www.`) + +```curl https://{AppSyncVPCEndpointDNS}/graphql \ + -H "Host:{AppSyncGraphQLAPIURL}" \ + -H "Content-Type:application/graphql" \ + -H "x-api-key:da2-{AppSyncApiKey}" \ + -d '{"query": "query MyQuery {listRestaurants {items {name state restaurantId zip cuisine }}}","variables":"{}"}' +``` + +4. Refer to the blog [Architecture Patterns for AWS AppSync Private APIs]() for further examples on how to test out GraphQL subscriptions. + +## Cleanup + +1. Delete the stack + ```bash + sam delete + ``` + +--- + +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/appsync-private-api-sam/example-pattern.json b/appsync-private-api-sam/example-pattern.json new file mode 100644 index 000000000..b1ee293c0 --- /dev/null +++ b/appsync-private-api-sam/example-pattern.json @@ -0,0 +1,57 @@ +{ + "title": "AWS AppSync Private API ", + "description": "Create an AWS AppSync Private API with a sample API to demonstrate how you can invoke Private API from resources in your private network", + "language": "YAML", + "level": "200", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern shows how you can deploy an AWS AppSync Private API which can only be invoked by resources within your private network.", + "The SAM application will create AppSync Interface VPC Endpoint and a sample AppSync Private API backed with a DynamoDB data source.", + "Requests to AppSync Private APIs will go through AWS’s private network without going over the internet.", + "GraphQL requests from your application are routed via the interface VPC endpoint to AppSync Private API." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/appsync-private-api-sam", + "templateURL": "serverless-patterns/appsync-private-api-sam", + "projectFolder": "appsync-private-api-sam", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AppSync Private API documentation:", + "link": "https://docs.aws.amazon.com/appsync/latest/devguide/using-private-apis.html" + } + ] + }, + "deploy": { + "text": [ + "sam deploy --guided" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: sam delete." + ] + }, + "authors": [ + { + "name": "Ozioma Uzoegwu", + "image": "./Ouzoegwu.jpeg", + "bio": "I am a Principal Solutions Architect working at AWS", + "linkedin": "ouzoegwu", + "twitter": "iam_tessot" + } + ] +} + diff --git a/appsync-private-api-sam/graphql/schema.graphql b/appsync-private-api-sam/graphql/schema.graphql new file mode 100644 index 000000000..f24f5f2ff --- /dev/null +++ b/appsync-private-api-sam/graphql/schema.graphql @@ -0,0 +1,57 @@ +input AddRestaurantInput { + name: String! + state: String + zip: String + cuisine: CuisineType! +} + +enum CuisineType { + Multi + Indian + Chinese + Italian + Thai + American + Continental +} + +input DeleteRestaurantInput { + restaurantId: ID! +} + +type Restaurant { + restaurantId: ID! + name: String! + state: String + zip: String + cuisine: CuisineType +} + +type RestaurantConnection { + items: [Restaurant] + nextToken: String +} + +input UpdateRestaurantInput { + restaurantId: ID! + name: String + state: String + zip: String + cuisine: CuisineType +} + +type Mutation { + addRestaurant(input: AddRestaurantInput!): Restaurant + updateRestaurant(input: UpdateRestaurantInput!): Restaurant + deleteRestaurant(input: DeleteRestaurantInput!): Restaurant +} + +type Query { + listRestaurants(limit: Int, nextToken: String): RestaurantConnection + getRestaurant(restaurantId: ID!): Restaurant +} + +schema { + query: Query + mutation: Mutation +} diff --git a/appsync-private-api-sam/resolvers/addRestaurant.js b/appsync-private-api-sam/resolvers/addRestaurant.js new file mode 100644 index 000000000..4b2704e1a --- /dev/null +++ b/appsync-private-api-sam/resolvers/addRestaurant.js @@ -0,0 +1,10 @@ +import * as ddb from '@aws-appsync/utils/dynamodb'; + +export function request(ctx) { + const key = { restaurantId: util.autoId() }; + const item = ctx.args.input; + const condition = { restaurantId: { attributeExists: false } }; + return ddb.put({ key, item, condition }); +} + +export const response = (ctx) => ctx.result; \ No newline at end of file diff --git a/appsync-private-api-sam/resolvers/deleteRestaurant.js b/appsync-private-api-sam/resolvers/deleteRestaurant.js new file mode 100644 index 000000000..a52ebc6c8 --- /dev/null +++ b/appsync-private-api-sam/resolvers/deleteRestaurant.js @@ -0,0 +1,4 @@ +import * as ddb from '@aws-appsync/utils/dynamodb'; + +export const request = (ctx) => ddb.remove({ key: { restaurantId: ctx.args.input.restaurantId } }); +export const response = (ctx) => ctx.result; \ No newline at end of file diff --git a/appsync-private-api-sam/resolvers/getRestaurant.js b/appsync-private-api-sam/resolvers/getRestaurant.js new file mode 100644 index 000000000..2175c9f92 --- /dev/null +++ b/appsync-private-api-sam/resolvers/getRestaurant.js @@ -0,0 +1,5 @@ +import * as ddb from '@aws-appsync/utils/dynamodb'; + +export const request = (ctx) => ddb.get({ key: { restaurantId: ctx.args.restaurantId } }); + +export const response = (ctx) => ctx.result; \ No newline at end of file diff --git a/appsync-private-api-sam/resolvers/listRestaurants.js b/appsync-private-api-sam/resolvers/listRestaurants.js new file mode 100644 index 000000000..d4fbfdfc1 --- /dev/null +++ b/appsync-private-api-sam/resolvers/listRestaurants.js @@ -0,0 +1,11 @@ +import * as ddb from '@aws-appsync/utils/dynamodb'; + +export function request(ctx) { + const { limit = 10, nextToken } = ctx.args; + return ddb.scan({ limit, nextToken }); +} + +export function response(ctx) { + const { items, nextToken } = ctx.result; + return { items: items ?? [], nextToken }; +} \ No newline at end of file diff --git a/appsync-private-api-sam/resolvers/updateRestaurant.js b/appsync-private-api-sam/resolvers/updateRestaurant.js new file mode 100644 index 000000000..a68251294 --- /dev/null +++ b/appsync-private-api-sam/resolvers/updateRestaurant.js @@ -0,0 +1,16 @@ +import { util } from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; + +export function request(ctx) { + const { restaurantId, ...values } = ctx.args.input; + const condition = { restaurantId: { attributeExists: true } }; + return ddb.update({ key: { restaurantId }, update: values, condition }); +} + +export function response(ctx) { + const { error, result } = ctx; + if (error) { + return util.error(error.message, error.type); + } + return result; +} \ No newline at end of file diff --git a/appsync-private-api-sam/template.yaml b/appsync-private-api-sam/template.yaml new file mode 100644 index 000000000..7ff1b4f70 --- /dev/null +++ b/appsync-private-api-sam/template.yaml @@ -0,0 +1,249 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Deploy AppSync Private API in a VPC, provide the VPC ID and Subnet ID to deploy the AppSync VPC Interface Endpoints + +Parameters: + #### Parameter for VPC ID of the subnets to deploy the AppSync API Interface Endpoint #### + VpcId: + Description: VPC ID + Type: AWS::EC2::VPC::Id + ConstraintDescription: Must be a valid VPC ID. + + #### Parameter for SubnetIds to deploy the AppSync API Interface Endpoint #### + SubnetIds: + Description: Comma-separated list of Subnet IDs + Type: List + ConstraintDescription: Must be a comma-separated list of valid Subnet IDs. + +Resources: + #### AppSync VPC Interface Endpoint #### + AppSyncVPCEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + ServiceName: !Sub "com.amazonaws.${AWS::Region}.appsync-api" + VpcEndpointType: Interface + VpcId: !Ref VpcId + SubnetIds: !Ref SubnetIds + SecurityGroupIds: + - !Ref AppSyncVPCEndpointSecurityGroup + PrivateDnsEnabled: true + + #### AppSync VPC Interface Endpoint Security Group, you can further restrict this security group to only the VPC or Subnet CIDR range #### + AppSyncVPCEndpointSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for AppSync VPC Endpoint + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 # Update for VPC CIDR range + VpcId: !Ref VpcId + + #### DynamoDb Table #### + RestaurantTable: + Type: AWS::Serverless::SimpleTable + Properties: + PrimaryKey: + Name: restaurantId + Type: String + + #### Appsync API #### + AppsyncGraphQLApi: + Type: AWS::AppSync::GraphQLApi + Properties: + Name: !Sub "RestaurantAPI-${AWS::StackName}" + AuthenticationType: API_KEY + LogConfig: + ExcludeVerboseContent: false + FieldLogLevel: "ALL" + CloudWatchLogsRoleArn: !GetAtt AppsyncPushToCloudWatchLogsRole.Arn + XrayEnabled: true + Visibility: PRIVATE + + AppsyncGraphQLApiKey: + Type: AWS::AppSync::ApiKey + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + + AppsyncGraphQLApiSchema: + Type: AWS::AppSync::GraphQLSchema + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + DefinitionS3Location: "./graphql/schema.graphql" + + #### Appsync API Logging #### + + AppsyncGraphQLApiLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/aws/appsync/apis/${AppsyncGraphQLApi.ApiId}" + RetentionInDays: 7 + + AppsyncPushToCloudWatchLogsRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: + - appsync.amazonaws.com + Action: + - sts:AssumeRole + + AppsyncPushToCloudWatchLogsRolePolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: !Sub ${AWS::StackName}-AppsyncPushToCloudWatchLogs-Policy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !GetAtt AppsyncGraphQLApiLogGroup.Arn + Roles: + - !Ref AppsyncPushToCloudWatchLogsRole + + #### Appsync DynamoDB Datasource #### + + AppsyncDynamoDBDatasourceRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - sts:AssumeRole + Principal: + Service: + - appsync.amazonaws.com + + AppsyncDynamoDBDatasourceRolePolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: !Sub ${AWS::StackName}-AppsyncDynamoDB-Policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - dynamodb:DeleteItem + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:Query + - dynamodb:Scan + - dynamodb:UpdateItem + Resource: + - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${RestaurantTable}" + - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${RestaurantTable}/*" + Roles: + - !Ref AppsyncDynamoDBDatasourceRole + + AppsyncGraphQLApiDataSource: + Type: AWS::AppSync::DataSource + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + Name: "Restaurant_Api_Datasource" + Type: "AMAZON_DYNAMODB" + ServiceRoleArn: !GetAtt AppsyncDynamoDBDatasourceRole.Arn + DynamoDBConfig: + AwsRegion: !Sub "${AWS::Region}" + TableName: !Ref RestaurantTable + + #### Appsync Resolvers #### + GetRestaurantResolver: + Type: AWS::AppSync::Resolver + DependsOn: + - AppsyncGraphQLApiSchema + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + TypeName: Query + FieldName: getRestaurant + DataSourceName: !GetAtt AppsyncGraphQLApiDataSource.Name + Runtime: + Name: APPSYNC_JS + RuntimeVersion: "1.0.0" + CodeS3Location: resolvers/getRestaurant.js + + ListRestaurantsResolver: + Type: AWS::AppSync::Resolver + DependsOn: + - AppsyncGraphQLApiSchema + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + TypeName: Query + FieldName: listRestaurants + DataSourceName: !GetAtt AppsyncGraphQLApiDataSource.Name + Runtime: + Name: APPSYNC_JS + RuntimeVersion: "1.0.0" + CodeS3Location: resolvers/listRestaurants.js + + AddRestaurantResolver: + Type: AWS::AppSync::Resolver + DependsOn: + - AppsyncGraphQLApiSchema + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + TypeName: Mutation + FieldName: addRestaurant + DataSourceName: !GetAtt AppsyncGraphQLApiDataSource.Name + Runtime: + Name: APPSYNC_JS + RuntimeVersion: "1.0.0" + CodeS3Location: resolvers/addRestaurant.js + + DeleteRestaurantResolver: + Type: AWS::AppSync::Resolver + DependsOn: + - AppsyncGraphQLApiSchema + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + TypeName: Mutation + FieldName: deleteRestaurant + DataSourceName: !GetAtt AppsyncGraphQLApiDataSource.Name + Runtime: + Name: APPSYNC_JS + RuntimeVersion: "1.0.0" + CodeS3Location: resolvers/deleteRestaurant.js + + UpdateRestaurantResolver: + Type: AWS::AppSync::Resolver + DependsOn: + - AppsyncGraphQLApiSchema + Properties: + ApiId: !GetAtt AppsyncGraphQLApi.ApiId + TypeName: Mutation + FieldName: updateRestaurant + DataSourceName: !GetAtt AppsyncGraphQLApiDataSource.Name + Runtime: + Name: APPSYNC_JS + RuntimeVersion: "1.0.0" + CodeS3Location: resolvers/updateRestaurant.js + +Outputs: + AppSyncGraphQLAPIURL: + Description: "AppSync GraphQL API URL" + Value: !GetAtt AppsyncGraphQLApi.GraphQLUrl + + AppSyncApiKey: + Description: "AppSync API Key" + Value: !GetAtt AppsyncGraphQLApiKey.ApiKey + + AppSyncVPCEndpointDNS: + Description: AppSync VPC Endpoint DNS + Value: !Select + - 1 + - !Split + - ":" + - !Select + - 0 + - !GetAtt + - AppSyncVPCEndpoint + - DnsEntries