Amplify Functionから、IAM認証を使ってAPIを呼ぶ

はじめに

この記事は、AmplifyのFunctionから、IAM認証を使ってAPIを操作する方法を共有するためのものです。

対象とするユーザー

  • AWSを使ったことがある
  • AmplifyのGetting startedを完了した
  • Amplifyに含まれている個別のサービスを使ったことがない

対象とする環境

この記事では、JavaScriptベースのWebアプリケーションを対象とします。モバイルアプリケーションは対象としません。

必須となるnpmモジュールは以下の通りです。

▼package.json

"dependencies": {
  "aws-amplify": "^3.3.6"
}

パッケージのバージョンを確認してください

この記事では2020/12/04時点でのAmplifyを前提にしています。Amplifyは活発に開発が行われているため、導入手順が大きく変更されている場合があります。記事を読む前に、お手元の環境を確認してください。

先に結論だけ

  • APIの追加の認証プロバイダーにIAMを追加する
  • スキーマの@authディレクティブで、IAM認証を追加する
  • Functionに、APIへのアクセス許可を追加する
  • Lambda関数内からAPIへリクエストするときに、リクエストヘッダーにIAMロールで署名をつける

という手順でAmplifyのFunctionからAPIをIAM認証で呼び出します。

Amplifyの構成

まず、今回の記事で対象となる各サービスを解説します。

Amplify

Amplifyとは、モバイル/Webアプリケーションの開発に必要なAWSサービスのパッケージです。アプリケーションに必要となるAPI/ユーザー認証/ホスティング/ユーザー分析といった機能がひとまとめにされています。

Amplifyの各サービスはAWS AppSyncによってアクセスが制御されます。開発者は各サービスのユーザー登録/認証を意識する必要はありません。

Amplify Auth

Amplify Authはユーザー認証と、その認証に基づいたアクセス制御のためのサービスです。

AuthサービスはAWS AppSyncAWS Cognitoの連携で実現されています。Cognitoがユーザー登録DBの役割を担い、AppSyncがCognitoの登録情報に基づきアクセス制御を担当します。

Amplify Function

Amplify Functionはサーバーレスでプログラムを実行するサービスです。AWS EC2のように仮想マシンが立ち上がっているわけではありません。何らかのトリガーによって呼び出された時だけ、事前に設定したプログラムが動きます。

FunctionサービスはAWS Lambdaで構成されています。

Amplifyは複数のサービスから構成されており、APIのアクセス制御機能はAppSyncのセキュリティ機能で実現されています。

Amplify FunctionへのIAM認証の追加

Amplifyのサービス構成を踏まえて、FunctionへのIAM認証を追加します。

AppSyncに認証プロバイダーを追加する

まずはAppSyncに認証プロバイダーとしてIAMを追加します。amplify update apiコマンドでAPIに認証プロバイダーを追加します。

% amplify update api
? Please select from one of the below mentioned services: GraphQL
? Select from the options below Update auth settings
? Choose the default authorization type for the API API key
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API (Press <space> to select, <a> to toggle all, <i> to invert selection)
 ◯ Amazon Cognito User Pool
❯◉ IAM
^^^^^^^
 ◯ OpenID Connect

ここで認証タイプを追加→IAMと選択します。

❗下線で強調した部分は複数選択可能なチェックボックスです。スペースキーで選択、リターンで決定という操作方法です。なにもチェックを行わないままリターンを押すと操作がキャンセルされたと解釈して処理が進みます。ご注意ください。

GraphQL schema compiled successfully.

Edit your schema at <プロジェクト名>/amplify/backend/api/<API名>/schema.graphql or place .graphql files in a directory at <プロジェクト名>/amplify/backend/api/<API名>/schema
Successfully updated resource

成功メッセージのあとに、amplify pushします。

AWSコンソールからAppSyncにアクセスし、「追加の認証プロバイダー」にIAMが登録されていることを確認します。

@authディレクティブで、APIへIAM認証でのアクセスを許可する

AppSyncのセキュリティ機能とAmplify APIがどのように関係するかを確認しましょう。まずAPIのスキーマに@authディレクティブのないモデルを追加してみます。

▼amplify/backend/api/API名/schema.graphql

type Todo @model{
  id: ID!
  url: String
}

このスキーマをamplify pushコマンドでビルドすると、以下のようなAppSync用のスキーマがbuildディレクトリに生成されます。

▼amplify/backend/api/API名/build/schema.graphql

type Todo {
  id: ID!
  url: String
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime!
}

この状態では認証用ディレクティブがないため、AppSyncはデフォルト認証形式でのアクセスを許可します。AmplifyのGetting startedの手順では、デフォルトの認証形式にAPI Keyを指定しています。したがってIAM認証ではアクセスできません。

次にAPIのスキーマに@authディレクティブを追加します。

▼amplify/backend/api/API名/schema.graphql

type Todo
  @model
  @auth(rules: [
    { allow: private, provider: userPools},
    { allow: private, provider: iam }
  ]){
  id: ID!
  url: String
}

このスキーマをamplify pushコマンドでビルドすると、AppSyncのスキーマに@aws_aimディレクティブが追加されます。

▼amplify/backend/api/API名/build/schema.graphql

type Todo @aws_iam @aws_cognito_user_pools {
  id: ID!
  url: String
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime!
}

これでAPIの特定のテーブルにIAM認証でのアクセスが許可されました。

FunctionにAPIへのアクセスを許可する

FunctionにAPIへのアクセス許可を追加します。amplify update functionコマンドを実行します。

% amplify update function
? Select which capability you want to update: Lambda function (serverless function)
? Select the Lambda function you want to update <function名>
General information
| Name: <function名>
| Runtime: nodejs

Lambda layers
- Not configured

? Which setting do you want to update? Resource access permissions
? Select the category (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ api
 ◯ auth
 ◯ analytics
 ◯ function
 ◯ storage

Api category has a resource called <API名>
? Select the operations you want to permit for <API名> (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ create
 ◉ read
 ◉ update
 ◉ delete

Functionを選択すると、どの設定を修正するかを聞かれますのでResource access permissions(リソースのアクセス権限)を選択します。次にAPIを選択し、新規/読み/書き/削除の権限を追加します。

設定を変更するとfunction-parameters.jsonに権限が追記されます。

▼amplify/backend/function/Function名/function-parameters.json

{
  "permissions": {
    "api": {
      "<API名>": [
        "create",
        "read",
        "update",
        "delete"
      ]
    }
  "lambdaLayers": []
}

この権限はFunction名-cloudformation-template.jsonに展開されます。 ▼amplify/backend/function/Function名/Function名-cloudformation-template.json

"LambdaExecutionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": {
          "Fn::If": [
            "ShouldNotCreateEnvResources",
            "IAMロール名",
            ^^^^^^^^^^^^
            {
              "Fn::Join": [
                "",
                [
                  "IAMロール名",
                  "-",
                  {
                    "Ref": "env"
                  }
                ]
              ]
            }
          ]
        },

amplify pushコマンドを実行すると、この権限が上記IAMロールにインラインポリシーとして展開されます。

Lambda関数内からAPIへリクエストするときに、ヘッダーにIAMロールで署名をつける

最後にAmplify FunctionのLambda関数を実装します。公式チュートリアルのSigning a request from Lambdaを参考に、リクエストヘッダーにIAMで署名します。

/* Amplify Params - DO NOT EDIT
	API_<API名>_GRAPHQLAPIENDPOINTOUTPUT
	API_<API名>_GRAPHQLAPIIDOUTPUT
	ENV
	REGION
Amplify Params - DO NOT EDIT */

const https = require("https");
const AWS = require("aws-sdk");
const urlParse = require("url").URL;
const appsyncUrl = process.env.API_<API名>_GRAPHQLAPIENDPOINTOUTPUT;
const region = process.env.REGION;
const endpoint = new urlParse(appsyncUrl).hostname.toString();

const ListTodos = `
query ListTodos(
    $filter: ModelPublicInfoFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        url
        createdAt
        updatedAt
      }
      nextToken
    }
  }
`;

exports.handler = async (event) => {

  /**
   * AppSyncのエンドポイントを宛先としたリクエストを組み立てる
   */
  const req = new AWS.HttpRequest(appsyncUrl, region);
  req.method = "POST";
  req.path = "/graphql";
  req.headers.host = endpoint;
  req.headers["Content-Type"] = "application/json";
  req.body = JSON.stringify({
    query: ListTodos,
  });

  /**
   * ここでリクエストヘッダーに署名を追加する
   */
  const signer = new AWS.Signers.V4(req, "appsync", true);
  signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());

  const data = await new Promise((resolve, reject) => {
    const httpRequest = https.request({ ...req, host: endpoint }, (result) => {
      result.on("data", (data) => {
        resolve(JSON.parse(data.toString()));
      });
    });

    httpRequest.write(req.body);
    httpRequest.end();
  });

  return {
    statusCode: 200,
    body: data,
  };
};

これでLambda関数内からTodoテーブルのリストを取得できました。

個人的な感想

Amplifyの導入はとても簡単です。データベースと認証機能をもったWebアプリケーションを数十分程度でデプロイできます。

しかしGetting startedから一歩進んだ機能を実装しようとすると、Amplify内の個別サービスの知識が必要になります。Amplifyのどの機能が、AWSのどのサービスに対応しているかを把握すると調査が楽になります。

参考記事

AWS Lambda から AppSync の API を ぶん殴る

以上、ありがとうございました。