こんにちは!
最近は日本で Graphql 開発がますます加熱しているように感じます。従来の RestAPI と比較すると、一度のリクエストで様々なデータを一括で取得できる Graphql は、RestAPI と比較してフロントエンドの実装を簡略化することができます。Graphql はデータ取得のオーバーヘッドをフロントエンドでコントロールできることから、パフォーマンスチューニングにおいて利便性高いのも特徴の一つです。
本記事では、AWS Lambda(NodeJS)から AppSync の Graphql API へ IAM 認証でリクエストを出す方法を解説します。
Lambda 関数から AppSync へリクエストを行うユースケースは以下です。本記事も以下のようなユースケースを想定しています。
尚、Lambda からリクエストする AppSync の Mutation は LocalResolver に設定し、ビジネスロジックを処理をさせないことが重要です。Lambda から Mutation をリクエストしビジネスロジックを発火させるワークロードは、デバッグがしにくく AppSync のスロットリングを招く恐れもあるため、非推奨です。
AppSync の API_KEY には有効期限が必ず設定されます。(最長365日)つまり、Lambda から AppSync へのリクエストをAPI_KEYで行った場合、API_KEY のローテーションが必要となってしまいます。
また、IAM 認証の場合は IAM ポリシードキュメントを使用して Lambda がリクエスト可能な Mutation を制限することが可能なため、運用面、セキュリティ面ともに 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 リクエストが実現可能となります!
いくつか実装方法がありますが、私たちが好みなのは RestAPI クライアントモジュール(Axios)で Graphql のエンドポイントへ POST リクエストを出す方式です。aws-amplifyやaws-appsync、Apploモジュールを使用すれば実装を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サーバーレス、フロントエンドの開発はお気軽にお問い合わせください。
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎