LoginSignup
1
2

More than 3 years have passed since last update.

amplify+AppSyncで拠点から拠点への距離順でソートしたい

Posted at

これをやりたい。

なんか難しそうだから体で覚えよう。

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はひとまずこれ

amplify/backend/api/parkthrees/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
  }
}

location.png

登録したレコードはこんなクエリーで取り出せる

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してみる

parameters.json
{
    "AppSyncApiName": "graphql",
    "BonsaiEndpoint": "https://park-trees-medium-7535634961.eu-west-1.bonsaisearch.net"
}
CustomResources.json
"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ってのは手動で作ってみたやつなので気にしないでください)

location3.png

Add Custom Schema + Resolvers for a Search Query

次に amplify/backend/api/parkthrees/schema.graphql を編集してみる。

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とか以外は見本と一緒なのをかいておく

Query.nearbyTrees.req.vtl
#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)
            }
          }
        }
      }
    }
  }
}
Query.nearbyTrees.res.vtl
#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に登録しておきます。

location4.png

そしてこれをLambdaにアタッチされてるRoleから読み込めるようにしておきます。
~わからんけどとりあえずコンソールから手動で設定しておいた。~
↑CustomResources.json内で定義してました。

amplify/backend/api/parkthrees/parameters.json に下記を追記

parameters.json
    "DDBToBonsaiFunctionRole":{
        "Fn::GetAtt": [
            "functionDDBToBonsai",
            "Outputs.LambdaExecutionRole"
        ]
    }

CustomResources.json にもいろいろ追記する。ParametersResourcesを追加ですね。

CusomResources.json
"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.jsonCustomResources.jsonを編集しろとある。

parameters.json
    "DDBToBonsaiFunction":{
        "Fn::GetAtt": [
            "functionDDBToBonsai",
            "Outputs.Name"
        ]
    }
CustomResource.json
"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
mutation make{
  createTree(input:{
    species:"Willow",
    height:15.5,
    location:{
      lat:50,
      lon:-1
    }
  }){
    id
    species
    height
    location{
      lat
      lon
    }
  }
}
query
query{
  listTrees{
    items{
      id
    }
  }
}

とりあえず、普通には実行できるのを確認。

location5.png

そしてbonsaiの方は・・

location6.png

やはりデータ入ってないかー。
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
    }
  }
}

お、なんか返ってきた!

location7.png

じゃあせっかくなので、こういうデータを入れつつ・・

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
    }
  }
}

群馬に近いのは埼玉、茨城の順。
なんか合ってる感じになりました!

location8.png

むずいけども、AppSyncの裏側でElasticSearchを使えたらいろいろと幅が広がりそうだ。
今度、@searchbleもためしてみます。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2