AppSyncのCloudFormation化

# 概要
[アドベントカレンダー](https://qiita.com/advent-calendar/2022/inoue-mn) の1日目から5日目で作成したAppSyncとDynamoDBをCloudFormationで自動作成する方法を紹介します。

# YMLファイルの作成
  ```cfn.yml
    Resources:
      DynamoDBRadioTable:
        Type: AWS::DynamoDB::Table
        Properties:
          TableName: Radio
          AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          KeySchema:
          - AttributeName: id
            KeyType: HASH
          BillingMode: PAY_PER_REQUEST
      
      AppSyncDynamoDBRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: AppSyncDynamoDBRole
          AssumeRolePolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - sts:AssumeRole
              Principal:
                Service:
                - appsync.amazonaws.com
          Path: '/'
          Policies:
          - PolicyName: AppSyncDynamoDBPolicy
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
              - Effect: Allow
                Action:
                - dynamodb:PutItem
                - dynamodb:DeleteItem
                - dynamodb:GetItem
                - dynamodb:Scan
                - dynamodb:Query
                - dynamodb:UpdateItem
                Resource: '*'
      
      ApiCache:
        Type: AWS::AppSync::ApiCache
        Properties:
          ApiCachingBehavior: PER_RESOLVER_CACHING
          ApiId: !GetAtt GraphQLApi.ApiId
          Ttl: 60
          Type: SMALL
      
      DataSourceRadio:
        Type: AWS::AppSync::DataSource
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          Name: Radio
          Type: AMAZON_DYNAMODB
          ServiceRoleArn: !GetAtt AppSyncDynamoDBRole.Arn
          DynamoDBConfig:
            AwsRegion: ap-northeast-1
            TableName: !Ref DynamoDBRadioTable
      
      GraphQLApi:
        Type: AWS::AppSync::GraphQLApi
        Properties:
          AuthenticationType: AWS_IAM
          Name: Radio App
    
      GraphQLSchema:
        Type: AWS::AppSync::GraphQLSchema
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          Definition: |
            input CreateRadioInput {
              id: ID!
              program_name: String
              cast: [String!]
              weekday: Int
              time: String
              favorite: Boolean
            }
    
            input DeleteRadioInput {
              id: ID!
            }
    
            type Mutation {
              createRadio(input: CreateRadioInput!): Radio
              updateRadio(input: UpdateRadioInput!): Radio
              deleteRadio(input: DeleteRadioInput!): Radio
            }
    
            type Query {
              getRadio(id: ID!): Radio
              listRadio(filter: TableRadioFilterInput, limit: Int, nextToken: String): RadioConnection
            }
    
            type Radio {
              id: ID!
              program_name: String
              cast: [String!]
              weekday: Int
              time: String
              favorite: Boolean
            }
    
            type RadioConnection {
              items: [Radio]
              nextToken: String
            }
    
            input TableBooleanFilterInput {
              ne: Boolean
              eq: Boolean
            }
    
            input TableIntFilterInput {
              ne: Int
              eq: Int
              le: Int
              lt: Int
              ge: Int
              gt: Int
              contains: Int
              notContains: Int
              between: [Int]
            }
    
            input TableRadioFilterInput {
              day: TableIntFilterInput
              time: TableStringFilterInput
              favorite: TableBooleanFilterInput
            }
    
            input TableStringFilterInput {
              ne: String
              eq: String
              le: String
              lt: String
              ge: String
              gt: String
              contains: String
              notContains: String
              between: [String]
              beginsWith: String
            }
    
            input UpdateRadioInput {
              id: ID!
              program_name: String
              cast: [String!]
              day: Int
              time: String
              favorite: Boolean
            }
    
      ResolverCreateRadio:
        Type: AWS::AppSync::Resolver
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          DataSourceName: !GetAtt DataSourceRadio.Name
          TypeName: "Mutation"
          FieldName: "createRadio"
          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),
              "condition": {
                  "expression": "attribute_not_exists(#id)",
                  "expressionNames": {
                  "#id": "id"
                  }
              }
            }
          ResponseMappingTemplate: |
            $util.toJson($ctx.result)
      
      ResolverUpdateRadio:
        Type: AWS::AppSync::Resolver
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          DataSourceName: !GetAtt DataSourceRadio.Name
          TypeName: "Mutation"
          FieldName: "updateRadio"
          Kind: UNIT
          RequestMappingTemplate: |
            {
              "version": "2017-02-28",
              "operation": "UpdateItem",
              "key": {
                "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
              },
    
              ## Set up some space to keep track of things we're updating **
              #set( $expNames  = {} )
              #set( $expValues = {} )
              #set( $expSet = {} )
              #set( $expAdd = {} )
              #set( $expRemove = [] )
    
              ## Iterate through each argument, skipping keys **
              #foreach( $entry in $util.map.copyAndRemoveAllKeys($ctx.args.input, ["id"]).entrySet() )
                #if( $util.isNull($entry.value) )
                  ## If the argument is set to "null", then remove that attribute from the item in DynamoDB **
    
                  #set( $discard = ${expRemove.add("#${entry.key}")} )
                  $!{expNames.put("#${entry.key}", "${entry.key}")}
                #else
                  ## Otherwise set (or update) the attribute on the item in DynamoDB **
    
                  $!{expSet.put("#${entry.key}", ":${entry.key}")}
                  $!{expNames.put("#${entry.key}", "${entry.key}")}
                  $!{expValues.put(":${entry.key}", $util.dynamodb.toDynamoDB($entry.value))}
                #end
              #end
    
              ## Start building the update expression, starting with attributes we're going to SET **
              #set( $expression = "" )
              #if( !${expSet.isEmpty()} )
                #set( $expression = "SET" )
                #foreach( $entry in $expSet.entrySet() )
                  #set( $expression = "${expression} ${entry.key} = ${entry.value}" )
                  #if ( $foreach.hasNext )
                    #set( $expression = "${expression}," )
                  #end
                #end
              #end
    
              ## Continue building the update expression, adding attributes we're going to ADD **
              #if( !${expAdd.isEmpty()} )
                #set( $expression = "${expression} ADD" )
                #foreach( $entry in $expAdd.entrySet() )
                  #set( $expression = "${expression} ${entry.key} ${entry.value}" )
                  #if ( $foreach.hasNext )
                    #set( $expression = "${expression}," )
                  #end
                #end
              #end
    
              ## Continue building the update expression, adding attributes we're going to REMOVE **
              #if( !${expRemove.isEmpty()} )
                #set( $expression = "${expression} REMOVE" )
    
                #foreach( $entry in $expRemove )
                  #set( $expression = "${expression} ${entry}" )
                  #if ( $foreach.hasNext )
                    #set( $expression = "${expression}," )
                  #end
                #end
              #end
    
              ## Finally, write the update expression into the document, along with any expressionNames and expressionValues **
              "update": {
                "expression": "${expression}",
                #if( !${expNames.isEmpty()} )
                  "expressionNames": $utils.toJson($expNames),
                #end
                #if( !${expValues.isEmpty()} )
                  "expressionValues": $utils.toJson($expValues),
                #end
              },
    
              "condition": {
                "expression": "attribute_exists(#id)",
                "expressionNames": {
                  "#id": "id",
                },
              }
            }
          ResponseMappingTemplate: |
            $util.toJson($ctx.result)
    
      ResolverDeleteRadio:
        Type: AWS::AppSync::Resolver
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          DataSourceName: !GetAtt DataSourceRadio.Name
          TypeName: "Mutation"
          FieldName: "deleteRadio"
          Kind: UNIT
          RequestMappingTemplate: |
            {
              "version": "2017-02-28",
              "operation": "DeleteItem",
              "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id)
              }
            }
          ResponseMappingTemplate: |
            $util.toJson($ctx.result)
    
      ResolverGetRadio:
        Type: AWS::AppSync::Resolver
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          DataSourceName: !GetAtt DataSourceRadio.Name
          TypeName: "Query"
          FieldName: "getRadio"
          Kind: UNIT
          RequestMappingTemplate: |
            {
              "version": "2017-02-28",
              "operation": "GetItem",
              "key": {
              "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
              }
            }
          ResponseMappingTemplate: |
            $util.toJson($ctx.result)

      ResolverListRadio:
        Type: AWS::AppSync::Resolver
        Properties:
          ApiId: !GetAtt GraphQLApi.ApiId
          DataSourceName: !GetAtt DataSourceRadio.Name
          TypeName: "Query"
          FieldName: "listRadio"
          Kind: UNIT
          RequestMappingTemplate: |
            {
              "version": "2017-02-28",
              "operation": "Scan",
              "filter": #if($context.args.filter) $util.transform.toDynamoDBFilterExpression($ctx.args.filter) #else null #end,
              "limit": $util.defaultIfNull($ctx.args.limit, 200),
              "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null)),
            }
          ResponseMappingTemplate: |
            $util.toJson($ctx.result)
  ```

# スタックの作成
  - CloudFormationのコンソールから[スタックの作成]を選択し、[新しいリソースを使用(標準)]を押下する。
  - ステップ1
  ![ステップ1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2318120/a418c1c2-68a6-1928-28ad-de447ee4ca29.png)
  - ステップ2
  ![ステップ2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2318120/dee72a39-832f-1ddf-0c61-db2814358f24.png)
  - ステップ3
      - デフォルトのまま[次へ]を選択する。
  - ステップ4
      - デフォルトのまま[送信]を押下する。
  - ステータスが``CREATE_COMPLETE``になれば成功です。