【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】AWS Step Functionsを触ってみる

  2. AWS

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

  3. AWS

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

  4. AWS

    【AWS】AWS LambdaとAmazon DynamoDBを使って…

  5. AWS

    【AWS】Amazon DynamoDBを使ってみる(CLI、API)…

  6. AWS

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

最近の記事

制作実績一覧

  1. Checkeys