AppSyncを理解するためにCloudFormationでの構成を深掘りしてみた🚀

AppSyncを理解するためにCloudFormationでの構成を深掘りしてみた🚀

こんにちは!

最近どんどん AppSync の熱が上がってきている当ブログです。今回は AppSyncを理解するために CloudFormation での構成を解説していきます。

AppSync に興味のある方、ぜひご注目いただければ幸いです。

想定する読者

  • AppSync を CloudFormation などでコード管理したいヒト
  • AppSync の構成を理解したいヒト

CloudFormation での AppSync 構成

AppSync のリソースタイプは7つあります。CloudFormation ではこれらを組み合わせ AppSync を構成していきます。

  • AWS::AppSync::ApiCache
  • AWS::AppSync::ApiKey
  • AWS::AppSync::DataSource
  • AWS::AppSync::FunctionConfiguration
  • AWS::AppSync::GraphQLApi
  • AWS::AppSync::GraphQLSchema
  • AWS::AppSync::Resolver
後述するサンプルコードを用いて、実際にデプロイしたものです

本記事では、どの記述が何を行っているかを理解するため、それぞれのリソースごとに解説していきます。最後に全リソースをデプロイするサンプルコードを用意しておりますので、こちらもぜひ参考にしてください。

CloudFormation の記述が何を指しているのかを理解していくうちに、AppSync の仕組みも理解できますので、どうぞお付き合いください。

※サンプルコードのコメントで required と記載のある部分は必須項目です。

AWS::AppSync::ApiCache

このリソースでは API キャッシュの設定を行います。

Resources:
  ApiCache:
    Type: AWS::AppSync::ApiCache
    Properties:
      ApiCachingBehavior: PER_RESOLVER_CACHING #required
      ApiId: !GetAtt GraphQLApi.ApiId #required
      AtRestEncryptionEnabled: false
      TransitEncryptionEnabled: false
      Ttl: 60 #required
      Type: SMALL #required

API キャッシュの設定を行うところになりますので、パラメータ「ApiCachingBehavior」で、FULL_REQUEST_CACHING または PER_RESOLVER_CACHING のどちらかを選択します。前者は名前通りすべてをキャッシュするもので、後者は指定したリゾルバーをキャッシュします。

暗号化は「EncryptoEnabled」と記載のあるパラメータで、インスタンスタイプはパラメータ「Type」で指定することができます。

AWS::AppSync::ApiKey

AppSync > API名 > Settings にあります

このリソースでは API キーの作成を行います。

Resources:
  ApiKey:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId: AppSyncDemoKey #required
      Description: This is demo.
      Expires: 1617148800

API キーを作成するだけの記述なので、特別なことは何もしません。

注意点としては、パラメータ「Expires」の設定です。ここでは API キーの有効期間を設定できるのですが、デフォルトだと7日となりますので、要件に合わせて調整してください。最小 1 日間~最大 365 日間( UNIX 秒)まで設定可能です。

AWS::AppSync::DataSource

このリソースではデータソースの作成を行います。

Resources:
  DataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Description: This is demo.
      DynamoDBConfig:
        AwsRegion: ap-northeast-1
        TableName: AppSyncDemoTable
      Name: AppSyncDemoDataSource #required
      Type: AMAZON_DYNAMODB #required
      ServiceRoleArn: DYNAMODB_ROLE_ARN

パラメータ「TYPE」で指定した型に基づいて記述します。データソースの型には、DynamoDB、Lambda、ElasticSearch などがあります。サンプルコードでは DynamoDB を使用しているため、パラメータ「DynamoDBConfig」を用いています。

AWS::AppSync::FunctionConfiguration

このリソースではパイプラインリゾルバーを設定します。

Resources:
  FunctionConfiguration:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource.Name #required
      Name: AppSyncDemoFuncConf #required
      Description: This is demo.
      FunctionVersion: '2018-05-29' #required
      RequestMappingTemplate: |
        {
          "version": "2017-02-28",
          "operation": "GetItem",
          "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
          }
        }
      ResponseMappingTemplate: |
        $util.toJson($context.result)

対象のデータソースを選択し、リクエストとレスポンスマッピングテンプレートを記述します。こちらのリソースはパイプラインリゾルバーを利用しない場合は不要となります。

AWS::AppSync::GraphQLApi

このリソースでは GraphQL API の作成を行います。

Resources:
  GraphQLApi:
    Type: AWS::AppSync::GraphQLApi
    Properties:
      AuthenticationType: API_KEY #required
      Name: AppSyncDemo #required
      XrayEnabled: false

記述のポイントは、パラメータ「AuthenticationType」を正しく記述するところになります。指定できる API_KEY、AWS_IAM、AMAZON_COGNITO_USER_POOLS、OPENID_CONNECT となっておりますので、要件に合わせて設定してください。

サンプルコードでは、API_KEY を指定しておりますが、ほかのものを設定すると別途パラメータが必要となります。ここでは例として Cognito を利用する場合を記述します。

Resources:
  GraphQLApi:
    Type: AWS::AppSync::GraphQLApi
    Properties:
   AuthenticationType: AMAZON_COGNITO_USER_POOLS
      UserPoolConfig:
        UserPoolId: !Ref UserPool
        AwsRegion: ap-northeast-1
        DefaultAction: DENY

タイプを変更し、パラメータ「UserPoolConfig」を加えました。この通り指定するタイプによって必要なパラメータが変化しますのでご注意ください。

AWS::AppSync::GraphQLSchema

このリソースではスキーマの作成を行います。

Resources:
 GraphQLSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Definition:  |
        schema {
          query: Query
          mutation: Mutation
        }
        # Your Schema

こちらはシンプルに、パラメータ「Definition」にスキーマ定義を記述するのみとなります。S3 を利用される方は、パラメータ「DefinitionS3Location」を使用します。

AWS::AppSync::Resolver

このリソースではリゾルバーの設定を行います。

Resources:
  Resolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource.Name
      TypeName: "Mutation" #required
      FieldName: "createEvent" #required
      Kind: UNIT
      RequestMappingTemplate: |
        {
            "version" : "2017-02-28",
            "operation" : "PutItem",
            "key" : {
                "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
            },
            "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input)
        }
      ResponseMappingTemplate: |
        $util.toJson($context.result)
      CachingConfig:
        CachingKeys: 
          - $context.arguments.id
        Ttl: 60

リゾルバーの設定ではフィールド名を指定して、リクエストとレスポンスマッピングテンプレートを記述します。

パイプラインリゾルバーを利用する場合は、パラメータ「Kind」にて UNIT ではなく、PIPELINE を指定します。加えてパラメータ「PipelineConfig」を記述することで、以下のように設定が可能です。

Resources:
  Resolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource.Name
      TypeName: "Mutation" #required
      FieldName: "createEvent" #required
      Kind: PIPELINE
      RequestMappingTemplate: |
        {}
      ResponseMappingTemplate: |
        $util.toJson($context.result)
      PipelineConfig:
        Functions:
        - !GetAtt FunctionConfiguration.FunctionId

FunctionConfiguration のリソースを CloudFormation で記述することを忘れないようにしましょう。パラメータ「PipelineConfig」で必要となります。

サンプルコード

それではこれまでに紹介したリソースタイプすべてを使って CloudFormation で AppSync のリソースをデプロイしてみます。

Resources:
  ApiCache:
    Type: AWS::AppSync::ApiCache
    Properties:
      ApiCachingBehavior: PER_RESOLVER_CACHING #required
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Ttl: 60 #required
      Type: SMALL #required

  ApiKey:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Expires: 1617148800

  DataSource1:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Name: AppSyncDemoDataSource1 #required
      Type: AMAZON_DYNAMODB #required
      ServiceRoleArn: !GetAtt DynamoDBRole.Arn
      DynamoDBConfig:
        AwsRegion: ap-northeast-1
        TableName: !Ref DynamoDBTable1

  DataSource2:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Name: AppSyncDemoDataSource2 #required
      Type: AMAZON_DYNAMODB #required
      ServiceRoleArn: !GetAtt DynamoDBRole.Arn
      DynamoDBConfig:
        AwsRegion: ap-northeast-1
        TableName: !Ref DynamoDBTable2

  FunctionConfigurationStep1:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource1.Name #required
      Description: This is demo.
      FunctionVersion: '2018-05-29' #required
      Name: AppSyncDemoFuncConf1 #required
      RequestMappingTemplate: |
        {
          "version": "2017-02-28",
          "operation": "GetItem",
          "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
          }
        }
      ResponseMappingTemplate: |
        $util.toJson($context.result)

  FunctionConfigurationStep2:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource2.Name #required
      Description: This is demo.
      FunctionVersion: '2018-05-29' #required
      Name: AppSyncDemoFuncConf2 #required
      RequestMappingTemplate: |
        {
          "version": "2017-02-28",
          "operation": "GetItem",
          "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.prev.result.groupId),
          }
        }
      ResponseMappingTemplate: |
        #if($ctx.error)
            $util.error($ctx.error.message, $ctx.error.type)
        #end
        $util.qr($ctx.prev.result.put("groupName", $ctx.result.groupName))
        $util.toJson($ctx.prev.result)

  GraphQLApi:
    Type: AWS::AppSync::GraphQLApi
    Properties:
      AuthenticationType: API_KEY #required
      Name: AppSyncDemo #required

  GraphQLSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      Definition:  |
        input CreateInputGroup {
          id: String!
          groupName: String!
        }

        input CreateInputUser {
            id: String!
            name: String!
            groupId: String!
        }

        type Mutation {
          createUser(input: CreateInputUser!): Test
          createGroup(input: CreateInputGroup!): Test
        }

        type Query {
          getUser(id: ID!): Test
          getUserAndGroup(id: ID!): Test
        }

        type Test {
          id: ID!
          name: String!
          groupId: String!
          groupName: String
        }

        schema {
          query: Query
          mutation: Mutation
        }

  Resolver1:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource1.Name
      TypeName: "Mutation" #required
      FieldName: "createUser" #required
      Kind: UNIT
      RequestMappingTemplate: |
        {
            "version" : "2017-02-28",
            "operation" : "PutItem",
            "key" : {
                "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
            },
            "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input)
        }
      ResponseMappingTemplate: |
        $util.toJson($ctx.result)

  Resolver2:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource2.Name
      TypeName: "Mutation" #required
      FieldName: "createGroup" #required
      Kind: UNIT
      RequestMappingTemplate: |
        {
            "version" : "2017-02-28",
            "operation" : "PutItem",
            "key" : {
                "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
            },
            "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input)
        }
      ResponseMappingTemplate: |
        $util.toJson($ctx.result)

  Resolver3:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      DataSourceName: !GetAtt DataSource1.Name
      TypeName: "Query" #required
      FieldName: "getUser" #required
      Kind: UNIT
      RequestMappingTemplate: |
        {
          "version": "2017-02-28",
          "operation": "GetItem",
          "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
          }
        }
      ResponseMappingTemplate: |
        $util.toJson($context.result)
      CachingConfig:
        CachingKeys: 
          - $context.arguments.id
        Ttl: 60

  PipelineResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId #required
      TypeName: "Query" #required
      FieldName: "getUserAndGroup" #required
      Kind: PIPELINE
      PipelineConfig:
        Functions: 
          - !GetAtt FunctionConfigurationStep1.FunctionId
          - !GetAtt FunctionConfigurationStep2.FunctionId
      RequestMappingTemplate: |
        {}
      ResponseMappingTemplate: |
        $util.toJson($context.result)

  DynamoDBTable1:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: AppSyncDemoUserTable
      AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
      KeySchema:
      - AttributeName: id
        KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1

  DynamoDBTable2:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: AppSyncDemoGroupTable
      AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
      KeySchema:
      - AttributeName: id
        KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1    
  
  DynamoDBRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppSyncDemoDynamoDBRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Action:
          - sts:AssumeRole
          Principal:
            Service:
            - appsync.amazonaws.com
      Path: '/'
      Policies:
        - PolicyName: DynamoDBRolePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - dynamodb:*           
              Resource: '*'

上記のCloudFormation スタックテンプレートは実際にデプロイすることが可能です。ユーザーやグループを作成したりユーザー情報を取得してみたりして、遊んでみてください。AppSync 以外には、データソースとして必要なDynamoDB、AppSync とデータソースが連携するのに必要な IAM ロールを作成するテンプレートも記載しております。ぜひ参考にしてください。

関連記事

まとめ

今回は7つのリソースタイプを説明していきました。CloudFormation の記述を追っていくごとに AppSync の仕組みが見えてきましたでしょうか?

AWS のコンソール画面と CloudFormation のテンプレートを見比べながら記述しているとより理解が深まりますので、ぜひ試してみてください。

このブログでは、GraphQL や GraphQL のマネージドサービスである AWS AppSync の記事をどんどん公開しておりますので、ご興味のある方はほかの記事もご覧いただければと思います。

AppSync に関する開発は、お気軽にお問い合わせください。