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

## はじめに

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

### 対象とするユーザー

-   AWSを使ったことがある
-   Amplifyの[Getting started](https://docs.amplify.aws/start/q/integration/js)を完了した
-   Amplifyに含まれている個別のサービスを使ったことがない

### 対象とする環境

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

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

_▼package.json_

```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](https://aws.amazon.com/jp/amplify/)とは、モバイル/Webアプリケーションの開発に必要なAWSサービスのパッケージです。アプリケーションに必要となるAPI/ユーザー認証/ホスティング/ユーザー分析といった機能がひとまとめにされています。

Amplifyの各サービスは[AWS AppSync](https://aws.amazon.com/jp/appsync/)によってアクセスが制御されます。開発者は各サービスのユーザー登録/認証を意識する必要はありません。

### Amplify Auth

[Amplify Auth](https://docs.amplify.aws/lib/auth/getting-started/q/platform/js)はユーザー認証と、その認証に基づいたアクセス制御のためのサービスです。

Authサービスは[AWS AppSync](https://aws.amazon.com/jp/appsync/)と[AWS Cognito](https://aws.amazon.com/jp/cognito/)の連携で実現されています。Cognitoがユーザー登録DBの役割を担い、AppSyncがCognitoの登録情報に基づきアクセス制御を担当します。

### Amplify Function

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

Functionサービスは[AWS Lambda](https://aws.amazon.com/jp/lambda/)で構成されています。

Amplifyは複数のサービスから構成されており、APIのアクセス制御機能はAppSyncの[セキュリティ](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/security.html)機能で実現されています。

## Amplify FunctionへのIAM認証の追加

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

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

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

```shell
% 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と選択します。

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

```shell
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`します。

![](https://www.evernote.com/l/AAlWXW9FZIhJHaBm0AnA0uc3kHS9oLShgPoB/image.png?01)

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

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

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

_▼amplify/backend/api/API名/schema.graphql_

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

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

_▼amplify/backend/api/API名/build/schema.graphql_

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

この状態では認証用ディレクティブがないため、AppSyncはデフォルト認証形式でのアクセスを許可します。Amplifyの[Getting startedの手順](https://docs.amplify.aws/start/getting-started/data-model/q/integration/js)では、デフォルトの認証形式に`API Key`を指定しています。したがってIAM認証ではアクセスできません。

次にAPIのスキーマに[@auth](https://docs.amplify.aws/cli/graphql-transformer/auth#api-configuration-in-the-amplify-folder)ディレクティブを追加します。

_▼amplify/backend/api/API名/schema.graphql_

```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_

```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`コマンドを実行します。

```shell
% 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_

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

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

```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](https://docs.amplify.aws/cli/function#signing-a-request-from-lambda)を参考に、リクエストヘッダーにIAMで署名します。

```js
/* 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 を ぶん殴る](https://www.r3it.com/blog/aws-lambda-to-appsync)

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