⚡️

Amplify(Gen1)+AppSyncでCloudFormationのAPIテンプレートサイズの削減を試みた話

2025/02/09に公開

解決したかったこと

Amplifyで作成していたAppSync APIのCloudFormationテンプレートのサイズが1000KB(上限)を超えてしまった。
テンプレートの分割や記述の変更を行い、上限を超えないようになんとかしたい。

結論

Amplify独自の@authディレクティブではなく、AppSyncネイティブのディレクティブ(@aws_iamや@aws_cognito_user_poolsなど)を使用することで、
認証ルールの挙動を維持しながらCloudFormationテンプレートのサイズを大幅に削減できました。

type CustomOutput @aws_iam @aws_cognito_user_pools {
  id: ID!
  name: String!
  postsId: ID!
  postTitle: String!
}

現状

改善前はこのような条件でスキーマを検討していました。

  • Amplify(Gen1)でAppSyncを作成しつつ、DynamoDBも作成する
  • schema.graphqlでは各スキーマに対して認証を設定する
  • Lambda関数を使ったカスタムリゾルバーを定義する

この設定を反映すると、以下のようなスキーマ定義が考えられます(サンプルコードです)。

schema.graphql
type Blog @model @auth(rules: [{ allow: private }]) {
  id: ID!
  name: String!
  posts: [Post] @hasMany
}

type Post @model @auth(rules: [{ allow: private }]) {
  id: ID!
  title: String!
  blog: Blog @belongsTo
  createdBy: User @hasOne
}

type Mutation {
  myCustomMutation(args: String): [CustomOutput]
  @function(name: "customFunction") 
  @auth(rules: [{ allow: private }])
}

type CustomOutput {
  id: ID! @auth(rules: [{ allow: private }])
  name: String! @auth(rules: [{ allow: private }])
  postsId: ID! @auth(rules: [{ allow: private }])
  postTitle: String! @auth(rules: [{ allow: private }])
}

この書き方は公式ドキュメントで紹介されている機能を使用して定義されているため、意図通りに動作します。
ただ、このCustomOutput@authをフィールド全てにつけているため、schema.graphqlファイルから作成されるCloudFormationテンプレートが肥大化し、上限(1MB)に達してしまう問題が発生していました。

Amplifyがやってくれること

AmplifyでAppSyncを作成する際、ディレクティブと呼ばれる@から始まる記述を使用することでDynamoDBやカスタムリゾルバー、Amazon OpenSearch Serviceの作成やCognitoによる認証などが簡単に設定できます。
また、これらはschema.graphqlという1ファイルで全て定義可能です。

このディレクティブの中でも@authは認証に関する設定を行うものです。
APIキー、Amazon Cognito identity pools, Amazon Cognito user poolsなど、@authのrulesの記述を変えることで様々な認証方法を選ぶことができます。
ここではそれぞれの認証方法の記述方法については触れませんが、詳しく知りたい方は公式ドキュメントを参照してください。
https://docs.amplify.aws/gen1/android/build-a-backend/graphqlapi/customize-authorization-rules/

この@authというディレクティブは、Amplifyをデプロイする際に内部でAppSyncネイティブのディレクティブに変換されます。
それが@aws_xxxというディレクティブになります。

AppSyncで用意されているディレクティブは以下です。

・@aws_api_key - フィールドが API_KEY で承認されることを指定します。
・@aws_iam - フィールドが AWS_IAM で承認されることを指定します。
・@aws_oidc - フィールドが OPENID_CONNECT で承認されることを指定します。
・@aws_cognito_user_pools - フィールドが AMAZON_COGNITO_USER_POOLS で承認されることを指定します。
・@aws_lambda - フィールドが AWS_LAMBDA で承認されることを指定します。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/security-authz.html#using-additional-authorization-modes

イメージとしては、以下のような対応になっています。(正確には細かいオプションなどを加えると異なります)
※provider次第で変化するiamやoidcなどは、他のallow値でも動作します。

Amplify AppSync
@auth(rules: [{ allow: public }]) @aws_api_key
@auth(rules: [{ allow: private, provider: iam }]) @aws_iam
@auth(rules: [{ allow: private }]) @aws_cognito_user_pools
@auth(rules: [{ allow: private, provider: oidc }]) @aws_oidc
@auth(rules: [{ allow: custom }]) @aws_lambda

図にするとこのような変換をAmplifyが行ってくれます。
これはschema.graphqlq(Amplify) → schema.graphql(AppSync) の変換が行われてるイメージ図です。

今回のスキーマファイルの問題点

今回問題だったのが、この@authの変換後にCloudFormationのテンプレートサイズが大きくなり制限を超えてしまったことです。
上の図ではschema.graphqlが、Amplify のものから AppSync のものに変わる図でした。
実際にはこれに加えて、CloudFormationのテンプレートに変換する処理も含まれます。

CloudFormationのテンプレートには、カスタムリゾルバーの作成やそれに伴うフィールドレベルの認証ルールなどが定義されていきます。
その中でフィールドが多く、かつ定義するtypeも多いとなると、テンプレートサイズが大きくなることは避けられません。

しかし、残念ながらテンプレートサイズ超過に対して、Amplifyのドキュメントには情報が見つかりませんでした。詰みかけました。(どうしようもなくAmplify脱却も考えた)

# ここで返り値の型としてCustomOutputを定義する
type Mutation {
  myCustomMutation(args: String): [CustomOutput]
  @function(name: "customFunction") 
  @auth(rules: [{ allow: private }])
}

# カスタムリゾルバの定義と共にこのようなOutputが増えていく
type CustomOutput {
  id: ID! @auth(rules: [{ allow: private }])
  name: String! @auth(rules: [{ allow: private }])
  postsId: ID! @auth(rules: [{ allow: private }])
  postTitle: String! @auth(rules: [{ allow: private }])
}

時間がかかることを悟ったので、仕様理解を深めることにしました。
そこで解決に向けて、いくつかの仮説が立てられます。

  1. @authを全てのスキーマから削除すれば良いのではないか?
  2. type TaskOutputのすぐ後ろに@authをつけれないのか?
  3. Outputからのみ@auth削除すれば良いのではないか?

1. @authを全てのスキーマから削除すれば良いのではないか?

A. NG
これは選択肢としてナシです。
理由は@authをつけないと全てのリクエストが拒否されるためです。
今回私が使用していたのは GraphQL Transformer v2 で、このv2では認証をつけない場合はデフォルトで拒否のルールになってしまいます。

https://docs.amplify.aws/gen1/react/tools/cli/migration/transformer-migration/

2. type TaskOutputのすぐ後ろに@authをつけれないのか?

A. NG
つけられません。
@auth@modelと一緒に使うことが必須であり、Outputの後ろに@authをつけるとエラーになってしまいます。
そのためOutputに@authをつけたいのであれば、フィールドレベルにつける必要があります。

Types annotated with @auth must also be annotated with @model.

https://github.com/aws-amplify/amplify-category-api/blob/e0f0b6034dc4132f5f2ef41445e88a631bbd3b0f/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts#L214

3. Outputからのみ@auth削除すれば良いのではないか?

A. NG
myCustomMutationのAPIを叩いた場合に認証エラーが返却されます。
これで今回のMutaionには、認証ルールが必須であることがわかりました。

返却エラー例
{
    "data": {
        "myCustomMutation": null
    },
    "errors": [
        {
            "path": [
                "myCustomMutation"
            ],
            "data": null,
            "errorType": "Unauthorized",
            "errorInfo": null,
            "locations": [
                {
                    "line": 2,
                    "column": 3,
                    "sourceName": null
                }
            ],
            "message": "Not Authorized to access CustomOutput on type Mutation"
        }
    ]
}

そして結論へ

上記の検証でOutputに対する認証ルールは必須、かつ@authはフィールドレベルにしか付けられないことが分かりました。
つまり元の記述は仕様通りだったわけです。テンプレートサイズの削減は不可能に思えました。(この時はAppSyncのネイティブディレクティブの存在など知らず)

そんな中、とあるre:Postを発見。
https://repost.aws/ja/questions/QUhvLRtvThS2am6-JGdn3zlw/not-authorized-to-access-on-type-error-when-accessing-output-from-custom-query-mutation?sc_ichannel=ha&sc_ilang=en&sc_isite=repost&sc_iplace=hp&sc_icontent=QUhvLRtvThS2am6-JGdn3zlw&sc_ipos=7

AppSyncのディレクティブなら動作すると記述が。

他にもIssueで同じ解決策を取っている方を見つけました。
https://github.com/aws-amplify/amplify-category-api/issues/652#issuecomment-1375073380

Amplifyのディレクティブが使えないならAppSyncのネイティブのディレクティブを使えば良いじゃない。そう思い試してみることに。

type Blog @model @auth(rules: [{ allow: private }]) {
  id: ID!
  name: String!
  posts: [Post] @hasMany
}

type Post @model @auth(rules: [{ allow: private }]) {
  id: ID!
  title: String!
  blog: Blog @belongsTo
  createdBy: User @hasOne
}

type Mutation {
  myCustomMutation(args: String): [CustomOutput]
  @function(name: "customFunction") 
  @auth(rules: [{ allow: private }])
}

type CustomOutput @aws_iam @aws_cognito_user_pools {
  id: ID!
  name: String!
  postsId: ID!
  postTitle: String!
}

APIは同じ挙動になりました。認証ルールもAmplifyディレクティブの時と同様に動作しています。
更にテンプレートサイズは大幅に減少しました。

本当にこれで良いのか?

CloudFormationのテンプレートサイズは驚くほど減少し、動作影響も無さそうであることを確認しました。しかし本当にこれでいいのでしょうか?大袈裟ですが、これで済んでしまうなら@authがある理由が無くなってしまいます。

Amplifyのコードを追ってみましたが、力不足であまり意図が読み取れませんでした。。
そこでダメ元でquestionのissueを立ててみることにしました。
https://github.com/aws-amplify/amplify-category-api/issues/3143

すると、Amplifyのメンテナーの方から親切に回答していただけました。
意訳すると
「その方法しか無さそう。ただ、認証ルールがどう付いているかは確認してね!」
と回答が返ってきました。詳しく知りたい方は、issueで申し訳ないと思いつつもたくさん質問させていただいているので覗いてみてください。

解決

完成後のコード

before
type Blog @model @auth(rules: [{ allow: private }]) {
  id: ID!
  name: String!
  posts: [Post] @hasMany
}

type Post @model @auth(rules: [{ allow: private }]) {
  id: ID!
  title: String!
  blog: Blog @belongsTo
  createdBy: User @hasOne
}

type Mutation {
  myCustomMutation(args: String): [CustomOutput]
  @function(name: "customFunction") 
  @auth(rules: [{ allow: private }])
}

type CustomOutput {
  id: ID! @auth(rules: [{ allow: private }])
  name: String! @auth(rules: [{ allow: private }])
  postsId: ID! @auth(rules: [{ allow: private }])
  postTitle: String! @auth(rules: [{ allow: private }])
}
after
type Blog @model @auth(rules: [{ allow: private }]) {
  id: ID!
  name: String!
  posts: [Post] @hasMany
}

type Post @model @auth(rules: [{ allow: private }]) {
  id: ID!
  title: String!
  blog: Blog @belongsTo
  createdBy: User @hasOne
}

type Mutation {
  myCustomMutation(args: String): [CustomOutput]
  @function(name: "customFunction") 
  @auth(rules: [{ allow: private }])
}

type CustomOutput @aws_iam @aws_cognito_user_pools {
  id: ID!
  name: String!
  postsId: ID!
  postTitle: String!
}

Special Thanks!

本記事の情報の一部は、Amplifyのissueの回答内容も含まれています。
親切に質問に回答してくださったAmplifyメンテナーの方々、そしてAmplifyを開発してくださっているすべての方に感謝いたします。
I appreciate their help!

アイレット株式会社

Discussion