AppSync の使用感を確認
AppSync は GraphQL を使った完全マネージド型サービス
データソースには DynamoDB や Lambda などを選択でき、アクセスも簡単な設定で自動化される
また、AWS コンソール上で GraphQL スキーマやリゾルバーを自動生成でき、クエリの作成・検証もできます
画像はAWS ドキュメントから引用
最低限、データソースの作成、GraphQL スキーマ・リゾルバーの作成を行えば使えるので、今回はこの辺を Terraform を使って実装します
以下は実用性を考えて、Cognito 認証にする手順
DynamoDB を作成
簡単なテーブルを用意
resource "aws_dynamodb_table" "this" {
name = "Example"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
billing_mode には、PROVISIONED
かPAY_PER_REQUEST
が指定可能で省略するとPROVISIONED
になる
-
PROVISIONED
- 固定料金
- 読み取り容量ユニット(RCU)と書き込み容量ユニット(WCU)のスループットタイプを指定しておき、それに応じて課金される
-
PAY_PER_REQUEST
- 従量課金
今回は個人開発で常時リクエストはないので、PAY_PER_REQUEST
にします
もし、PROVISIONED
にする場合は、Terraform の read_capacity と write_capacity は必須です
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/*"
]
}
]
}
GraphQL のスキーマを定義
以下の単純な Query と Mutation を定義しています
- findAll
- すべてのキーワードを取得
- add
- キーワードを追加
- delete
- キーワードを削除
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
}
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
- 独自の認証ロジックを実装する
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
}
}
AppSync のリゾルバーを設定
データソースに設定した DynamoDB 用リゾルバーのマッピングテンプレートを作成します
(以下はコンソール上からスキーマのリソースを作成した時に自動生成されたものをほぼそのまま使っています)
AppSync リゾルバーは Lambda が使えるので、Lambdaがサポートしている言語を使って実装することもできますが、今回は手軽に VTL で書きます
Mutation.add
DynamoDB へアイテムの追加
{
"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 の属性名のマッピング
$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 で削除する
{
"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 のアイテムをすべて返す
{
"version": "2017-02-28",
"operation": "Scan"
}
DynamoDB の Scan は以下に書かれているとおり、コストが高くなる可能性や制限がかかることもある処理ですが、今回は個人開発の規模の小さいアプリなので普通に使います
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
}
動作確認
Terraform で以下を追加し、コンソールから API キーを発行します
resource "aws_appsync_graphql_api" "this" {
...
# 以下を追加
additional_authentication_provider {
authentication_type = "API_KEY"
}
}
API_KEY 認証を有効にするため、aws_api_key
ディレクティブを付与します
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 とキーはコンソールの設定から確認できます
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 はALLOW
かDENY
が設定できる
- ALLOW
- 認証済ユーザーであれば、すべての GraphQL 操作を許可する
- 認証のみ行える設定
- DENY
- 認証済ユーザーであっても、スキーマで
@aws_auth
を定義しないと GraphQL 操作が行えない -
@aws_auth
を使うことでユーザーごとに利用可能な GraphQL 操作を定義できる - 認証・認可が行える設定
- 認証済ユーザーであっても、スキーマで
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
- トークンベースでの認証を許可
- これはデフォルトで必須になってます
ユーザーを作成
ユーザーは 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 .
動作確認
Cognito 認証で生成したトークンを使って AppSync を実行します
トークンは CLI のレスポンスのIdToken
を使います
cURL
POST https://[APIのURL]/graphql
Content-Type: application/graphql
Authorization: [IdToken]
{
"query": "query { findAll { items { id keyword } } }"
}
コンソール
AppSync のコンソールからクエリを選択
画面にユーザープールでログインというボタンがあるので、そこからログインするとクエリが実行できます