[アップデート] AWS AppSync が SAM でデプロイ出来るようになりました

2023.06.24

いわさです。

AWS SAM は CloudFormation の拡張機能で、サーバーレースアプリケーションでよく使われるリソースを CloudFormation よりも抽象化した拡張コンポーネントで定義出来るようにする機能です。
SAM によって迅速にサーバーレスアプリケーションを構築出来るようにすることを目的にしています。

AWS AppSync は AWS マネージドなサーバーレス GraphQL サービスです。
SAM でサポートされていてもおかしくない AppSync でしたが、これまでは SAM でサポートされていませんでした。
そのため、SAM を使ってサーバーレスアプリケーションを管理したい場合でも、AppSync 関係のリソースについては CloudFormation コンポーネントを使う必要がありました。

しかし、本日のアップデートで AWS SAM で AppSync がサポートされました。

今回のアップデートによって、他の SAM コンポーネントと同様に抽象的な定義が出来るようになるはず。

ただ、よく考えてみたら私はそもそも CloudFormation で AppSync を構築したことがありませんでした。
そこで本日は CloudFormation と SAM で次のような基本的な AppSync + DynamoDB な API を作成し、その特徴を比べてみました。

CloudFormation

出来るだけひとつのテンプレートに今回まとめたかったので、リゾルバーやスキーマなども全部インラインです。
作ってみたところ CloudFormation のテンプレートは次のような感じになりました。

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  MyDynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: MyDynamoDBTable
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  MyAppSyncApi:
    Type: AWS::AppSync::GraphQLApi
    Properties:
      Name: MyAppSyncApi
      AuthenticationType: API_KEY
  MyAppSyncApiKey:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId:
        Fn::GetAtt:
          - MyAppSyncApi
          - ApiId
  MyAppSyncDynamoDBDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      Name: MyAppSyncDataSource
      ApiId:
        Fn::GetAtt:
          - MyAppSyncApi
          - ApiId
      Type: AMAZON_DYNAMODB
      ServiceRoleArn:
        Fn::GetAtt:
          - AppSyncDynamoDBRole
          - Arn
      DynamoDBConfig:
        AwsRegion: !Ref AWS::Region
        TableName: !Ref MyDynamoDBTable
  MyAppSyncSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId:
        Fn::GetAtt:
          - MyAppSyncApi
          - ApiId
      Definition: >
        schema {
          query: Query
          mutation: Mutation
        }

        type Query {
          getItem(id: ID!): Item
        }

        type Mutation {
          addItem(id: ID!, hogetext: String!): Item
        }

        type Item {
          id: ID!
          hogetext: String
        }
  GetItemResolver:
    Type: AWS::AppSync::Resolver
    DependsOn: MyAppSyncSchema
    Properties:
      ApiId: !GetAtt MyAppSyncApi.ApiId
      TypeName: Query
      FieldName: getItem
      DataSourceName: !GetAtt MyAppSyncDynamoDBDataSource.Name
      RequestMappingTemplate: |
        {
          "version" : "2017-02-28",
          "operation" : "GetItem",
          "key" : {
            "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
          }
        }
      ResponseMappingTemplate: |
        $util.toJson($ctx.result)
  AddItemResolver:
    Type: AWS::AppSync::Resolver
    DependsOn: MyAppSyncSchema
    Properties:
      ApiId: !GetAtt MyAppSyncApi.ApiId
      TypeName: Mutation
      FieldName: addItem
      DataSourceName: !GetAtt MyAppSyncDynamoDBDataSource.Name
      RequestMappingTemplate: |
          {
            "version": "2017-02-28",
            "operation": "PutItem",
            "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
            },
            "attributeValues": {
              "hogetext": $util.dynamodb.toDynamoDBJson($ctx.args.hogetext)
            }
          }
      ResponseMappingTemplate: |
          {
            "id": $util.toJson($ctx.args.id),
            "hogetext": $util.toJson($ctx.args.hogetext)
          }

  AppSyncDynamoDBRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppSyncDynamoDBRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "appsync.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: AppSyncDynamoDBAccessPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - "dynamodb:BatchGetItem"
                  - "dynamodb:GetItem"
                  - "dynamodb:Query"
                  - "dynamodb:Scan"
                  - "dynamodb:BatchWriteItem"
                  - "dynamodb:PutItem"
                  - "dynamodb:UpdateItem"
                  - "dynamodb:DeleteItem"
                Resource: !Sub
                  - "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MyDynamoDBTable}"
                  - MyDynamoDBTable: !Ref MyDynamoDBTable

作っていて思ったのは、API Gateway とだいぶ似てるなって思いました。 API Gateway と AppSync の共通点として細かいコンポーネントが非常に多いです。
AppSync でいえば、基本的な構成だけで以下をそれぞれ別で定義して参照させる必要があります。(ApiKey あたりは認証方法によりそうだが)

  • AWS::AppSync::GraphQLApi
  • AWS::AppSync::ApiKey
  • AWS::AppSync::DataSource
  • AWS::AppSync::GraphQLSchema
  • AWS::AppSync::Resolver

バラバラなので依存関係に注意が必要です。
特に、Resolver からスキーマのフィールド名を参照するのですが、リソース参照ではなくて名称で指定しているだけなので、Resolver に DependsOn をつけるとか気を配る必要がありました。

あと、CloudFormation だとやはり IAM はしっかり意識する必要がありますね。

こちらのテンプレートで次のように DynamoDB への項目の書き込みと読み込みが出来ます。

SAM

SAM の場合はAWS::Serverless::GraphQLApiひとつで定義出来ます。

定義出来るというか先程バラバラだったものを詰め込む感じではありますが。

AWSTemplateFormatVersion: 2010-09-09
Description: ---
Transform: AWS::Serverless-2016-10-31
Resources:
  MyDynamoDBTable2:
    Type: AWS::Serverless::SimpleTable
    Properties:
       TableName: MyDynamoDBTable2
       PrimaryKey: 
        Name: id
        Type: String
       
  MyGraphQLAPI:
    Type: AWS::Serverless::GraphQLApi
    Properties:
      Auth:
        Type: API_KEY
      ApiKeys:
        MyApiKey:
          Description: my api key
      DataSources:
        DynamoDb:
          ItemsDataSource:
            TableName: !Ref MyDynamoDBTable2
            TableArn: !GetAtt MyDynamoDBTable2.Arn
      SchemaInline: >
        schema {
          query: Query
          mutation: Mutation
        }

        type Query {
          getItem(id: ID!): Item
        }

        type Mutation {
          addItem(id: ID!, hogetext: String!): Item
        }

        type Item {
          id: ID!
          hogetext: String
        }
      Functions:
        addItem:
          Runtime:
            Name: APPSYNC_JS
            Version: 1.0.0
          DataSource: ItemsDataSource
          InlineCode: >
            import { util } from "@aws-appsync/utils";
            export function request(ctx) {
              const { id, hogetext } = ctx.arguments
              return {
                operation: "PutItem",
                key: util.dynamodb.toMapValues({id: id}),
                attributeValues: util.dynamodb.toMapValues({
                  hogetext: hogetext
                })
              };
            }
            export function response(ctx) {
              return ctx.result;
            }
        getItem:
          Runtime:
            Name: APPSYNC_JS
            Version: "1.0.0"
          DataSource: ItemsDataSource
          InlineCode: >
            import { util } from "@aws-appsync/utils";
            export function request(ctx) {
              return dynamoDBGetItemRequest({ id: ctx.args.id });
            }
            export function response(ctx) {
              return ctx.result;
            }
            function dynamoDBGetItemRequest(key) {
              return {
                operation: "GetItem",
                key: util.dynamodb.toMapValues(key),
              };
            }
      Resolvers:
        Mutation:
          addItem:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
            - addItem
        Query:
          getItem:
            Runtime:
              Name: APPSYNC_JS
              Version: "1.0.0"
            Pipeline:
            - getItem

スキーマ定義は CloudFormation のものをそのまま使うことが出来ました。

また、AppSync から DynamoDB へアクセスするためのサービスロールについては次のように自動構成されました。
データソースのプロパティで Connector を使って独自に定義も出来ますが、デフォルトで自動設定してくれるのは便利ですね。
カスタマイズしたい場合は次を参考にしてください。

DynamoDb - AWS Serverless Application Model

CloudFormation と大きく違う点というか、作成していて最初に戸惑ったのはリゾルバーですね。

SAM では JavaScript パイプラインリゾルバーのみサポートされている

SAM の場合はパイプラインリゾルバーのみがサポートされています。

AWS SAM supports JavaScript pipeline resolvers.

AWS::Serverless::GraphQLApi - AWS Serverless Application Model より

以前は Unit Resolver というリゾルバータイプのみが AppSync ではサポートされていて、以前の AppSync では VTL でマッピングテンプレートを定義していることが多いのではないでしょうか。

2022 年 11 月のアップデートでリゾルバーを JavaScript で定義出来るようになりました。

今回作成したテンプレートでは CloudFormation で使っていた VTL をベースに JavaScript パイプラインリゾルバーに変更してみました。
マネジメントコンソール上でも「推奨」という表記があったので、VTL よりもこちらを使いましょうという方針のようですね。

さいごに

本日は AWS AppSync が SAM でデプロイ出来るようになったので、使ってみました。

バラバラで依存関係を気にしなければならなかった問題などは気にしなくてよくなりそうです。
また、権限周りがすっきりするのはやはり SAM のひとつのメリットかなと思いました。カスタム定義したい場合も Connector で抽象化した定義が可能なので。

AppSync リソース自体は、AWS::Serverless::GraphQLApi自体に明示的に指定する必要のある必須プロパティが多いので、CloudFormation と比べて記述量はあまり変わってないですね。

AppSync は理解しておかなければいけない概念が元々多いほうだと思いますが、それらが不要になって単純になったというわけではなさそうです。

JavaScript パイプラインリゾルバーの兼ね合いで CloudFormation から SAM へ、必ずしも単純に移行出来るわけではないという点は注意しておきたいですね。