【AWS】AWS SAMを使い、CLIでGraphQL APIを開発(AppSync)

AWS

1. 概要

前回はS3+Cloudfrontにウェブコンテンツをデプロイし、サブドメインにアクセスする内容でした。今回はSAMテンプレートを使い、GraphQL APIを開発する内容となります。

2. Nodeのインストール

こちらを参考

3. AWSアカウントにサインアップ

3-1. 前提条件

4. AWSアクセスキーの取得

4-1. AWSアクセスキーの取得

5. AWS CLI のインストール

5-1. インストール

6. AWS SAM CLIのインストール

6-1. インストール

7. アプリケーションを初期化

7-1. init

sam init
  • Which template source would you like to use?
    • AWS Quick Start Templates
  • Choose an AWS Quick Start application template
    • 10 – GraphQLApi Hello World Example
  • Project name
    • sam-graphql-api

8. ソースコード

ディレクトリ構成

.
├── gql
│   ├── createPostItem.js
│   ├── deletePostItem.js
│   ├── getPostFromTable.js
│   ├── greet.js
│   ├── listPostsFromTable.js
│   ├── preprocessPostItem.js
│   ├── schema.graphql
│   └── updatePostItem.js
├── greeter
│   ├── app.mjs
│   ├── package.json
│   └── tests
│       ├── events
│       │   └── appsync.json
│       └── unit
│           └── test-handler.mjs
├── samconfig.toml
└── template.yaml

5 directories, 14 files

※変更のあるもののみを記載

8-1. gql/preprocessPostItem.js

import { util } from "@aws-appsync/utils";

export const request = (ctx) => {
  const id = util.autoId();
  const now = util.time.nowISO8601();
  const item = {
    id,
    author: ctx.args.author,
    title: ctx.args.title,
    content: ctx.args.content,
    ups: 1,
    downs: 0,
    version: 1,
    gsi1pk: ctx.args.category,
    gsi1sk: now,
  };
  return { payload: { id, item } };
};

export const response = (ctx) => {
  return ctx.result;
};

8-2. gql/createPostItem.js

import * as ddb from "@aws-appsync/utils/dynamodb";

export const request = (ctx) => {
  const { id, item } = ctx.prev.result;
  return ddb.put({ key: { id }, item });
};

export const response = (ctx) => ctx.result;

8-3. gql/updatePostItem.js

※新規

import * as ddb from "@aws-appsync/utils/dynamodb";

export const request = (ctx) => {
  const { id, ...rest } = ctx.args;

  const values = Object.entries(rest).reduce((obj, [key, value]) => {
    obj[key] = value ?? ddb.operations.remove();
    return obj;
  }, {});

  return ddb.update({
    key: { id },
    update: { ...values, version: ddb.operations.increment(1) },
  });
};

export const response = (ctx) => ctx.result;

8-4. gql/listPostsFromTable.js

※新規

import * as ddb from "@aws-appsync/utils/dynamodb";

export const request = (ctx) => {
  const { limit = 20, nextToken, category } = ctx.args;
  return ddb.query({
    index: "GSI1",
    query: {
      gsi1pk: { eq: category },
    },
    limit,
    nextToken: nextToken || undefined,
  });
};

export const response = (ctx) => {
  const { items = [], nextToken } = ctx.result;
  return { posts: items, nextToken };
};

8-5. gql/getPostFromTable.js

import * as ddb from "@aws-appsync/utils/dynamodb";

export const request = (ctx) => {
  return ddb.get({ key: { id: ctx.args.id } });
};

export const response = (ctx) => ctx.result;

8-6. gql/deletePostItem.js

※新規

import * as ddb from "@aws-appsync/utils/dynamodb";

export const request = (ctx) => {
  const id = ctx.args.id;
  return ddb.remove({ key: { id } });
};

export const response = (ctx) => ctx.result;

8-7. gql/schema.graphql

schema {
  query: Query
  mutation: Mutation
}

type Query {
  sayHello(name: String): String
  sayGoodbye(name: String): String
  getPost(id: ID!): Post
  listPosts(limit: Int, nextToken: String, category: String!): PaginatedPosts
}

type Mutation {
  addPost(
    category: String!
    author: String!
    title: String!
    content: String!
  ): Post!
  updatePost(id: ID!, author: String, title: String, content: String): Post
  deletePost(id: ID!): Post
}

type Post {
  id: String!
  author: String
  title: String
  content: String
  ups: Int!
  downs: Int!
  version: Int!
}

type PaginatedPosts {
  posts: [Post!]!
  nextToken: String
}

8-7. template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-graphql-api

  Sample SAM Template for sam-graphql-api

Resources:
  PostsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: PostsTable
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
        - AttributeName: gsi1pk
          AttributeType: S
        - AttributeName: gsi1sk
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      GlobalSecondaryIndexes:
        - IndexName: GSI1
          KeySchema:
            - AttributeName: gsi1pk
              KeyType: HASH
            - AttributeName: gsi1sk
              KeyType: RANGE
          Projection:
            ProjectionType: ALL

  Greeter:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: greeter/
      Handler: app.lambdaHandler
      Runtime: nodejs22.x
      Architectures:
        - x86_64

  HelloWorldGraphQLApi:
    Type: AWS::Serverless::GraphQLApi
    Properties:
      SchemaUri: ./gql/schema.graphql
      Auth:
        Type: API_KEY
      ApiKeys:
        MyApiKey:
          Description: my api key
      DataSources:
        DynamoDb:
          Posts:
            TableName: !Ref PostsTable
            TableArn: !GetAtt PostsTable.Arn
        Lambda:
          Greeter:
            FunctionArn: !GetAtt Greeter.Arn
      Functions:
        preprocessPostItem:
          Runtime:
            Name: APPSYNC_JS
            Version: 1.0.0
          DataSource: NONE
          CodeUri: ./gql/preprocessPostItem.js
        createPostItem:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: Posts
          CodeUri: ./gql/createPostItem.js
        updatePostItem:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: Posts
          CodeUri: ./gql/updatePostItem.js
        deletePostItem:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: Posts
          CodeUri: ./gql/deletePostItem.js
        getPostFromTable:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: Posts
          CodeUri: ./gql/getPostFromTable.js
        listPostsFromTable:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: Posts
          CodeUri: ./gql/listPostsFromTable.js
        greet:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: Greeter
          CodeUri: ./gql/greet.js
      Resolvers:
        Mutation:
          addPost:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - preprocessPostItem
              - createPostItem
          updatePost:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - updatePostItem
          deletePost:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - deletePostItem
        Query:
          getPost:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - getPostFromTable
          listPosts:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - listPostsFromTable
          sayHello:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - greet
          sayGoodbye:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
              - greet

Outputs:
  HelloWorldGraphQLApi:
    Description: HelloWorldGraphQLApi endpoint URL for Prod environment
    Value: !GetAtt HelloWorldGraphQLApi.GraphQLUrl
  HelloWorldGraphQLApiMyApiKey:
    Description: API Key for HelloWorldGraphQLApi
    Value: !GetAtt HelloWorldGraphQLApiMyApiKey.ApiKey

9. クラウド上にAPIデプロイし、確認

※必要に応じてログイン

aws sso login

9-1. sam sync

※ルートディレクトリ(sam-graphql-api)にて

sam sync --stack-name sam-graphql-api --watch

.
.
.
CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldGraphQLApi                                                                                                         
Description         HelloWorldGraphQLApi endpoint URL for Prod environment                                                                       
Value               https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql                                          

Key                 HelloWorldGraphQLApiMyApiKey                                                                                                 
Description         API Key for HelloWorldGraphQLApi                                                                                             
Value               da2-abcdefghijklmnopqrstuvwxyz                                                                                               
--------------------------------------------------------------------------------------------------------------------------------------------------

                                                                                                                                                     
Stack creation succeeded. Sync infra completed.                                                                                                      
                                                                                                                                                     
Infra sync completed.
  • GraphQL API Endpoint
    • HelloWorldGraphQLApi
      • https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql
    • API Key
      • da2-abcdefghijklmnopqrstuvwxyz

10. APIを実行して、CRUDを確認

10-1. データを登録

curl -X POST https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: da2-abcdefghijklmnopqrstuvwxyz" \
  -d '{
   "query":"mutation MyMutation($category: String!, $author: String!, $title: String!, $content: String!) { addPost(category: $category, author: $author, title: $title, content: $content) { id } }",
   "variables": {
     "category": "POST",
     "author": "Sondon",
     "title": "Hello",
     "content": "GraphQL API"
   }
}'

{"data":{"addPost":{"id":"629b4ad7-dbb7-4aa7-b96d-50c162e8bbb9"}}}

curl -X POST https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: da2-abcdefghijklmnopqrstuvwxyz" \
  -d '{
   "query":"mutation MyMutation($category: String!, $author: String!, $title: String!, $content: String!) { addPost(category: $category, author: $author, title: $title, content: $content) { id } }",
   "variables": {
     "category": "POST",
     "author": "Saeko",
     "title": "Aroha",
     "content": "Hawaii"
   }
}'

{"data":{"addPost":{"id":"db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf"}}}

10-2. データを取得(複数件)

curl -X POST https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: da2-abcdefghijklmnopqrstuvwxyz" \
  -d '{
   "query":"query ListPosts($limit: Int, $nextToken: String, $category: String!) { listPosts(limit: $limit, nextToken: $nextToken, category: $category) { nextToken posts { id author title content ups downs version } } }",
   "variables": {
     "limit": 10,
     "nextToken": "",
     "category": "POST"
   }
}'

{"data":{"listPosts":{"nextToken":null,"posts":[{"id":"629b4ad7-dbb7-4aa7-b96d-50c162e8bbb9","author":"Sondon","title":"Hello","content":"GraphQL API","ups":1,"downs":0,"version":1},{"id":"db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf","author":"Saeko","title":"Aroha","content":"Hawaii","ups":1,"downs":0,"version":1}]}}}

  • limit: 1
    • データが1件取得される
    • nextTokenが取得される
      • 次のデータを取得する際に指定して使用

10-3. データを更新

curl -X POST https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: da2-abcdefghijklmnopqrstuvwxyz" \
  -d '{
   "query": "mutation UpdatePost($id: ID!, $author: String, $title: String) { updatePost(id: $id, author: $author, title: $title) { id version author title content } }",
   "variables": {
     "id": "db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf",
     "author": "Taylor",
     "title": "Hi"
   }
}'

{"data":{"updatePost":{"id":"db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf","version":2,"author":"Taylor","title":"Hi","content":"Hawaii"}}}

10-4. データを取得(1件)

curl -X POST https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: da2-abcdefghijklmnopqrstuvwxyz" \
  -d '{
   "query":"query GetPost($id: ID!) { getPost(id: $id) { id author title content ups downs version } }",
   "variables": {
     "id": "db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf"
   }
}'

{"data":{"getPost":{"id":"db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf","author":"Taylor","title":"Hi","content":"Hawaii","ups":1,"downs":0,"version":2}}}

10-5. データを削除

curl -X POST https://abcdefghijklmnopqrstuvwxyz.appsync-api.ap-northeast-1.amazonaws.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: da2-abcdefghijklmnopqrstuvwxyz" \
  -d '{
   "query":"mutation DeletePost($id: ID!) { deletePost(id: $id) { id author title content } }",
   "variables": {
     "id": "db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf"
   }
}'

{"data":{"deletePost":{"id":"db8c9fa5-fe24-4ea5-85bb-b433d0e2bbdf","author":"Taylor","title":"Hi","content":"Hawaii"}}}

11. Management Consoleで確認

11-1. 画面で確認

  • DynamoDB

12. AWSクラウドからアプリケーションを削除

※必要に応じ削除

sam delete --stack-name sam-graphql-api
aws cloudformation delete-stack --stack-name sam-graphql-api

13. 備考

今回はSAMテンプレートを使い、GraphQL APIを開発する内容でした。

14. 参考

  1. AWS Serverless Application Model (AWS SAM) とは何ですか? – AWS Serverless Application Model (amazon.com)
  2. とは AWS AppSync – AWS AppSync GraphQL
  3. AWS::Serverless::GraphQLApi – AWS Serverless Application Model
  4. DynamoDB を使用した設計とアーキテクチャの設計に関するベストプラクティス – Amazon DynamoDB

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. AWS

    【AWS】API Gateway + Lambda + SESで汎用メ…

  2. AWS

    【AWS】AWS Step Functionsを触ってみる

  3. AWS

    【AWS】Github ActionsやAWS SAMを使ってAWS …

  4. AWS

    【AWS】APIGatewayのバックアップ、復元

  5. AWS

    NuGetパッケージの管理で「このソースでは利用できません」と表示され…

  6. AWS

    【AWS】AWS SAMを使いCLIでDynamoDBやLambda関…

最近の記事

  1. AWS
  2. AWS

制作実績一覧

  1. Checkeys