undefined

bokuweb.me

AWS CDKで Rust + AppSyncの構成をつくるメモ

構成としては認証をCognito UserPoolで行い、AppSyncからLambdaを呼び出してJSONを返す構成とする。

UserPool

UserPoolを用意する。this.node.tryGetContextでコンテキストが渡せるのでここに環境名、例えばprodなどを与えてSuffixとするようにした。

import * as path from "path";

import * as cdk from "@aws-cdk/core";
import * as cognito from "@aws-cdk/aws-cognito";

export class MyStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const env = this.node.tryGetContext("env");
    const userPool = new cognito.UserPool(this, "MyUserPool" + env, {
      userPoolName: "MyUserPool-" + env
    });
    }
}

cognito.UserPoolHigh-level constructsというものっぽくて細かい設定が隠蔽されてて触れない気がする。

正しいかわからないけど

const userPoolCfn = userPool.node.defaultChild as cognito.CfnUserPool;

とすると細かい設定ができるように見える

github.com

Lambda

Lambdaとロールを作る。ひとまずCloudWatchLogsのみアクセス可能とする。必要に応じてDynamoやRDS、S3への権限をつける。

Rustの場合はruntimeはlambda.Runtime.PROVIDEDとする。PROVIDEDの場合handlerの設定は無視されるっぽい。

lambda.Code.fromAssetにバイナリのパスを設定しておくとcdk deployでバイナリをS3にあげてセットしてくれる。

import * as path from "path";

import * as cdk from "@aws-cdk/core";
import * as appsync from "@aws-cdk/aws-appsync";
import * as cognito from "@aws-cdk/aws-cognito";
import * as lambda from "@aws-cdk/aws-lambda";
import * as iam from "@aws-cdk/aws-iam";

import { definition } from "./schema";

export class MyStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
  
     ...省略...

    const lambdaRole = new iam.Role(this, "MyLambdaRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      path: "/service-role/",
      inlinePolicies: {
        CloudWatchWritePolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              resources: ["*"]
            })
          ]
        })
      }
    });

    const MyLambda = new lambda.Function(this, "MyLambda", {
      functionName: `my-function-${env}`,
      runtime: lambda.Runtime.PROVIDED,
      handler: "index.handler",
      code: lambda.Code.fromAsset(
        path.join(
          __dirname,
          "../../target/x86_64-unknown-linux-musl/release"
        )
      ),
      role: lambdaRole
    });
  }
}

AppSync

AppSync作成時にauthenticationType: "AMAZON_COGNITO_USER_POOLS"にしてUserPoolIdを渡す。

new appsync.CfnGraphQLSchemaにSchemaを渡す。 schemaは別ファイルに以下のように書いといてこれを読ます。

export const definition = `
  type Hello {
    world: String!
  }

  type Query {
    getHello: Hello!
  }

  type Schema {
    query: Query
  }
`;

あとはDataSourceとResolverを追加する。 DataSource用のRoleにはLambda呼び出し権限を忘れないように。

import * as path from "path";

import * as cdk from "@aws-cdk/core";
import * as appsync from "@aws-cdk/aws-appsync";
import * as cognito from "@aws-cdk/aws-cognito";
import * as lambda from "@aws-cdk/aws-lambda";
import * as iam from "@aws-cdk/aws-iam";

import { definition } from "./schema";

export class MyStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {

    ...省略...

    const gql = new appsync.CfnGraphQLApi(this, "AppSyncAPI", {
      name: `MyGQL-${env}`,
      authenticationType: "AMAZON_COGNITO_USER_POOLS",
      userPoolConfig: {
        awsRegion: "ap-northeast-1",
        defaultAction: "ALLOW",
        userPoolId: userPool.userPoolId
      }
    });

    const schema = new appsync.CfnGraphQLSchema(this, "GqlSchema", {
      apiId: gql.attrApiId,
      definition
    });

    const dataSourceIamRole = new iam.Role(this, "dataSourceIamRole", {
      assumedBy: new iam.ServicePrincipal("appsync.amazonaws.com"),
      inlinePolicies: {
        InvokeLambdaFunction: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: ["lambda:invokeFunction"],
              resources: ["*"]
            })
          ]
        })
      }
    });

    const dataSource = new appsync.CfnDataSource(this, "DataSource", {
      apiId: gql.attrApiId,
      name: "LambdaDataSource",
      serviceRoleArn: dataSourceIamRole.roleArn,
      type: "AWS_LAMBDA",
      lambdaConfig: {
        lambdaFunctionArn: MyLambda.functionArn
      }
    });

    const addItemResolver = new appsync.CfnResolver(this, "HelloResolver", {
      apiId: gql.attrApiId,
      typeName: "Query",
      fieldName: "getHello",
      dataSourceName: dataSource.name,
      requestMappingTemplate: `{
        "version": "2018-05-29",
        "operation": "Invoke",
        "payload": {
          "now": $util.toJson($util.time.nowISO8601()),
        }
      }`,
      responseMappingTemplate: `$util.toJson($ctx.result)`
    });
    addItemResolver.addDependsOn(schema);
  }
}

Rust

main.rsを以下のように用意。 基本的にはここに書いてある通り。

aws.amazon.com

実行バイナリの名前をbootstrapにするのを忘れないこと。

#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;

use lambda::error::HandlerError;
use std::error::Error;

#[derive(Deserialize, Clone)]
struct CustomEvent {}
#[derive(Serialize, Clone)]
struct CustomOutput {
    world: String,
}
fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info)?;
    lambda!(my_handler);
    Ok(())
}
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    Ok(CustomOutput {
        world: format!("Dekita!"),
    })
}

あとはビルド。もちろんmuslで。

cargo build --release --target x86_64-unknown-linux-musl

あとはCDKでデプロイすればよい。

cdk deploy -c env="dev"

ローカルでLambdaを実行

ローカルで動かすにはSAMを使えばいいっぽい。 以下のようなtemplate.yamlを用意し

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
  TestRustFunc:
    Type: "AWS::Serverless::Function"
    Properties:
      Handler: sam_local_test
      Runtime: provided
      CodeUri: ./rust.zip

zipしてから

zip -j rust.zip ./target/x86_64-unknown-linux-musl/release/bootstrap

以下で一応動作は確認できた

sam local invoke "TestRustFunc" -e event.json
2020-01-29 19:34:57 Mounting /tmp/tmp6efof_c2 as /var/task:ro,delegated inside runtime container
START RequestId: 136f6ca2-6a4f-121d-5ece-30d4999340b1 Version: $LATEST
2020-01-29 10:34:58,203 INFO  [lambda_runtime::runtime] Received new event with AWS request id: 136f6ca2-6a4f-121d-5ece-30d4999340b1
2020-01-29 10:34:58,205 INFO  [lambda_runtime::runtime] Response for 136f6ca2-6a4f-121d-5ece-30d4999340b1 accepted by Runtime API
END RequestId: 136f6ca2-6a4f-121d-5ece-30d4999340b1
REPORT RequestId: 136f6ca2-6a4f-121d-5ece-30d4999340b1  Init Duration: 277.65 ms        Duration: 3.08 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 9 MB

{"world":"Dekita!"}

今やってるお仕事、これに置き換えたい...