これをやりたい。
なんか難しそうだから体で覚えよう。
Setup Amplify Backend
まずは初期設定。
project作ってadd apiとかしてって感じですね。
$ mkdir park_threes
$ cd park_threes
$ amplify init
Scanning for plugins...
Plugin scan successful
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project parkthrees
? Enter a name for the environment dev
? Choose your default editor: Vim (via Terminal, Mac OS only)
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path: src
? Distribution Directory Path: dist
? Build Command: npm run-script build
? Start Command: npm run-script serve
...
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: parkthrees
? Choose the default authorization type for the API API key
? Enter a description for the API key: aaaa
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes
お試しだし、認証はsimpleにapi keyにしといた。
schema.graphqlはひとまずこれ
type Tree @model {
id: ID!
species: String!
location: Location!
height: Float
}
type Location {
lat: Float!
lon: Float!
}
Location って型が設定されてますね。
そしてamplify push
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | ----------------- |
| Api | parkthrees | Create | awscloudformation |
? Are you sure you want to continue? Yes
The following types do not have '@auth' enabled. Consider using @auth with @model
- Tree
Learn more about @auth here: https://docs.amplify.aws/cli/graphql-transformer/directives#auth
GraphQL schema compiled successfully.
Edit your schema at /mnt/c/Users/masra/park_threes/amplify/backend/api/parkthrees/schema.graphql or place .graphql files in a directory at /mnt/c/Users/masra/park_threes/amplify/backend/api/parkthrees/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
⠙ Updating resources in the cloud. This may take a few minutes...
そしてamplify codegen
。これって具体的に何が生成されるんだっけ?
$ amplify codegen
✔ Downloaded the schema
✔ Generated GraphQL operations successfully and saved at src/graphql
push されてるschemaをDLしてローカルのsrc/graphql関連のスクリプトを生成するわけか。
queryとかmutationとかの定義すね。
amplify mock
ここでmockでも動かしてみよう。
$ amplify mock
ってやるとmockが起動するので
こんな感じでmutationを実行しておく
mutation MyMutation {
createTree(input:
{
species: "tokyo tower",
location: {
lat: 35.658584,
lon: 139.7454316},
height: 1.5,
id: "100"}) {
id
}
}
登録したレコードはこんなクエリーで取り出せる
query MyQuery {
getTree(id: "100") {
id
species
updatedAt
location {
lat
lon
}
}
}
Elastic Search
お手本のサイトだとBonsaiっていうサービスを使ってる。 free tier
って書いてあるし、試しに使ってみますか。
https://bonsai.io/ にアクセスしsign up。いくつかの情報を登録するだけでさっくりと登録できました。
で、登録と同時にclusterは作られていたようなので、credentialsをチェック。
こういう感じのやつをメモっておきます。
https://xxxxxxxx:yyyyyyyy@zzzzzzzz-3471393681.ap-southeast-2.bonsaisearch.net:443
Key xxxxxxxx
Secret yyyyyyyy
Create the Resolvers
parameters.json
とか CustomResources.json
ってなんだろう?って思ったけど、amplify/backend/api/parkthrees/stacks/
にあるやつかな。
CloudFormationのテンプレートっぽい。
ここにElasticSearchのリソースを書けばいいのか。
見本の通りに追記してみて、一回、$ amplify push
してみる
{
"AppSyncApiName": "graphql",
"BonsaiEndpoint": "https://park-trees-medium-7535634961.eu-west-1.bonsaisearch.net"
}
"Parameters": {
"AppSyncApiId": {
"Type": "String",
},
"BonsaiEndpoint": {
"Type": "String",
}
},
"Resources": {
"BonsaiDatasource":{
"Type" : "AWS::AppSync::DataSource",
"Properties" : {
"ApiId" :{
"Ref": "AppSyncApiId"
},
"HttpConfig" : {
"Endpoint" : {
"Ref":"BonsaiEndpoint"
}
},
"Name" : "BonsaiElasticsearch",
"Type" : "HTTP"
}
}
}
すると、DataSourceが作られました。(一番上のBonsaiDataSourceってのは手動で作ってみたやつなので気にしないでください)
Add Custom Schema + Resolvers for a Search Query
次に amplify/backend/api/parkthrees/schema.graphql
を編集してみる。
input LocationInput {
lat: Float!
lon: Float!
}
input NearbyTreesInput {
location: LocationInput!
km: Int!
limit: Int!
}
type Query {
nearbyTrees(input: NearbyTreesInput!): NearbyTreesConnection!
}
type NearbyTreesConnection {
items: [NearbyTreeConnectionItem!]!
}
type NearbyTreeConnectionItem {
tree: Tree!
distance: Float!
}
すでに存在してるし、追記です。
その次は amplify/backend/api/parkthrees/resolvers/
に Query.nearbyTrees.re(q|s).vtl
とかを作成してみればいいのかな。
lsしてみるとこんな感じでファイルがあってnearbyTreesはまだない。
$ ls amplify/backend/api/parkthrees/build/resolvers/
Mutation.createTree.req.vtl Mutation.deleteTree.res.vtl Query.getTree.req.vtl Query.listTrees.res.vtl
Mutation.createTree.res.vtl Mutation.updateTree.req.vtl Query.getTree.res.vtl
Mutation.deleteTree.req.vtl Mutation.updateTree.res.vtl Query.listTrees.req.vtl
reqがrequestでresがresponseか。keyとか以外は見本と一緒なのをかいておく
#set( $indexPath = "/tree/_doc/_search" )
#set( $input = $ctx.args.input)
#set( $distance = $util.defaultIfNull($input.km, 10) )
#set( $creds = "zx5dej31tt:5inkmy78lz")
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "$indexPath",
"params": {
"headers" : {
"Content-Type" : "application/json",
"Authorization":"Basic $util.base64Encode($creds)"
},
"body": {
"size": #if( $input.limit ) $input.limit #else 100 #end,
"sort": [{
"_geo_distance": {
"location": $util.toJson($input.location),
"order": "asc",
"unit": "km",
"distance_type": "plane"
}
}],
"query": {
"bool" : {
"must" : {
"match_all" : {}
},
"filter" : {
"geo_distance" : {
"distance" : "${distance}km",
"location" : $util.toJson($input.location)
}
}
}
}
}
}
}
#set( $es_items = [] )
#set( $body = $util.parseJson($context.result.body))
#foreach( $entry in $body.hits.hits )
#set ( $item = {
"tree": $entry.get("_source"),
"distance": $entry.get("sort").get(0)
})
$util.qr($es_items.add($item))
#end
#set( $es_response = {
"items": $es_items
} )
$util.toJson($es_response)
何やってるのか理解は追いついてないが先に進もう。
Create Bonsai Index
ElasticSearchのConsoleで下記をPUTするといいらしい。
tree
っていう名前のindexができるってさ。
{
"mappings": {
"properties": {
"createdAt": {
"type": "date"
},
"height": {
"type": "float"
},
"id": {
"type": "text"
},
"species": {
"type": "text"
},
"updatedAt": {
"type": "date"
},
"location": {
"type": "geo_point"
}
}
}
}
なるほど、geo_point
ってtypeがあるわけか。
Create the Lambda Function
最後の山場かな。DynamoDBのデータをElasticSearchに伝播するLambda Functionが必要っぽい。
とりあえず add function
$ amplify function add
? Select which capability you want to add: Lambda function (serverless function)
? Provide a friendly name for your resource to be used as a label for this category in the project: DDBToBonsai
? Provide the AWS Lambda function name: DDBToBonsai
? Choose the runtime that you want to use: Python
? Do you want to access other resources in this project from your Lambda function? No
? Do you want to invoke this function on a recurring schedule? No
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local lambda function now? No
Successfully added resource DDBToBonsai locally.
$ amplify push
ちなみにpython3.8以上が必要だっていわれたけど、ubuntuだったので apt install python3.8
をして解決です。
push後、 amplify/backend/awscloudformation/nested-cloudformation-stack.yml
をみるとFunctionの情報も追加されております。
amplify/backend/function/DDBToBonsai/DDBToBonsai-cloudformation-template.json
も作られてる。
Safely store the Connection String もやらないといけないのか。ElasticSearchのEndpointをSecret Managerに登録しておきます。
そしてこれをLambdaにアタッチされてるRoleから読み込めるようにしておきます。
~わからんけどとりあえずコンソールから手動で設定しておいた。~
↑CustomResources.json内で定義してました。
amplify/backend/api/parkthrees/parameters.json
に下記を追記
"DDBToBonsaiFunctionRole":{
"Fn::GetAtt": [
"functionDDBToBonsai",
"Outputs.LambdaExecutionRole"
]
}
CustomResources.json
にもいろいろ追記する。Parameters
と Resources
を追加ですね。
"Parameters": {
...,
"DDBToBonsaiFunctionRole": {
"Type": "String"
}
},
"Resources": {
...,
"BonsaiFunctionPolicy": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "main",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams"
],
"Resource": "*"
},
{
"Sid": "10",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": {
"Fn::Sub": "arn:aws:secretsmanager:eu-west-2:${AWS::AccountId}:secret:medium-bonsai"
}
}
]
},
"Roles": [
{
"Ref": "DDBToBonsaiFunctionRole"
}
]
}
}
}
そして Lambdaのコードだけど・・これだけ?
import boto3
DEBUG = True
secretsClient = boto3.session.Session().client(service_name='secretsmanager',region_name='eu-west-2')
get_secret_value_response = secretsClient.get_secret_value(
SecretId='arn:aws:secretsmanager:eu-west-2:818304461904:secret:medium-bonsai-BHiuhN'
)
ES_ENDPOINT = get_secret_value_response['SecretString']
DOC_TYPE = '_doc'
わからん。とりあえず、Hello Worldのreturn文の前くらいに入れておくか。
ファイルはamplify/backend/function/DDBToBonsai/src/index.py
かと思います。
そしてまたparameters.json
とCustomResources.json
を編集しろとある。
"DDBToBonsaiFunction":{
"Fn::GetAtt": [
"functionDDBToBonsai",
"Outputs.Name"
]
}
"Parameters:{
"AppSyncApiId":{
"Type":"String"
},
...,
"DDBToBonsaiFunction":{
"Type":"String"
},
},
"Resources":{
...,
"ESMTreeDDBtoBonsai":{
"Type" : "AWS::Lambda::EventSourceMapping",
"Properties" : {
"Enabled" : true,
"EventSourceArn" : {
"Fn::ImportValue": {
"Fn::Join":[
":",
[
{
"Ref":"AppSyncApiId"
},
"GetAtt",
"TreeTable",
"StreamArn"
]
]
}
},
"FunctionName" : {
"Ref":"DDBToBonsaiFunction"
},
"MaximumRetryAttempts" : 0,
"StartingPosition" : "LATEST"
}
}
}
さて・・functionはこれじゃダメな気がするぞ。
DynamoDBStreamのあたりを何も書いてない。
しかしひとまずpushだ。
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | ----------------- |
| Api | parkthrees | Update | awscloudformation |
| Function | DDBToBonsai | Update | awscloudformation |
? Are you sure you want to continue? Yes
deployは正常に終わった。
mutationとかqueryを実行してみよう。
mutation make{
createTree(input:{
species:"Willow",
height:15.5,
location:{
lat:50,
lon:-1
}
}){
id
species
height
location{
lat
lon
}
}
}
query{
listTrees{
items{
id
}
}
}
とりあえず、普通には実行できるのを確認。
そしてbonsaiの方は・・
やはりデータ入ってないかー。
lambdaのコードでしょうね。ログ見ると、dynamodbstreamは飛んできてた。
{'Records': [{'eventID': '9397a35657f2c07e8ddacee1fb5e3b25', 'eventName': 'INSERT', 'eventVersion': '1.1', 'eventSource': 'aws:dynamodb', 'awsRegion': 'ap-northeast-1', 'dynamodb': {'ApproximateCreationDateTime': 1598387040.0, 'Keys': {'id': {'S': '64ef921a-26e2-49a5-8e5a-5d5ce39977df'}}, 'NewImage': {'createdAt': {'S': '2020-08-25T20:24:00.458Z'}, 'species': {'S': 'Willow'}, '__typename': {'S': 'Tree'}, 'location': {'M': {'lon': {'N': '-1'}, 'lat': {'N': '50'}}}, 'id': {'S': '64ef921a-26e2-49a5-8e5a-5d5ce39977df'}, 'height': {'N': '15.5'}, 'updatedAt': {'S': '2020-08-25T20:24:00.458Z'}}, 'SequenceNumber': '30000700000000027252856258', 'SizeBytes': 202, 'StreamViewType': 'NEW_AND_OLD_IMAGES'}, 'eventSourceARN': 'arn:aws:dynamodb:ap-northeast-1:273074835969:table/Tree-wzt4cjcy5zgmhofbulk5i2pljm-dev/stream/2020-08-19T10:14:02.825'}]}
DynamoDB to ES
コードは一部しか書かないけどあとは察しろって事だったのかな。
このあたりのコードがあればいけるでしょう、たぶん。
indexがtree
でtypeが_doc
になるのかな。authは省いていいと思う。
requestsモジュールが必要なので amplify/backend/function/DDBToBonsai/Pipfile
に書いておく。
amplifyの@searchble使うとこのへんもautoで作られちゃうみたいなので、今度試してみよう。
https://docs.amplify.aws/cli/graphql-transformer/resolvers
しかし、このへんを自前で実装してみて為にはなったかもしれん。
deployして再pushします
そろそろ終わるよ
そんなわけで、mutationからのbonsaiでGET
・・とおもったけど、bonsaiにデータ入ってない。あれ?
もしやjsonがDynamoDB特有のやつになっちゃってる?
こういう型付きのやつ。
"species": {
"S": "Willow"
},
ためしにこういう感じのテストデータぶん投げたら入った。
{
"Records": [
{
"eventID": "9397a35657f2c07e8ddacee1fb5e3b25",
"eventName": "INSERT",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "ap-northeast-1",
"dynamodb": {
"ApproximateCreationDateTime": 1598387040,
"Keys": {
"id": {
"S": "64ef921a-26e2-49a5-8e5a-5d5ce39977df"
}
},
"NewImage": {
"createdAt": "2020-08-25T20:24:00.458Z",
"species": "Willow",
"__typename":"Tree",
"location": {
"lon": -1,
"lat": 50
},
"id": "64ef921a-26e2-49a5-8e5a-5d5ce39977df",
"height": 15.5,
"updatedAt": "2020-08-25T20:24:00.458Z"
},
"SequenceNumber": "30000700000000027252856258",
"SizeBytes": 202,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:ap-northeast-1:273074835969:table/Tree-wzt4cjcy5zgmhofbulk5i2pljm-dev/stream/2020-08-19T10:14:02.825"
}
]
}
NewImageの中だけ変えてある。
ここ、Deserializeすればいいのかなぁ。
お。まさしくこれだ。
https://qiita.com/segawa/items/5c6d418019e0ca931dff
ESに投げるとこだけ抜粋するとこんな感じになりました。めんどいのでやっつけコードです。
document = record['dynamodb']['NewImage']
image = {'location':{}}
for key in document:
if key == "location":
image[key]['lat'] = float(deserializer.deserialize(document[key]['M']['lat']))
image[key]['lon'] = float(deserializer.deserialize(document[key]['M']['lon']))
else:
image[key] = deserializer.deserialize(document[key])
r = requests.put(url + id, data=simplejson.dumps(image), headers=headers)
Decimal to Jsonも問題もあったりして場当たり的なやっつけ感、ここに極まってます。
・・さて、あとはちゃんとResolver経由でESからデータを取れるかだ。
query{
nearbyTrees(input:{
location:{
lat:50.1,
lon:-0.5
},
km:100,
limit:10
}){
items{
tree{
species
height
location{
lat
lon
}
}
distance
}
}
}
お、なんか返ってきた!
じゃあせっかくなので、こういうデータを入れつつ・・
mutation make{
createTree(input:{
species:"Tokyo",
height:1,
location:{
lat:35.68944,
lon:139.69167
}
}){
id
}
},
mutation make2{
createTree(input:{
species:"Saitama",
height:1,
location:{
lat:35.85694,
lon:139.64889
}
}){
id
}
},
mutation make3{
createTree(input:{
species:"Ibaraki",
height:1,
location:{
lat:36.34139,
lon:140.44667
}
}){
id
}
},
mutation make4{
createTree(input:{
species:"Fukushima",
height:1,
location:{
lat:37.75,
lon:140.46778
}
}){
id
}
}
群馬あたりからクエリーを。
query{
nearbyTrees(input:{
location:{
lat:36.39111,
lon:139.06083
},
km:300,
limit:10
}){
items{
tree{
species
height
location{
lat
lon
}
}
distance
}
}
}
群馬に近いのは埼玉、茨城の順。
なんか合ってる感じになりました!
むずいけども、AppSyncの裏側でElasticSearchを使えたらいろいろと幅が広がりそうだ。
今度、@searchbleもためしてみます。