Closed12

AppSync の使用感を確認

ピン留めされたアイテム
kurosamekurosame

AppSync は GraphQL を使った完全マネージド型サービス
データソースには DynamoDB や Lambda などを選択でき、アクセスも簡単な設定で自動化される
また、AWS コンソール上で GraphQL スキーマやリゾルバーを自動生成でき、クエリの作成・検証もできます


画像はAWS ドキュメントから引用

最低限、データソースの作成、GraphQL スキーマ・リゾルバーの作成を行えば使えるので、今回はこの辺を Terraform を使って実装します

以下は実用性を考えて、Cognito 認証にする手順

kurosamekurosame

DynamoDB を作成

簡単なテーブルを用意

resource "aws_dynamodb_table" "this" {
  name         = "Example"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"

  attribute {
    name = "id"
    type = "S"
  }
}

billing_mode には、PROVISIONEDPAY_PER_REQUESTが指定可能で省略するとPROVISIONEDになる

  • PROVISIONED
    • 固定料金
    • 読み取り容量ユニット(RCU)と書き込み容量ユニット(WCU)のスループットタイプを指定しておき、それに応じて課金される
  • PAY_PER_REQUEST
    • 従量課金

今回は個人開発で常時リクエストはないので、PAY_PER_REQUESTにします
もし、PROVISIONEDにする場合は、Terraform の read_capacity と write_capacity は必須です

kurosamekurosame

IAM ポリシー作成

AppSync から DynamoDB を操作するのに必要なポリシーをロールにアタッチ

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query",
        "dynamodb:Scan"
      ],
      "Resource": [
        "arn:aws:dynamodb:ap-northeast-1:[アカウントID]:table/Example",
        "arn:aws:dynamodb:ap-northeast-1:[アカウントID]:table/Example/*"
      ]
    }
  ]
}
kurosamekurosame

GraphQL のスキーマを定義

以下の単純な Query と Mutation を定義しています

  • findAll
    • すべてのキーワードを取得
  • add
    • キーワードを追加
  • delete
    • キーワードを削除
keyword.graphql
input Add {
  keyword: String!
}

input Delete {
  id: ID!
}

type Keyword {
  id: ID!
  keyword: String!
}

type Keywords {
  items: [Keyword]
}

type Query {
  findAll: Keywords
}

type Mutation {
  add(input: Add!): Keyword
  delete(input: Delete!): Keyword
}
kurosamekurosame

AppSync のスキーマを設定

data "local_file" "graphql_schema" {
  filename = "${path.cwd}/appsync/schemas/keyword.graphql"
}

resource "aws_appsync_graphql_api" "this" {
  name                = "example"
  authentication_type = "AWS_IAM"
  schema              = data.local_file.graphql_schema.content
}

AppSync のクエリを実行するには認証が必要で、以下の 5 つの認証モードが用意されています
今回はAWS_IAM認証のみを利用しますが、AppSync は複数の認証モードの利用をサポートしています
たとえば、Cognito 未認証でも使用できる操作を IAM ポリシーに定義しておき、Cognito 認証済ユーザーが実行できる AppSync クエリと Cognito 未認証ユーザーが実行できる AppSync クエリを切り分けるなどできるようになります

  • API_KEY
    • 認証モードをAPI_KEYにすると API キーが発行される
      • コンソール上で確認可能で、キーの再発行もできる
    • HTTP ヘッダーのX-API-Keyを付与してリクエストする
      • ブラウザから利用すると API キーはユーザーに見える
    • キーの expire は最大 365 日となり、有効期限になったら手動で更新が必要
    • 本番用途ではなく、主に開発用途で利用する
  • AWS_IAM
    • AppSync を操作できるポリシーを付与したリソースでクエリを実行できるようにする
  • AMAZON_COGNITO_USER_POOLS
    • Amazon Cognito を使った認証・認可のサービスを使える
    • ユーザー認証によって発行されたトークンを使って AppSync のクエリを実行する
  • OPENID_CONNECT
    • Auth0 などの利用する認証プロバイダーを決める
    • ユーザー認証によって発行されたトークンを使って AppSync のクエリを実行する
  • AWS_LAMBDA
    • 独自の認証ロジックを実装する
kurosamekurosame

AppSync のデータソースを設定

resource "aws_appsync_datasource" "this" {
  name             = "Example"
  api_id           = aws_appsync_graphql_api.this.id
  type             = "AMAZON_DYNAMODB"
  service_role_arn = [上記で作成したロールのARN]
  dynamodb_config {
    table_name = aws_dynamodb_table.this.name
  }
}
kurosamekurosame

AppSync のリゾルバーを設定

データソースに設定した DynamoDB 用リゾルバーのマッピングテンプレートを作成します
(以下はコンソール上からスキーマのリソースを作成した時に自動生成されたものをほぼそのまま使っています)

AppSync リゾルバーは Lambda が使えるので、Lambdaがサポートしている言語を使って実装することもできますが、今回は手軽に VTL で書きます

Mutation.add

DynamoDB へアイテムの追加

mutation-add-request.vtl
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($util.autoId())
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
  "condition": {
    "expression": "attribute_not_exists(#id)",
    "expressionNames": {
      "#id": "id"
    }
  }
}
  • key
    • DynamoDB のキー(パーティションキー・ソートキー)を設定
    • キーはクライアントから渡さず、$util.autoId()で UUID を生成して設定している
  • attributeValues
    • 上記のキー以外の項目を設定
    • こちらはクライアントからリクエストパラメーターで渡す
  • condition
    • 条件式を設定でき、true の場合は操作が実行され、false の場合はエラーを返す
    • ここでは、キー(生成された id)がテーブル内で重複していない場合のみ書き込みが行われるようにしている
      • DynamoDB のデフォルト動作だとキーが重複した場合は値を上書きする
    • expressionNames
      • expression の変数名と DynamoDB の属性名のマッピング
response.vtl
$util.toJson($context.result)
data "local_file" "resolver_mutation_add_request" {
  filename = "${path.cwd}/appsync/resolvers/mutation-add-request.vtl"
}

data "local_file" "resolver_response" {
  filename = "${path.cwd}/appsync/resolvers/response.vtl"
}

resource "aws_appsync_resolver" "mutation_add" {
  api_id            = aws_appsync_graphql_api.this.id
  type              = "Mutation"
  field             = "add"
  data_source       = aws_appsync_datasource.this.name
  request_template  = data.local_file.resolver_mutation_add_request.content
  response_template = data.local_file.resolver_response.content
}

Mutation.delete

DynamoDB のアイテムを指定した ID で削除する

mutation-delete-request.vtl
{
  "version": "2017-02-28",
  "operation": "DeleteItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id)
  }
}
data "local_file" "resolver_mutation_delete_request" {
  filename = "${path.cwd}/appsync/resolvers/mutation-delete-request.vtl"
}

resource "aws_appsync_resolver" "mutation_delete" {
  api_id            = aws_appsync_graphql_api.this.id
  type              = "Mutation"
  field             = "delete"
  data_source       = aws_appsync_datasource.this.name
  request_template  = data.local_file.resolver_mutation_delete_request.content
  response_template = data.local_file.resolver_response.content
}

Query.findAll

DynamoDB のアイテムをすべて返す

query-findall-request.vtl
{
  "version": "2017-02-28",
  "operation": "Scan"
}

DynamoDB の Scan は以下に書かれているとおり、コストが高くなる可能性や制限がかかることもある処理ですが、今回は個人開発の規模の小さいアプリなので普通に使います
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html

data "local_file" "resolver_query_findall_request" {
  filename = "${path.cwd}/appsync/resolvers/query-findall-request.vtl"
}

resource "aws_appsync_resolver" "query_findall" {
  api_id            = aws_appsync_graphql_api.this.id
  type              = "Query"
  field             = "findAll"
  data_source       = aws_appsync_datasource.this.name
  request_template  = data.local_file.resolver_query_findall_request.content
  response_template = data.local_file.resolver_response.content
}
kurosamekurosame

動作確認

Terraform で以下を追加し、コンソールから API キーを発行します

resource "aws_appsync_graphql_api" "this" {
  ...

  # 以下を追加
  additional_authentication_provider {
    authentication_type = "API_KEY"
  }
}

API_KEY 認証を有効にするため、aws_api_keyディレクティブを付与します

keyword.graphql
type Keyword @aws_api_key {
  id: ID!
  keyword: String!
}

type Keywords @aws_api_key {
  items: [Keyword]
}

type Query @aws_api_key {
  findAll: Keywords
}

上記の設定で API キーを使った認証が有効になりますが、デフォルトの認証に設定した AWS_IAM 認証が無効になります
よって、動作確認が終わったらこのディレクティブは削除します
(ちなみに@aws_api_key @aws_iamのように両方有効にすることも可能です)

以下のリクエストで結果を取得できます

POST https://[APIのURL]/graphql
Content-Type: application/graphql
X-API-Key: [APIキー]

{
  "query": "query { findAll { items { id keyword } } }"
}

API の URL とキーはコンソールの設定から確認できます

kurosamekurosame

Cognito 認証に変更

デフォルトの認証を IAM 認証から Cognito 認証へ変更します
開発用に API キー認証は引き続き許可しています

data "aws_region" "current" {}

resource "aws_appsync_graphql_api" "this" {
  name                = "example"
  authentication_type = "AMAZON_COGNITO_USER_POOLS"
  schema              = data.local_file.graphql_schema.content
  user_pool_config {
    aws_region     = data.aws_region.current.name
    user_pool_id   = aws_cognito_user_pool.this.id
    default_action = "ALLOW"
  }

  additional_authentication_provider {
    authentication_type = "API_KEY"
  }
}

default_action はALLOWDENYが設定できる

  • ALLOW
    • 認証済ユーザーであれば、すべての GraphQL 操作を許可する
    • 認証のみ行える設定
  • DENY
    • 認証済ユーザーであっても、スキーマで@aws_authを定義しないと GraphQL 操作が行えない
    • @aws_authを使うことでユーザーごとに利用可能な GraphQL 操作を定義できる
    • 認証・認可が行える設定
kurosamekurosame

Cognito を設定

Cognito は認証・認可とユーザー管理機能を提供します
今回やりたいことは Cognito 認証で生成したトークンを使って、AppSync リソースへのアクセス権をユーザーに付与することです

Cognito にはユーザープールと ID プールという 2 つの概念があって、ユーザープールが認証、ID プールが認可を担当します
今回はユーザープールのみを利用します
もし、認可の処理が必要になったら AppSync の GraphQL スキーマで@aws_authを付与してなんとかします
また、ID プールは未認証ユーザーにそれ用の IAM ロールを付与して、AWS リソースへのアクセスが可能になりますが、今回はやりません

サインインについては、ユーザー名とパスワードを利用して行います
(今回は設定しませんが、Google や Facebook などのサードパーティを利用したサインインも可能です)

ユーザープールを作成します

resource "aws_cognito_user_pool" "this" {
  name             = "example"
  alias_attributes = ["email"]
  password_policy {
    minimum_length                   = 8
    require_numbers                  = true
    require_symbols                  = false
    require_uppercase                = true
    require_lowercase                = true
    temporary_password_validity_days = 1
  }
  mfa_configuration = "OFF"
  account_recovery_setting {
    recovery_mechanism {
      name     = "verified_email"
      priority = 1
    }
  }
  email_configuration {
    email_sending_account = "COGNITO_DEFAULT"
  }
}

色々設定していますが、以下だけでもデフォルト値でいい感じに作ってくれます

resource "aws_cognito_user_pool" "this" {
  name = "example"
}

次にアプリクライアントを作成します
Cognito ユーザープールへはこのアプリクライアント経由でアクセスします
よって、認証トークンの発行などを SDK を使って行う際はこのアプリクライアントのクライアント ID が必要になります

resource "aws_cognito_user_pool_client" "this" {
  name            = "example"
  user_pool_id    = aws_cognito_user_pool.this.id
  generate_secret = false
  explicit_auth_flows = [
    "ALLOW_ADMIN_USER_PASSWORD_AUTH",
    "ALLOW_USER_SRP_AUTH",
    "ALLOW_REFRESH_TOKEN_AUTH"
  ]
  refresh_token_validity = 30
  access_token_validity  = 1
  id_token_validity      = 1
}

explicit_auth_flows はアプリケーションが許可する認証フローを設定します

  • ALLOW_ADMIN_USER_PASSWORD_AUTH
    • ユーザー名とパスワードによる認証を許可
    • 後述の CLI 経由でパスワードを設定する際に必要なので設定しました
  • ALLOW_USER_SRP_AUTH
    • SRP プロトコルに基づいた認証を許可
    • 後述の AppSync のコンソール上からログインして、クエリを実行する際に必要なので設定しました
  • ALLOW_REFRESH_TOKEN_AUTH
    • トークンベースでの認証を許可
    • これはデフォルトで必須になってます
kurosamekurosame

ユーザーを作成

ユーザーは Cognito ユーザープールのコンソール上から作成します
作成後、ユーザーのステータスがFORCE_CHANGE_PASSWORD(パスワードを強制的に変更)となっており、これはユーザー自身がパスワードを変更する必要があります
よって、本来はそれ用の Web ページを用意して、SDK 経由でパスワードを変更させる手段を提供する必要がありますが、今回は CLI 経由で変更します

aws cognito-idp admin-initiate-auth \
  --user-pool-id [ユーザープールID] \
  --client-id [アプリクライアントID] \
  --auth-flow ADMIN_USER_PASSWORD_AUTH \
  --auth-parameters USERNAME=[ユーザー名],PASSWORD=[パスワード] | jq .

ユーザープール ID やアプリクライアント ID はコンソール上から確認できます
パスワードはユーザーを作成した際に仮発行したパスワードを設定します

実行すると以下のレスポンスが返ってきます

{
  "ChallengeName": "NEW_PASSWORD_REQUIRED",
  "Session": "...",
  "ChallengeParameters": {
    "USER_ID_FOR_SRP": "[ユーザー名]",
    "requiredAttributes": "[]",
    "userAttributes": "{\"email_verified\":\"true\",\"email\":\"[メールアドレス]\"}"
  }
}

NEW_PASSWORD_REQUIRED はまだトークンが発行できず、パスワードを変更する必要があることを示しています
トークンの代わりにセッション値が返ってきます

以下のコマンドで新しいパスワードを設定します

aws cognito-idp admin-respond-to-auth-challenge \
  --user-pool-id [ユーザープールID] \
  --client-id [アプリクライアントID] \
  --challenge-name NEW_PASSWORD_REQUIRED \
  --challenge-responses NEW_PASSWORD=[パスワード],USERNAME=[ユーザー名] \
  --session "[上記レスポンスのセッション値]" | jq .

実行すると以下のレスポンスが返ってきます

{
  "ChallengeParameters": {},
  "AuthenticationResult": {
    "AccessToken": "...",
    "ExpiresIn": 3600,
    "TokenType": "Bearer",
    "RefreshToken": "...",
    "IdToken": "..."
  }
}

トークンが発行できました

  • AccessToken
    • ユーザー属性の変更に使う
  • IdToken
    • AWS リソースへの認証に使う
    • このトークンを使って、AppSync にアクセスする
  • RefreshToken
    • 期限切れなどで無効になった AccessToken と IdToken を再生成するのに使う

また、ユーザーのステータスもCONFIRMED(確認済み)となっています

再度トークンを発行する場合は、最初のコマンドと同じ以下を実行します

aws cognito-idp admin-initiate-auth \
  --user-pool-id [ユーザープールID] \
  --client-id [アプリクライアントID] \
  --auth-flow ADMIN_USER_PASSWORD_AUTH \
  --auth-parameters USERNAME=[ユーザー名],PASSWORD=[パスワード] | jq .
kurosamekurosame

動作確認

Cognito 認証で生成したトークンを使って AppSync を実行します
トークンは CLI のレスポンスのIdTokenを使います

cURL

POST https://[APIのURL]/graphql
Content-Type: application/graphql
Authorization: [IdToken]

{
  "query": "query { findAll { items { id keyword } } }"
}

コンソール

AppSync のコンソールからクエリを選択
画面にユーザープールでログインというボタンがあるので、そこからログインするとクエリが実行できます

このスクラップは2022/03/16にクローズされました