CloudFormationと「amplify add custom」を使ってAmplifyにカスタムリソースを追加

この記事は、[「AWS Amplify Advent Calendar 2021」](https://qiita.com/advent-calendar/2021/amplify)の12日目の記事です。


## 背景

Amplifyは比較的簡単にwebアプリケーションの基盤を構成するインフラの構築や管理であったりCI/CDの導入などを行うことが可能なサービスである一方で、便利であるが故にAmplifyが用意/想定しているユースケースから違う構成を組もうとすると面倒ということがこれまではありました。

この手の課題はAmplifyだからというものではなく抽象化されたサービスあるあるだと思います。

そんな中、今年11月6日に[リリース](https://aws.amazon.com/jp/about-aws/whats-new/2021/11/aws-amplify-custom-aws-resources-amplify-created-backends-cdk-cloudformation/)されたAmplifyの新しい機能は、CDKやCloudFormaitonを使用してAmplifyプロジェクトのバックエンドに対してAmplify上で追加のAWSリソースを関連付ける事が出来るようになりました。

Amplifyで最小限の機能を持ったwebアプリケーションを構築した後、サービスの需要が伸びてきて新たにあれやこれやと機能を追加したくなった際にそのままAmplifyでできるよというイメージですね。

## サンプルをやってみよう
何事も手を動かすのが一番、先ずは実際にやってみる。

### Reactプロジェクト作成

```sh
npx create-react-app amplified-shopping
cd amplified-shopping
```

Reactプロジェクト初期状態

```sh
amplified-shopping % ll                                                                                     
-rw-r--r--     1 xxxx  staff    3362 12 11 23:36 README.md
drwxr-xr-x  1052 xxxx  staff   33664 12 11 23:36 node_modules
-rw-r--r--     1 xxxx  staff     822 12 11 23:36 package.json
drwxr-xr-x     8 xxxx  staff     256 12 11 23:36 public
drwxr-xr-x    10 xxxx  staff     320 12 11 23:36 src
-rw-r--r--     1 xxxx  staff  510617 12 11 23:36 yarn.lock
```

### Amplifyプロジェクト初期化
※`-y`：デフォルト設定で初期化

```sh
amplify init -y
```

各設定情報

※`-y`無しでinitした場合は下記設定項目をそれぞれ対話的に選択して実行することになります。

```sh
Project information
| Name: amplifiedshopping
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm run-script build
| Start Command: npm run-script start
```

`amplisy init`を実行したことでカレントディレクトリに`amplify`ディレクトリが作成される

```sh
-rw-r--r--     1 xxxx  staff    3362 12 11 23:36 README.md
drwxr-xr-x     9 xxxx  staff     288 12 11 23:41 amplify
drwxr-xr-x  1052 xxxx  staff   33664 12 11 23:36 node_modules
-rw-r--r--     1 xxxx  staff     822 12 11 23:36 package.json
drwxr-xr-x     8 xxxx  staff     256 12 11 23:36 public
drwxr-xr-x    10 xxxx  staff     320 12 11 23:36 src
-rw-r--r--     1 xxxx  staff  510617 12 11 23:36 yarn.lock
```

配下にはinitで生成されたAWSリソースが定義されたCloudFormationがあります。
中身としてはIAMロール2つとS3バケット1つがそれぞれ作成される記述になっています。

※実際はJsonファイルですがyamlの方が読みやすいので置換しています。

<details><summary>【展開】amplify/#current-cloud-backend/awscloudformation/build/root-cloudformation-stack.yaml</summary><div>

```amplify/#current-cloud-backend/awscloudformation/build/root-cloudformation-stack.yaml
Description: Root Stack for AWS Amplify CLI
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  DeploymentBucketName:
    Type: String
    Default: DeploymentBucket
    Description: Name of the common deployment bucket provided by the parent stack
  AuthRoleName:
    Type: String
    Default: AuthRoleName
    Description: Name of the common deployment bucket provided by the parent stack
  UnauthRoleName:
    Type: String
    Default: UnAuthRoleName
    Description: Name of the common deployment bucket provided by the parent stack
Outputs:
  Region:
    Description: CloudFormation provider root stack Region
    Value:
      Ref: AWS::Region
    Export:
      Name:
        Fn::Sub: "${AWS::StackName}-Region"
  StackName:
    Description: CloudFormation provider root stack ID
    Value:
      Ref: AWS::StackName
    Export:
      Name:
        Fn::Sub: "${AWS::StackName}-StackName"
  StackId:
    Description: CloudFormation provider root stack name
    Value:
      Ref: AWS::StackId
    Export:
      Name:
        Fn::Sub: "${AWS::StackName}-StackId"
  AuthRoleArn:
    Value:
      Fn::GetAtt:
      - AuthRole
      - Arn
  UnauthRoleArn:
    Value:
      Fn::GetAtt:
      - UnauthRole
      - Arn
  DeploymentBucketName:
    Description: CloudFormation provider root stack deployment bucket name
    Value:
      Ref: DeploymentBucketName
    Export:
      Name:
        Fn::Sub: "${AWS::StackName}-DeploymentBucketName"
  AuthRoleName:
    Value:
      Ref: AuthRole
  UnauthRoleName:
    Value:
      Ref: UnauthRole
Resources:
  DeploymentBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName:
        Ref: DeploymentBucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  AuthRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: ''
          Effect: Deny
          Principal:
            Federated: cognito-identity.amazonaws.com
          Action: sts:AssumeRoleWithWebIdentity
      RoleName:
        Ref: AuthRoleName
  UnauthRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: ''
          Effect: Deny
          Principal:
            Federated: cognito-identity.amazonaws.com
          Action: sts:AssumeRoleWithWebIdentity
      RoleName:
        Ref: UnauthRoleName
```

</div></details>

CloudFormationのコンソール上でも確認出来ますね。
<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/fadbd43a-2bcc-6c06-1350-df8da48d7bef.png">

また、Amplifyコンソール上でも対象のアプリケーションが作成されています。
まだこの段階では何もないのでこれからapiなどをそれぞれ作成していきます。
<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/c93b2e08-a6d7-7c71-cd33-861e774daf20.png">

### GraphQL API作成

```sh
% amplify add api                                                                          
? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

⚠️  WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules

GraphQL schema compiled successfully.

Edit your schema at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema.graphql or place .graphql files in a directory at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema
✔ Do you want to edit the schema now? (Y/n) · yes
? Choose your default editor: Visual Studio Code
Couldn’t find selected code editor (vscode) on your machine.
? Try opening with system-default editor instead? Yes
✅ Successfully added resource amplifiedshopping locally

✅ Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
```

schema.graphqlを下記に置き換え

```amplify/backend/api/amplifiedshopping/schema.graphql
type ShoppingItem @model { #Creates a database for ShoppingItem 
  id: ID!
  ingredient: String
  quantity: Float
  unit: String
}

type Mutation {
  sendSummaryEmail: Boolean @function(name: "sendSummary-${env}")
}
```

デプロイ

```sh
amplify push
⠙ Fetching updates to backend environment: dev from the cloud.
⚠️  WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules

GraphQL schema compiled successfully.

Edit your schema at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema.graphql or place .graphql files in a directory at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema
✔ Successfully pulled backend environment dev from the cloud.
⠸ Building resource api/amplifiedshopping
⚠️  WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules

GraphQL schema compiled successfully.

Edit your schema at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema.graphql or place .graphql files in a directory at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema

    Current Environment: dev
    
┌──────────┬───────────────────┬───────────┬───────────────────┐
│ Category │ Resource name     │ Operation │ Provider plugin   │
├──────────┼───────────────────┼───────────┼───────────────────┤
│ Api      │ amplifiedshopping │ Create    │ awscloudformation │
└──────────┴───────────────────┴───────────┴───────────────────┘
? Are you sure you want to continue? Yes

⚠️  WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules

GraphQL schema compiled successfully.

Edit your schema at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema.graphql or place .graphql files in a directory at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema
⠸ Building resource api/amplifiedshopping
⚠️  WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules

GraphQL schema compiled successfully.

Edit your schema at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema.graphql or place .graphql files in a directory at /Users/xxxx/amplified-shopping/amplify/backend/api/amplifiedshopping/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
⠴ Updating resources in the cloud. This may take a few minutes...
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
✔ Generated GraphQL operations successfully and saved at ../../../../src/graphql
✔ All resources are updated in the cloud

GraphQL endpoint: https://xxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
GraphQL API KEY: xxxxxxxxxxxxxxxxxxxxx
```

今回もCloudFormationにより各AWSリソースがプロビジョニングされています。

<details><summary>【展開】amplify-amplifiedshopping-dev-234028-apiamplifiedshopping.yaml</summary><div>

```amplify-amplifiedshopping-dev-234028-apiamplifiedshopping.yaml
Parameters:
  env:
    Type: String
    Default: NONE
  DynamoDBModelTableReadIOPS:
    Type: Number
    Default: 5
    Description: The number of read IOPS the table should support.
  DynamoDBModelTableWriteIOPS:
    Type: Number
    Default: 5
    Description: The number of write IOPS the table should support.
  DynamoDBBillingMode:
    Type: String
    Default: PAY_PER_REQUEST
    AllowedValues:
    - PAY_PER_REQUEST
    - PROVISIONED
    Description: Configure @model types to create DynamoDB tables with PAY_PER_REQUEST
      or PROVISIONED billing modes.
  DynamoDBEnablePointInTimeRecovery:
    Type: String
    Default: 'false'
    AllowedValues:
    - 'true'
    - 'false'
    Description: Whether to enable Point in Time Recovery on the table.
  DynamoDBEnableServerSideEncryption:
    Type: String
    Default: 'true'
    AllowedValues:
    - 'true'
    - 'false'
    Description: Enable server side encryption powered by KMS.
  AppSyncApiName:
    Type: String
    Default: AppSyncSimpleTransform
  S3DeploymentBucket:
    Type: String
    Description: An S3 Bucket name where assets are deployed
  S3DeploymentRootKey:
    Type: String
    Description: An S3 key relative to the S3DeploymentBucket that points to the root
      of the deployment directory.
Resources:
  GraphQLAPI:
    Type: AWS::AppSync::GraphQLApi
    Properties:
      AuthenticationType: API_KEY
      Name:
        Fn::Join:
        - ''
        - - Ref: AppSyncApiName
          - "-"
          - Ref: env
  GraphQLAPITransformerSchema3CB2AE18:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId:
        Fn::GetAtt:
        - GraphQLAPI
        - ApiId
      DefinitionS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: S3DeploymentBucket
          - "/"
          - Ref: S3DeploymentRootKey
          - "/schema.graphql"
  GraphQLAPIDefaultApiKey215A6DD7:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId:
        Fn::GetAtt:
        - GraphQLAPI
        - ApiId
      Expires: 1639840236
  GraphQLAPINONEDS95A13CF0:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId:
        Fn::GetAtt:
        - GraphQLAPI
        - ApiId
      Name: NONE_DS
      Type: NONE
      Description: None Data Source for Pipeline functions
  ShoppingItem:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL:
        Fn::Join:
        - ''
        - - https://s3.
          - Ref: AWS::Region
          - "."
          - Ref: AWS::URLSuffix
          - "/"
          - Ref: S3DeploymentBucket
          - "/"
          - Ref: S3DeploymentRootKey
          - "/stacks/ShoppingItem.json"
      Parameters:
        DynamoDBModelTableReadIOPS:
          Ref: DynamoDBModelTableReadIOPS
        DynamoDBModelTableWriteIOPS:
          Ref: DynamoDBModelTableWriteIOPS
        DynamoDBBillingMode:
          Ref: DynamoDBBillingMode
        DynamoDBEnablePointInTimeRecovery:
          Ref: DynamoDBEnablePointInTimeRecovery
        DynamoDBEnableServerSideEncryption:
          Ref: DynamoDBEnableServerSideEncryption
        referencetotransformerrootstackenv10C5A902Ref:
          Ref: env
        referencetotransformerrootstackGraphQLAPI20497F53ApiId:
          Fn::GetAtt:
          - GraphQLAPI
          - ApiId
        referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name:
          Fn::GetAtt:
          - GraphQLAPINONEDS95A13CF0
          - Name
        referencetotransformerrootstackS3DeploymentBucket7592718ARef:
          Ref: S3DeploymentBucket
        referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref:
          Ref: S3DeploymentRootKey
    DependsOn:
    - GraphQLAPITransformerSchema3CB2AE18
  FunctionDirectiveStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL:
        Fn::Join:
        - ''
        - - https://s3.
          - Ref: AWS::Region
          - "."
          - Ref: AWS::URLSuffix
          - "/"
          - Ref: S3DeploymentBucket
          - "/"
          - Ref: S3DeploymentRootKey
          - "/stacks/FunctionDirectiveStack.json"
      Parameters:
        referencetotransformerrootstackenv10C5A902Ref:
          Ref: env
        referencetotransformerrootstackGraphQLAPI20497F53ApiId:
          Fn::GetAtt:
          - GraphQLAPI
          - ApiId
        referencetotransformerrootstackS3DeploymentBucket7592718ARef:
          Ref: S3DeploymentBucket
        referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref:
          Ref: S3DeploymentRootKey
    DependsOn:
    - GraphQLAPITransformerSchema3CB2AE18
  CustomResourcesjson:
    Type: AWS::CloudFormation::Stack
    Properties:
      Parameters:
        AppSyncApiId:
          Fn::GetAtt:
          - GraphQLAPI
          - ApiId
        AppSyncApiName:
          Ref: AppSyncApiName
        env:
          Ref: env
        S3DeploymentBucket:
          Ref: S3DeploymentBucket
        S3DeploymentRootKey:
          Ref: S3DeploymentRootKey
      TemplateURL:
        Fn::Join:
        - "/"
        - - https://s3.amazonaws.com
          - Ref: S3DeploymentBucket
          - Ref: S3DeploymentRootKey
          - stacks
          - CustomResources.json
    DependsOn:
    - GraphQLAPI
    - GraphQLAPITransformerSchema3CB2AE18
    - ShoppingItem
    - FunctionDirectiveStack
Outputs:
  GraphQLAPIKeyOutput:
    Description: Your GraphQL API ID.
    Value:
      Fn::GetAtt:
      - GraphQLAPIDefaultApiKey215A6DD7
      - ApiKey
    Export:
      Name:
        Fn::Join:
        - ":"
        - - Ref: AWS::StackName
          - GraphQLApiKey
  GraphQLAPIIdOutput:
    Description: Your GraphQL API ID.
    Value:
      Fn::GetAtt:
      - GraphQLAPI
      - ApiId
    Export:
      Name:
        Fn::Join:
        - ":"
        - - Ref: AWS::StackName
          - GraphQLApiId
  GraphQLAPIEndpointOutput:
    Description: Your GraphQL API endpoint.
    Value:
      Fn::GetAtt:
      - GraphQLAPI
      - GraphQLUrl
    Export:
      Name:
        Fn::Join:
        - ":"
        - - Ref: AWS::StackName
          - GraphQLApiEndpoint
Description: '{"createdOn":"Mac","createdBy":"Amplify","createdWith":"7.6.3","stackType":"api-AppSync","metadata":{}}'
```

</div><details>

<details><summary>【展開】amplify-amplifiedshopping-dev-234028-apiamplifiedshopping-180U9-FunctionDirectiveStack-1HF5ANTFE59F1.yaml</summary><div>

```amplify-amplifiedshopping-dev-234028-apiamplifiedshopping-180U9-FunctionDirectiveStack-1HF5ANTFE59F1.yaml
Description: An auto-generated nested stack for the @function directive.
AWSTemplateFormatVersion: '2010-09-09'
Conditions:
  HasEnvironmentParameter:
    Fn::Not:
    - Fn::Equals:
      - Ref: referencetotransformerrootstackenv10C5A902Ref
      - NONE
Resources:
  SendSummaryLambdaDataSourceServiceRole5FDDC207:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: appsync.amazonaws.com
        Version: '2012-10-17'
  SendSummaryLambdaDataSourceServiceRoleDefaultPolicy89AA0443:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
        - Action: lambda:InvokeFunction
          Effect: Allow
          Resource:
            Fn::If:
            - HasEnvironmentParameter
            - Fn::Sub:
              - arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:sendSummary-${env}
              - env:
                  Ref: referencetotransformerrootstackenv10C5A902Ref
            - Fn::Sub: arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:sendSummary
        Version: '2012-10-17'
      PolicyName: SendSummaryLambdaDataSourceServiceRoleDefaultPolicy89AA0443
      Roles:
      - Ref: SendSummaryLambdaDataSourceServiceRole5FDDC207
  SendSummaryLambdaDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      Name: SendSummaryLambdaDataSource
      Type: AWS_LAMBDA
      LambdaConfig:
        LambdaFunctionArn:
          Fn::If:
          - HasEnvironmentParameter
          - Fn::Sub:
            - arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:sendSummary-${env}
            - env:
                Ref: referencetotransformerrootstackenv10C5A902Ref
          - Fn::Sub: arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:sendSummary
      ServiceRoleArn:
        Fn::GetAtt:
        - SendSummaryLambdaDataSourceServiceRole5FDDC207
        - Arn
  InvokeSendSummaryLambdaDataSourceInvokeSendSummaryLambdaDataSourceAppSyncFunction653F6BEF:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Fn::GetAtt:
        - SendSummaryLambdaDataSource
        - Name
      FunctionVersion: '2018-05-29'
      Name: InvokeSendSummaryLambdaDataSource
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/InvokeSendSummaryLambdaDataSource.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/InvokeSendSummaryLambdaDataSource.res.vtl"
    DependsOn:
    - SendSummaryLambdaDataSource
  MutationsendSummaryEmailResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: sendSummaryEmail
      TypeName: Mutation
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - InvokeSendSummaryLambdaDataSourceInvokeSendSummaryLambdaDataSourceAppSyncFunction653F6BEF
          - FunctionId
      RequestMappingTemplate: |-
        ## [Start] Stash resolver specific context.. **
        $util.qr($ctx.stash.put("typeName", "Mutation"))
        $util.qr($ctx.stash.put("fieldName", "sendSummaryEmail"))
        {}
        ## [End] Stash resolver specific context.. **
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.sendSummaryEmail.res.vtl"
Parameters:
  referencetotransformerrootstackenv10C5A902Ref:
    Type: String
  referencetotransformerrootstackGraphQLAPI20497F53ApiId:
    Type: String
  referencetotransformerrootstackS3DeploymentBucket7592718ARef:
    Type: String
  referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref:
    Type: String
```

</div><details>

<details><summary>【展開】amplify-amplifiedshopping-dev-234028-apiamplifiedshopping-180U94VGK18QI-ShoppingItem.yaml</summary><div>

```amplify-amplifiedshopping-dev-234028-apiamplifiedshopping-180U94VGK18QI-ShoppingItem.yaml
Parameters:
  DynamoDBModelTableReadIOPS:
    Type: Number
    Default: 5
    Description: The number of read IOPS the table should support.
  DynamoDBModelTableWriteIOPS:
    Type: Number
    Default: 5
    Description: The number of write IOPS the table should support.
  DynamoDBBillingMode:
    Type: String
    Default: PAY_PER_REQUEST
    AllowedValues:
    - PAY_PER_REQUEST
    - PROVISIONED
    Description: Configure @model types to create DynamoDB tables with PAY_PER_REQUEST
      or PROVISIONED billing modes.
  DynamoDBEnablePointInTimeRecovery:
    Type: String
    Default: 'false'
    AllowedValues:
    - 'true'
    - 'false'
    Description: Whether to enable Point in Time Recovery on the table.
  DynamoDBEnableServerSideEncryption:
    Type: String
    Default: 'true'
    AllowedValues:
    - 'true'
    - 'false'
    Description: Enable server side encryption powered by KMS.
  referencetotransformerrootstackenv10C5A902Ref:
    Type: String
  referencetotransformerrootstackGraphQLAPI20497F53ApiId:
    Type: String
  referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name:
    Type: String
  referencetotransformerrootstackS3DeploymentBucket7592718ARef:
    Type: String
  referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref:
    Type: String
Conditions:
  HasEnvironmentParameter:
    Fn::Not:
    - Fn::Equals:
      - Ref: referencetotransformerrootstackenv10C5A902Ref
      - NONE
  ShouldUseServerSideEncryption:
    Fn::Equals:
    - Ref: DynamoDBEnableServerSideEncryption
    - 'true'
  ShouldUsePayPerRequestBilling:
    Fn::Equals:
    - Ref: DynamoDBBillingMode
    - PAY_PER_REQUEST
  ShouldUsePointInTimeRecovery:
    Fn::Equals:
    - Ref: DynamoDBEnablePointInTimeRecovery
    - 'true'
Resources:
  ShoppingItemTable:
    Type: AWS::DynamoDB::Table
    Properties:
      KeySchema:
      - AttributeName: id
        KeyType: HASH
      AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
      BillingMode:
        Fn::If:
        - ShouldUsePayPerRequestBilling
        - PAY_PER_REQUEST
        - Ref: AWS::NoValue
      PointInTimeRecoverySpecification:
        Fn::If:
        - ShouldUsePointInTimeRecovery
        - PointInTimeRecoveryEnabled: true
        - Ref: AWS::NoValue
      ProvisionedThroughput:
        Fn::If:
        - ShouldUsePayPerRequestBilling
        - Ref: AWS::NoValue
        - ReadCapacityUnits:
            Ref: DynamoDBModelTableReadIOPS
          WriteCapacityUnits:
            Ref: DynamoDBModelTableWriteIOPS
      SSESpecification:
        SSEEnabled:
          Fn::If:
          - ShouldUseServerSideEncryption
          - true
          - false
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      TableName:
        Fn::Join:
        - ''
        - - ShoppingItem-
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - "-"
          - Ref: referencetotransformerrootstackenv10C5A902Ref
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  ShoppingItemIAMRole6DDC2C52:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: appsync.amazonaws.com
        Version: '2012-10-17'
      RoleName:
        Fn::Join:
        - ''
        - - ShoppingItemIAMRole59c021-
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - "-"
          - Ref: referencetotransformerrootstackenv10C5A902Ref
  ShoppingItemIAMRoleDefaultPolicyCAD24A0A:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
        - Action:
          - dynamodb:BatchGetItem
          - dynamodb:GetRecords
          - dynamodb:GetShardIterator
          - dynamodb:Query
          - dynamodb:GetItem
          - dynamodb:Scan
          - dynamodb:ConditionCheckItem
          - dynamodb:BatchWriteItem
          - dynamodb:PutItem
          - dynamodb:UpdateItem
          - dynamodb:DeleteItem
          Effect: Allow
          Resource:
          - Fn::GetAtt:
            - ShoppingItemTable
            - Arn
          - Ref: AWS::NoValue
        Version: '2012-10-17'
      PolicyName: ShoppingItemIAMRoleDefaultPolicyCAD24A0A
      Roles:
      - Ref: ShoppingItemIAMRole6DDC2C52
  DynamoDBAccess71ABE5AE:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
        - Action:
          - dynamodb:BatchGetItem
          - dynamodb:BatchWriteItem
          - dynamodb:PutItem
          - dynamodb:DeleteItem
          - dynamodb:GetItem
          - dynamodb:Scan
          - dynamodb:Query
          - dynamodb:UpdateItem
          Effect: Allow
          Resource:
          - Fn::Sub:
            - arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}
            - tablename:
                Fn::Join:
                - ''
                - - ShoppingItem-
                  - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
                  - "-"
                  - Ref: referencetotransformerrootstackenv10C5A902Ref
          - Fn::Sub:
            - arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*
            - tablename:
                Fn::Join:
                - ''
                - - ShoppingItem-
                  - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
                  - "-"
                  - Ref: referencetotransformerrootstackenv10C5A902Ref
        Version: '2012-10-17'
      PolicyName: DynamoDBAccess71ABE5AE
      Roles:
      - Ref: ShoppingItemIAMRole6DDC2C52
  ShoppingItemDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      Name: ShoppingItemTable
      Type: AMAZON_DYNAMODB
      DynamoDBConfig:
        AwsRegion:
          Ref: AWS::Region
        TableName:
          Ref: ShoppingItemTable
      ServiceRoleArn:
        Fn::GetAtt:
        - ShoppingItemIAMRole6DDC2C52
        - Arn
    DependsOn:
    - ShoppingItemIAMRole6DDC2C52
  QuerygetShoppingItempostAuth0FunctionQuerygetShoppingItempostAuth0FunctionAppSyncFunctionD0FB91E4:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: QuerygetShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Query.getShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  QueryGetShoppingItemDataResolverFnQueryGetShoppingItemDataResolverFnAppSyncFunctionEB8045E2:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Fn::GetAtt:
        - ShoppingItemDataSource
        - Name
      FunctionVersion: '2018-05-29'
      Name: QueryGetShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Query.getShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Query.getShoppingItem.res.vtl"
    DependsOn:
    - ShoppingItemDataSource
  GetShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: getShoppingItem
      TypeName: Query
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - QuerygetShoppingItempostAuth0FunctionQuerygetShoppingItempostAuth0FunctionAppSyncFunctionD0FB91E4
          - FunctionId
        - Fn::GetAtt:
          - QueryGetShoppingItemDataResolverFnQueryGetShoppingItemDataResolverFnAppSyncFunctionEB8045E2
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Query"))
            $util.qr($ctx.stash.put("fieldName", "getShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "AMAZON_DYNAMODB"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))
            $util.qr($ctx.stash.put("tableName", "
          - Ref: ShoppingItemTable
          - |-
            "))
            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  QuerylistShoppingItemspostAuth0FunctionQuerylistShoppingItemspostAuth0FunctionAppSyncFunctionE4B7F5A2:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: QuerylistShoppingItemspostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Query.listShoppingItems.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  QueryListShoppingItemsDataResolverFnQueryListShoppingItemsDataResolverFnAppSyncFunctionCDEBD9F6:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Fn::GetAtt:
        - ShoppingItemDataSource
        - Name
      FunctionVersion: '2018-05-29'
      Name: QueryListShoppingItemsDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Query.listShoppingItems.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Query.listShoppingItems.res.vtl"
    DependsOn:
    - ShoppingItemDataSource
  ListShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: listShoppingItems
      TypeName: Query
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - QuerylistShoppingItemspostAuth0FunctionQuerylistShoppingItemspostAuth0FunctionAppSyncFunctionE4B7F5A2
          - FunctionId
        - Fn::GetAtt:
          - QueryListShoppingItemsDataResolverFnQueryListShoppingItemsDataResolverFnAppSyncFunctionCDEBD9F6
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Query"))
            $util.qr($ctx.stash.put("fieldName", "listShoppingItems"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "AMAZON_DYNAMODB"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))
            $util.qr($ctx.stash.put("tableName", "
          - Ref: ShoppingItemTable
          - |-
            "))
            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  MutationcreateShoppingIteminit0FunctionMutationcreateShoppingIteminit0FunctionAppSyncFunctionA6C0AC09:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: MutationcreateShoppingIteminit0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.createShoppingItem.init.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  MutationcreateShoppingItempostAuth0FunctionMutationcreateShoppingItempostAuth0FunctionAppSyncFunctionE48AE597:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: MutationcreateShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.createShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  MutationCreateShoppingItemDataResolverFnMutationCreateShoppingItemDataResolverFnAppSyncFunction1FB43923:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Fn::GetAtt:
        - ShoppingItemDataSource
        - Name
      FunctionVersion: '2018-05-29'
      Name: MutationCreateShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.createShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.createShoppingItem.res.vtl"
    DependsOn:
    - ShoppingItemDataSource
  CreateShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: createShoppingItem
      TypeName: Mutation
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - MutationcreateShoppingIteminit0FunctionMutationcreateShoppingIteminit0FunctionAppSyncFunctionA6C0AC09
          - FunctionId
        - Fn::GetAtt:
          - MutationcreateShoppingItempostAuth0FunctionMutationcreateShoppingItempostAuth0FunctionAppSyncFunctionE48AE597
          - FunctionId
        - Fn::GetAtt:
          - MutationCreateShoppingItemDataResolverFnMutationCreateShoppingItemDataResolverFnAppSyncFunction1FB43923
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Mutation"))
            $util.qr($ctx.stash.put("fieldName", "createShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "AMAZON_DYNAMODB"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))
            $util.qr($ctx.stash.put("tableName", "
          - Ref: ShoppingItemTable
          - |-
            "))
            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  MutationupdateShoppingIteminit0FunctionMutationupdateShoppingIteminit0FunctionAppSyncFunction03DE626C:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: MutationupdateShoppingIteminit0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.updateShoppingItem.init.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  MutationupdateShoppingItempostAuth0FunctionMutationupdateShoppingItempostAuth0FunctionAppSyncFunction740FBEF0:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: MutationupdateShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.updateShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  MutationUpdateShoppingItemDataResolverFnMutationUpdateShoppingItemDataResolverFnAppSyncFunctionBABBDB50:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Fn::GetAtt:
        - ShoppingItemDataSource
        - Name
      FunctionVersion: '2018-05-29'
      Name: MutationUpdateShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.updateShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.updateShoppingItem.res.vtl"
    DependsOn:
    - ShoppingItemDataSource
  UpdateShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: updateShoppingItem
      TypeName: Mutation
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - MutationupdateShoppingIteminit0FunctionMutationupdateShoppingIteminit0FunctionAppSyncFunction03DE626C
          - FunctionId
        - Fn::GetAtt:
          - MutationupdateShoppingItempostAuth0FunctionMutationupdateShoppingItempostAuth0FunctionAppSyncFunction740FBEF0
          - FunctionId
        - Fn::GetAtt:
          - MutationUpdateShoppingItemDataResolverFnMutationUpdateShoppingItemDataResolverFnAppSyncFunctionBABBDB50
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Mutation"))
            $util.qr($ctx.stash.put("fieldName", "updateShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "AMAZON_DYNAMODB"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))
            $util.qr($ctx.stash.put("tableName", "
          - Ref: ShoppingItemTable
          - |-
            "))
            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  MutationdeleteShoppingItempostAuth0FunctionMutationdeleteShoppingItempostAuth0FunctionAppSyncFunctionD47D4CA7:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: MutationdeleteShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.deleteShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  MutationDeleteShoppingItemDataResolverFnMutationDeleteShoppingItemDataResolverFnAppSyncFunction538640D0:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Fn::GetAtt:
        - ShoppingItemDataSource
        - Name
      FunctionVersion: '2018-05-29'
      Name: MutationDeleteShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.deleteShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Mutation.deleteShoppingItem.res.vtl"
    DependsOn:
    - ShoppingItemDataSource
  DeleteShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: deleteShoppingItem
      TypeName: Mutation
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - MutationdeleteShoppingItempostAuth0FunctionMutationdeleteShoppingItempostAuth0FunctionAppSyncFunctionD47D4CA7
          - FunctionId
        - Fn::GetAtt:
          - MutationDeleteShoppingItemDataResolverFnMutationDeleteShoppingItemDataResolverFnAppSyncFunction538640D0
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Mutation"))
            $util.qr($ctx.stash.put("fieldName", "deleteShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "AMAZON_DYNAMODB"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))
            $util.qr($ctx.stash.put("tableName", "
          - Ref: ShoppingItemTable
          - |-
            "))
            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  SubscriptiononCreateShoppingItempostAuth0FunctionSubscriptiononCreateShoppingItempostAuth0FunctionAppSyncFunction6A23BE99:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: SubscriptiononCreateShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onCreateShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  SubscriptionOnCreateShoppingItemDataResolverFnSubscriptionOnCreateShoppingItemDataResolverFnAppSyncFunction189F1792:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: SubscriptionOnCreateShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onCreateShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onCreateShoppingItem.res.vtl"
  SubscriptiononCreateShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: onCreateShoppingItem
      TypeName: Subscription
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - SubscriptiononCreateShoppingItempostAuth0FunctionSubscriptiononCreateShoppingItempostAuth0FunctionAppSyncFunction6A23BE99
          - FunctionId
        - Fn::GetAtt:
          - SubscriptionOnCreateShoppingItemDataResolverFnSubscriptionOnCreateShoppingItemDataResolverFnAppSyncFunction189F1792
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Subscription"))
            $util.qr($ctx.stash.put("fieldName", "onCreateShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "NONE"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))

            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  SubscriptiononUpdateShoppingItempostAuth0FunctionSubscriptiononUpdateShoppingItempostAuth0FunctionAppSyncFunctionA50D451B:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: SubscriptiononUpdateShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onUpdateShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  SubscriptionOnUpdateShoppingItemDataResolverFnSubscriptionOnUpdateShoppingItemDataResolverFnAppSyncFunctionAE9ECF37:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: SubscriptionOnUpdateShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onUpdateShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onUpdateShoppingItem.res.vtl"
  SubscriptiononUpdateShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: onUpdateShoppingItem
      TypeName: Subscription
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - SubscriptiononUpdateShoppingItempostAuth0FunctionSubscriptiononUpdateShoppingItempostAuth0FunctionAppSyncFunctionA50D451B
          - FunctionId
        - Fn::GetAtt:
          - SubscriptionOnUpdateShoppingItemDataResolverFnSubscriptionOnUpdateShoppingItemDataResolverFnAppSyncFunctionAE9ECF37
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Subscription"))
            $util.qr($ctx.stash.put("fieldName", "onUpdateShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "NONE"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))

            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
  SubscriptiononDeleteShoppingItempostAuth0FunctionSubscriptiononDeleteShoppingItempostAuth0FunctionAppSyncFunction6E719826:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: SubscriptiononDeleteShoppingItempostAuth0Function
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onDeleteShoppingItem.postAuth.1.req.vtl"
      ResponseMappingTemplate: "$util.toJson({})"
  SubscriptionOnDeleteShoppingItemDataResolverFnSubscriptionOnDeleteShoppingItemDataResolverFnAppSyncFunctionC61ED2BF:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      DataSourceName:
        Ref: referencetotransformerrootstackGraphQLAPINONEDS2BA9D1C8Name
      FunctionVersion: '2018-05-29'
      Name: SubscriptionOnDeleteShoppingItemDataResolverFn
      RequestMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onDeleteShoppingItem.req.vtl"
      ResponseMappingTemplateS3Location:
        Fn::Join:
        - ''
        - - s3://
          - Ref: referencetotransformerrootstackS3DeploymentBucket7592718ARef
          - "/"
          - Ref: referencetotransformerrootstackS3DeploymentRootKeyA71EA735Ref
          - "/resolvers/Subscription.onDeleteShoppingItem.res.vtl"
  SubscriptiononDeleteShoppingItemResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId:
        Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
      FieldName: onDeleteShoppingItem
      TypeName: Subscription
      Kind: PIPELINE
      PipelineConfig:
        Functions:
        - Fn::GetAtt:
          - SubscriptiononDeleteShoppingItempostAuth0FunctionSubscriptiononDeleteShoppingItempostAuth0FunctionAppSyncFunction6E719826
          - FunctionId
        - Fn::GetAtt:
          - SubscriptionOnDeleteShoppingItemDataResolverFnSubscriptionOnDeleteShoppingItemDataResolverFnAppSyncFunctionC61ED2BF
          - FunctionId
      RequestMappingTemplate:
        Fn::Join:
        - ''
        - - |-
            $util.qr($ctx.stash.put("typeName", "Subscription"))
            $util.qr($ctx.stash.put("fieldName", "onDeleteShoppingItem"))
            $util.qr($ctx.stash.put("conditions", []))
            $util.qr($ctx.stash.put("metadata", {}))
            $util.qr($ctx.stash.metadata.put("dataSourceType", "NONE"))
            $util.qr($ctx.stash.metadata.put("apiId", "
          - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - |-
            "))

            $util.toJson({})
      ResponseMappingTemplate: "$util.toJson($ctx.prev.result)"
Outputs:
  GetAttShoppingItemTableStreamArn:
    Description: Your DynamoDB table StreamArn.
    Value:
      Fn::GetAtt:
      - ShoppingItemTable
      - StreamArn
    Export:
      Name:
        Fn::Join:
        - ":"
        - - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - GetAtt:ShoppingItemTable:StreamArn
  GetAttShoppingItemTableName:
    Description: Your DynamoDB table name.
    Value:
      Ref: ShoppingItemTable
    Export:
      Name:
        Fn::Join:
        - ":"
        - - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - GetAtt:ShoppingItemTable:Name
  GetAttShoppingItemDataSourceName:
    Description: Your model DataSource name.
    Value:
      Fn::GetAtt:
      - ShoppingItemDataSource
      - Name
    Export:
      Name:
        Fn::Join:
        - ":"
        - - Ref: referencetotransformerrootstackGraphQLAPI20497F53ApiId
          - GetAtt:ShoppingItemDataSource:Name
```

</div><details>


<details><summary>【展開】amplify-amplifiedshopping-dev-234028-apiamplifiedshopping-180U94VG-CustomResourcesjson.yaml</summary><div>

```amplify-amplifiedshopping-dev-234028-apiamplifiedshopping-180U94VG-CustomResourcesjson.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: An auto-generated nested stack.
Metadata: {}
Parameters:
  AppSyncApiId:
    Type: String
    Description: The id of the AppSync API associated with this project.
  AppSyncApiName:
    Type: String
    Description: The name of the AppSync API
    Default: AppSyncSimpleTransform
  env:
    Type: String
    Description: The environment name. e.g. Dev, Test, or Production
    Default: NONE
  S3DeploymentBucket:
    Type: String
    Description: The S3 bucket containing all deployment assets for the project.
  S3DeploymentRootKey:
    Type: String
    Description: |-
      An S3 key relative to the S3DeploymentBucket that points to the root
      of the deployment directory.
Resources:
  EmptyResource:
    Type: Custom::EmptyResource
    Condition: AlwaysFalse
Conditions:
  HasEnvironmentParameter:
    Fn::Not:
    - Fn::Equals:
      - Ref: env
      - NONE
  AlwaysFalse:
    Fn::Equals:
    - 'true'
    - 'false'
Outputs:
  EmptyOutput:
    Description: An empty output. You may delete this if you have at least one resource
      above.
    Value: ''
```

</div><details>

### AmazonSNSトピックをカスタムAWSリソースとしてAmplifyプロジェクトに追加

```sh
amplify add custom
```

sampleではCDKを使用していますが、普段はCloudFormationを使うことが多いので今回はCloudFormationを選択しました。

```sh
amplifiedshopping % amplify add custom
? How do you want to define this custom resource? …  (Use arrow keys or type to filter)
　　　　 AWS CDK
❯  AWS CloudFormation
```

```sh
amplified-shopping % amplify add custom                                                                     ?[main]
✔ How do you want to define this custom resource? · AWS CloudFormation
✔ Provide a name for your custom resource · customResource4da9f265
✔ Do you want to access Amplify generated resources in your custom CloudFormation file? (y/N) · yes
? Select the categories you want this custom resource to have access to. custom
⚠️ No resources found for custom
✅ Created skeleton CloudFormation stack in amplify/backend/custom/customResource4da9f265 directory
✔ Do you want to edit the CloudFormation stack now? (Y/n) · no
```

snsトピックを作成するCloudFormationを作成

※Endpointには任意のアドレスを入力

```amplified-shopping/amplify/backend/custom/customResource4da9f265/customResource4da9f265-cloudformation-template.json
{
  "Description": "sns-topic",
  "Parameters": {
    "env": {
      "Type": "String",
      "Description": "Current Amplify CLI env name"
    }
  },
  "Resources": {
    "snstopic": {
      "Type": "AWS::SNS::Topic",
      "Properties": {
        "TopicName": {
          "Fn::Join": [
            "",
            [
              "sns-topic-amplifiedshopping-",
              {
                "Ref": "env"
              }
            ]
          ]
        }
      }
    },
    "snstopicsubscription": {
      "Type": "AWS::SNS::Subscription",
      "Properties": {
        "Protocol": "email",
        "TopicArn": {
          "Ref": "snstopic"
        },
        "Endpoint": "任意のアドレス"
      }
    }
  }
}
```

apiとカスタムリソースをデプロイ

```sh
amplify push -y
```

デプロイが完了するとAWS　AppsyncやDynamoDB、SNS関連のリソースがそれぞれ作成される。
また、Amplifyコンソール上ではBackend側のAPIタブからGraphQLAPIが作成されていることが確認出来るかと思います。
加えて、AWSから指定したアドレスに対して以下のお知らせが送信されているので`Confirm subscription`のリンクをクリックしてSNSのsubscribedを完了させます。

<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/d1821e35-c429-26ec-9946-52fd29772c72.png">

<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/3c5fc993-203e-c080-bbd9-a9fa6ab45f69.png">


届いたメール

```
You have chosen to subscribe to the topic:
arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:sns-topic-amplifiedshopping-dev

To confirm this subscription, click or visit the link below (If this was in error no action is necessary):
Confirm subscription

Please do not reply directly to this email. If you wish to remove yourself from receiving all future SNS subscription confirmation requests please send an email to sns-opt-out
```

### Lambda関数の追加
※function nameは`sendSummary`を入力


```sh
amplify add function                                                                   +[main]
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: sendSummary
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes
? Select the categories you want this function to have access to. api
? Select the operations you want to permit on amplifiedshopping Mutation

You can access the following resource attributes as environment variables from your Lambda function
	API_AMPLIFIEDSHOPPING_GRAPHQLAPIENDPOINTOUTPUT
	API_AMPLIFIEDSHOPPING_GRAPHQLAPIIDOUTPUT
	API_AMPLIFIEDSHOPPING_GRAPHQLAPIKEYOUTPUT
	ENV
	REGION
? Do you want to invoke this function on a recurring schedule? No
? Do you want to enable Lambda layers for this function? No
? Do you want to configure environment variables for this function? No
? Do you want to configure secret values this function can access? No
? Do you want to edit the local lambda function now? Yes
? Choose your default editor: None
Successfully added resource sendSummary locally.

Next steps:
Check out sample function code generated in <project-dir>/amplify/backend/function/sendSummary/src
"amplify function build" builds all of your functions currently in the project
"amplify mock function <functionName>" runs your function locally
To access AWS resources outside of this Amplify app, edit the /Users/kumada/amplified-shopping/amplify/backend/function/sendSummary/custom-policies.json
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud
```

node-fetchのパッケージ追加

```sh
cd amplify/backend/function/sendSummary/src
npm install node-fetch@2
```

※`<YOUR-SNS-TOPIC-ARN-HERE>`にはCloudFormationで作成されたSNSのARNを入力

```amplify/backend/function/sendSummary/src/index.js
/* Amplify Params - DO NOT EDIT
    API_AMPLIFIEDSHOPPING_GRAPHQLAPIENDPOINTOUTPUT
    API_AMPLIFIEDSHOPPING_GRAPHQLAPIIDOUTPUT
    API_AMPLIFIEDSHOPPING_GRAPHQLAPIKEYOUTPUT
    ENV
    REGION
Amplify Params - DO NOT EDIT */
const fetch = require("node-fetch")
const AWS = require('aws-sdk')
const sns = new AWS.SNS()
const graphqlQuery = `query listShoppingItems {
    listShoppingItems {
        items {
            id
            ingredient
            quantity
            unit
        }
    }
}
`

exports.handler = async (event) => {
    const response = await fetch(process.env.API_AMPLIFIEDSHOPPING_GRAPHQLAPIENDPOINTOUTPUT, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "x-api-key": process.env.API_AMPLIFIEDSHOPPING_GRAPHQLAPIKEYOUTPUT
        },
        body: JSON.stringify({
            query: graphqlQuery,
            operationName: "listShoppingItems",
        })
    })

    const result = await response.json()
    const shoppingItems = result.data.listShoppingItems.items

    await sns.publish({
        // For demo purposes hard-coded, normally recommended to use environment variable
        TopicArn: "<YOUR-SNS-TOPIC-ARN-HERE>",
        Message: `Here's shopping cart summary - ${new Date().toDateString()}:\n` +
            `${shoppingItems.map(item => `${item.quantity} ${item.unit} - ${item.ingredient}`)
                .join('\n')}`
    }).promise().catch(e => console.log(e))

    return true
};
```

```amplify/backend/function/amplifiedshopping96ab7bee/custom-policies.json
[
  {
    "Action": ["sns:Publish"],
    "Resource": ["arn:aws:sns:*:*:sns-topic-amplifiedshopping-${env}"]
  }
]
```

この時点でのAmplifyで管理しているリソースの確認

```sh
amplify status                                                                          
Current Environment: dev
    
┌──────────┬────────────────────────┬───────────┬───────────────────┐
│ Category │ Resource name          │ Operation │ Provider plugin   │
├──────────┼────────────────────────┼───────────┼───────────────────┤
│ Function │ sendSummary            │ Update    │ awscloudformation │
├──────────┼────────────────────────┼───────────┼───────────────────┤
│ Api      │ amplifiedshopping      │ No Change │ awscloudformation │
├──────────┼────────────────────────┼───────────┼───────────────────┤
│ Custom   │ customResource4da9f265 │ No Change │ awscloudformation │
└──────────┴────────────────────────┴───────────┴───────────────────┘

GraphQL endpoint: https://xxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
GraphQL API KEY: xxxxxxxxxxxx
```

lambda関数デプロイ

```sh
amplify push -y
```

<details><summary>【展開】amplify-amplifiedshopping-dev-234028-functionsendSummary-1UAI16JAJGVBK.yaml</summary><div>

```amplify-amplifiedshopping-dev-234028-functionsendSummary-1UAI16JAJGVBK.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: '{"createdOn":"Mac","createdBy":"Amplify","createdWith":"7.6.3","stackType":"function-Lambda","metadata":{}}'
Parameters:
  CloudWatchRule:
    Type: String
    Default: NONE
    Description: " Schedule Expression"
  deploymentBucketName:
    Type: String
  env:
    Type: String
  s3Key:
    Type: String
Conditions:
  ShouldNotCreateEnvResources:
    Fn::Equals:
    - Ref: env
    - NONE
Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Metadata:
      aws:asset:path: "./src"
      aws:asset:property: Code
    Properties:
      Code:
        S3Bucket:
          Ref: deploymentBucketName
        S3Key:
          Ref: s3Key
      Handler: index.handler
      FunctionName:
        Fn::If:
        - ShouldNotCreateEnvResources
        - sendSummary
        - Fn::Join:
          - ''
          - - sendSummary
            - "-"
            - Ref: env
      Environment:
        Variables:
          ENV:
            Ref: env
          REGION:
            Ref: AWS::Region
      Role:
        Fn::GetAtt:
        - LambdaExecutionRole
        - Arn
      Runtime: nodejs14.x
      Layers: []
      Timeout: 25
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName:
        Fn::If:
        - ShouldNotCreateEnvResources
        - amplifiedshoppingLambdaRole84cee484
        - Fn::Join:
          - ''
          - - amplifiedshoppingLambdaRole84cee484
            - "-"
            - Ref: env
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
  lambdaexecutionpolicy:
    DependsOn:
    - LambdaExecutionRole
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: lambda-execution-policy
      Roles:
      - Ref: LambdaExecutionRole
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Action:
          - logs:CreateLogGroup
          - logs:CreateLogStream
          - logs:PutLogEvents
          Resource:
            Fn::Sub:
            - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*
            - region:
                Ref: AWS::Region
              account:
                Ref: AWS::AccountId
              lambda:
                Ref: LambdaFunction
  CustomLambdaExecutionPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: custom-lambda-execution-policy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Action:
          - sns:Publish
          Resource:
          - Fn::Sub:
            - arn:aws:sns:*:*:sns-topic-amplifiedshopping-${env}
            - env:
                Ref: env
          Effect: Allow
      Roles:
      - Ref: LambdaExecutionRole
    DependsOn: LambdaExecutionRole
Outputs:
  Name:
    Value:
      Ref: LambdaFunction
  Arn:
    Value:
      Fn::GetAtt:
      - LambdaFunction
      - Arn
  Region:
    Value:
      Ref: AWS::Region
  LambdaExecutionRole:
    Value:
      Ref: LambdaExecutionRole
```

</div><details>

<details><summary>【展開】amplify-amplifiedshopping-dev-234028-customcustomResource4da9f265-124STK37CCF2A.yaml</summary><div>

```amplify-amplifiedshopping-dev-234028-customcustomResource4da9f265-124STK37CCF2A.yaml
Description: '{"createdOn":"Mac","createdBy":"Amplify","createdWith":"7.6.3","stackType":"custom-customCloudformation","metadata":{}}'
Parameters:
  env:
    Type: String
    Description: Current Amplify CLI env name
Resources:
  snstopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName:
        Fn::Join:
        - ''
        - - sns-topic-amplifiedshopping-
          - Ref: env
  snstopicsubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      TopicArn:
        Ref: snstopic
      Endpoint: xxxx@xxxx.co.jp

```

</div><details>

### フロントエンド構築

Reactプロジェクトルートディレクトリに移動してライブラリをインストール

```sh
npm install aws-amplify recipe-ingredient-parser-v3
```

index.jsファイルに追記

```src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Amplify from 'aws-amplify' #追記
import awsconfig from './aws-exports' #追記

Amplify.configure(awsconfig) #追記
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

App.jsを置き換え

```src/App.js
import { useEffect, useState } from 'react';
import { API } from 'aws-amplify';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
import { parse } from 'recipe-ingredient-parser-v3';


function App() {
  const [ingredient, setIngredient] = useState()
  const [shoppingItems, setShoppingItems] = useState([])
  const fetchShoppingList = async () => {
    const response = await API.graphql({ query: queries.listShoppingItems })
    setShoppingItems(response.data.listShoppingItems.items)
  }

  useEffect(() => {
    fetchShoppingList()
  }, [])

  return (
    <div className="App">
      <input value={ingredient} onChange={e => setIngredient(e.target.value)}/>
      <button onClick={async () => {
        const parsed = parse(ingredient, 'eng')
        console.log(parsed)
        if (parsed.unit && parsed.ingredient && parsed.quantity) {
          setIngredient("")
          await API.graphql({
            query: mutations.createShoppingItem,
            variables: {
              input: {
                ingredient: parsed.ingredient,
                quantity: parsed.quantity,
                unit: parsed.unit
              }
            }
          })
          fetchShoppingList()
        }
      }}>Add ingredient</button>
      <h1>Ingredients</h1>
      <button onClick={fetchShoppingList}>Refresh</button>
      <button onClick={async () => {
        await API.graphql({
          query: mutations.sendSummaryEmail
        })
      }}>Send summary email</button>
      <ul>
        {shoppingItems.map(({ ingredient, quantity, unit }) => <li>
          <div>{quantity} {unit} - {ingredient}</div>
        </li>)}
      </ul>
    </div>
  );
}

export default App;
```

これでFrontとBackendは完成。
ソースコードを任意のGithubリポジトリにpushしてAmplifyと連携させます。

### Amplifyホスティング

```sh
amplify add hosting                                                                    
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous 
deployment)
? Choose a type Continuous deployment (Git-based deployments)
```

Amplifyコンソールの`Hosting environments`タブよりGithubを選択して`ブランチを接続`を押下

<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/d7f6516e-11ca-2bf5-cf8e-c5378b5cab76.png">

リモートブランチの追加画面でリポジトリとブランチをそれぞれ選択

<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/1d33c502-a714-427c-0d07-f6011ddfae94.png">

構築設定の構成画面で作成したバックエンド環境を選択、他は項目はデフォルトを指定

<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/bd499aa3-227a-afa0-d645-c6ba54060a67.png">

プロビジョニング→ビルド→デプロイ→検証の各ステップが自動的に進み、全てが正常に完了したらURLをクリック

<img width="500"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/d4345885-efec-b12a-320e-beffab277308.png">

アプリケーショの画面が表示されたら成功

<img width="300"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/3d5844f8-a5c7-a9a5-3ca7-c5ffb6600280.png">

`数字 単位 品物`の順に入力して`Add ingredient`を押下

```
6 cup of milk
```
<img width="200"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/287790d0-4c36-0642-a0f0-5371cb5b0379.png">

`Send summary email`を押下してSNS経由で指定したアドレスにメールを送信

<img width="200"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/f585b852-1825-8bb8-d599-d46dcf7ca735.png">

メールが届いていれば成功。

<img width="400"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/748aa7e7-d4a7-0bb1-b889-789b1da73b20.png">

lambdaの画面で関数が実行された事を確認

見辛いですが、`Invocations`MetricsにCountが計上されているのが分かります。
<img width="400"　alt="1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/e77bea5a-65e0-5263-8721-f392d7e4d2a2.png">

DynamoDBのテーブルにも反映されている事を確認

![スクリーンショット 2021-12-12 14.21.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/48065ed7-1f16-3ffb-05d7-6eb357fbd4ad.png)


Appsyncでクエリを実行して値を取得できるか確認

```graphql
query MyQuery {
  listShoppingItems {
    items {
      quantity
      ingredient
      unit
    }
  }
}
```

![スクリーンショット 2021-12-12 14.25.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/414848c4-073b-0996-0c6d-e920a3baf8eb.png)


## まとめ
今回Amplifyでサーバレスアプリケーションを作成して、その後Amplifyの組み込みでは対応していないカスタムリソースの追加を実施しました。その裏側ではCloudFormationによってDynamoDBやAppsync、lambda、IAM、S3などのリソースが作成されており、それぞれが実際に連携しているところまで確認出来ました。裏側の動きを並行して見ていくことでサービスへの理解が深めることができたので、これでまた一歩Amplifyと仲良くなれたと気がします。また、裏側を知ることでAmplifyの強力さも身に沁みました。Amplifyの学習にある程度の慣れが必要ですがそれは他でも同じで、それぞれを個別で環境を用意するとなるとそれもまた大変なので使い所見極めて上手く使っていきたいですね。

## 掃除

```sh
amplify remove custom
amplify remove api
amplify remove function
amplify push -y
amplify delete
```