LambdaからAppSync Mutation へIAM認証リクエストする方法を解説します!API_KEYレスなリクエストを行いましょう😎

LambdaからAppSync Mutation へIAM認証リクエストする方法を解説します!API_KEYレスなリクエストを行いましょう😎

こんにちは!

最近は日本で Graphql 開発がますます加熱しているように感じます。従来の RestAPI と比較すると、一度のリクエストで様々なデータを一括で取得できる Graphql は、RestAPI と比較してフロントエンドの実装を簡略化することができます。Graphql はデータ取得のオーバーヘッドをフロントエンドでコントロールできることから、パフォーマンスチューニングにおいて利便性高いのも特徴の一つです。

本記事では、AWS Lambda(NodeJS)から AppSync の Graphql API へ IAM 認証でリクエストを出す方法を解説します。

想定する読者

  • Graphql 開発を行うヒト
  • AppSync で開発を行うヒト
  • Lambda から AppSync へ Graphql リクエストを行う方法を模索しているヒト

はじめに

Lambda から App Sync へリクエストするユースケース

Lambda 関数から AppSync へリクエストを行うユースケースは以下です。本記事も以下のようなユースケースを想定しています。

  • フロントエンドがサブスクリプション接続を開始
  • Lambda から AppSync の Mutation へリクエスト
  • フロントエンドがサブスクリプション通知を受信

尚、Lambda からリクエストする AppSync の Mutation は LocalResolver に設定し、ビジネスロジックを処理をさせないことが重要です。Lambda から Mutation をリクエストしビジネスロジックを発火させるワークロードはデバッグがしにくく AppSync のスロットリングを招く恐れもあるため、非推奨です。

AppSync の API_KEY ではなく何故 IAM 認証を使用するのか?

AppSync の API_KEY には有効期限が必ず設定されます。(最長365日)つまり、Lambda から AppSync へのリクエストをAPI_KEYで行った場合、API_KEY のローテーションが必要となってしまいます。

また、IAM 認証の場合は IAM ポリシードキュメントを使用して Lambda がリクエスト可能な Mutation を制限することが可能なため、運用面、セキュリティ面ともに IAM 認証の方が優れていることがわかります。

Lambda から AppSync の Mutation への IAM 認証リクエスト

Lambda の IAM ロールへ IAM ポリシードキュメントを適用

Lambda へ設定する IAM ロールの IAM ポリシードキュメントへ、AppSync へのリクエストを許可します。以下にIAMロールの Cloudformation コードのサンプルを紹介します。

LambdaRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: "LambdaRole"
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: "Allow"
          Principal:
            Service:
              - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
    Policies:
      - PolicyName: "InLinePolicy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: "Allow"
              Action:
                - "appsync:GraphQL"
              Resource:
                - !GetAtt GraphQlApi.Arn
                - !Join [
                    "/",
                    [
                      !GetAtt GraphQlApi.Arn,
                      "types",
                      "Mutation",
                      "fields",
                      "hoo*",
                    ],
                  ]

※ 本題と関係のない IAM ポリシー(例:CloudWatchLogGroup のポリシー)は省略しています

上記の IAM ロールでは、AppSync の以下のような Mutation API に対してリクエストするポリシーを提供しています。

type Mutation {
  hooA(Example:String): String! @aws_iam // リクエスト可
  hooB(Example:String): String! @aws_iam // リクエスト可
  hooC(Example:String): String! // AppSyncのデフォルト認証設定がIAM以外の場合リクエスト不可
  hugaC(Example:String): String! @aws_iam // リクエスト不可
  hugaD(Example:String): String! @aws_iam // リクエスト不可
}

この IAM ロールによる Graphql リクエストの実装方法を応用すれば、Lambda に対して最小権限を付与できるため、セキュアな API リクエストが実現可能となります!

Graphql リクエストを TypeScript で実現する

いくつか実装方法がありますが、私たちが好みなのは RestAPI クライアントモジュール(Axios)で Graphql のエンドポイントへ POST リクエストを出す方式です。aws-amplifyaws-appsyncApploモジュールを使用すれば実装をAxiosよりも簡略化できそうですが、過去の経験上デプロイ後に Lambda 上で例外が発生したり、ビルドでハマったりとあまり良い記憶がありません。(最近は改善された様子ですが…)シンプルに、Axiosで AppSync のエンドポイントへ POST リクエストした方が確実です。

以下は、AWS SDK v3を使用した TypeScript の実装例です。(過去のプロジェクトのものを抜粋)環境変数はお好みで指定してください。

// services/appsyncServive.ts

import { URL } from 'url';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import axios from 'axios';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import { Sha256 } from '@aws-crypto/sha256-universal';
import _ from 'lodash';
import * as API from 'types/API';
import * as mutations from 'utils/mutations';

export default class {
  constructor(graphQlUrl: string = process.env.GRAPHQL_ENDPOINT as string, region: string = process.env.REGION as string) {
    if (_.isEmpty(graphQlUrl)) {
      throw new Error('env GRAPHQL_ENDPOINT is not defined');
    }
    if (_.isEmpty(region)) {
      throw new Error('env REGION is not defined');
    }
    this.graphQlUrl = graphQlUrl;
    this.url = new URL(this.graphQlUrl);
    this.region = region;
    this.credentialsProvider = defaultProvider();
  }
  private readonly graphQlUrl: string;
  private readonly region: string;
  private readonly url: URL;
  private readonly credentialsProvider: ReturnType<typeof defaultProvider>;
  private async request<V, R>(query: string, variables: V) {
    const hostname = this.url.hostname;
    const body = {
      query,
      variables,
    };
    const request = {
      headers: {
        'Content-Type': 'application/json',
        host: hostname,
      },
      hostname,
      method: 'POST',
      path: '/graphql',
      protocol: 'https',
      body: JSON.stringify(body),
    };
    const signer = new SignatureV4({
      credentials: this.credentialsProvider,
      region: this.region,
      service: 'appsync',
      sha256: Sha256,
    });
    const signature = await signer.sign(request);
    return axios
      .post(`${request.protocol}://${request.hostname}${request.path}`, body, {
        headers: signature.headers,
      })
      .then((res) => res.data as R);
  }
  public changedUserProfile(args: Omit<API.ChangedUserProfileMutationVariables, 'Payload'> & { Payload?: API.UserProfile | Record<string, unknown> }) {
    return this.request<API.ChangedUserProfileMutationVariables, API.ChangedUserProfileMutation>(mutations.changedUserProfile, {
      ...args,
      Payload: JSON.stringify({
        User: {
          UserProfile: args.Payload || {},
        },
      }),
    });
  }
}
// utils/mutations.ts

export const changedUserProfile = /* GraphQL */ `mutation ChangedUserProfile(
  $UserId: ID!
  $SubscriptionType: SubscriptionType!
  $Payload: AWSJSON
) {
  changedUserProfile(
    UserId: $UserId
    SubscriptionType: $SubscriptionType
    Payload: $Payload
  ) {
    UserId
    SubscriptionType
    Payload
    __typename
  }
}
// 呼び出す方法

import AppsyncService from 'services/appsyncService';

appsyncServive.changedUserProfile({
  UserId: "user id here",
  SubscriptionType: "Example",
  Payload: {hoge: "foo"},
});

まとめ

Lambda から AppSync へリクエストする際は、IAM 認証を使いましょう。本記事を参考に実装いただければ幸いです。やむを得なく API_KEY にて実装する場合は、ローテーションすることをお忘れなく。

AWSサーバーレスフロントエンドの開発はお気軽にお問い合わせください。