HAMADAの語り草

興味のある技術のアウトプットをしたいと思います

AWS CDK V2をGoで書いてみた ~DynamoDBとAppSyncを添えて~

はじめに

こんにちは、会津大学学部二年のHAMADAです。今年も始まってもう少しで1ヶ月ですね。 最強寒波も来るらしいですが、体調にきをつけて今年も頑張りたいと思います。 さて今回は AWS CDK をGoで書いてみた話です!!

目的と動機

最近は、Alche株式会社インターンをさせていただいています!その中でAWSを学んだり、使ったりする機会が多いのでそのアウトプットとして記事を書きます。GraphQL自体は興味はあったものの使ったことがなかったので、ある程度慣れてきたAWSを使って勉強できる!という気持ちもあり、掘りさげて取り組もうと思いました。

使用技術

  • Go

    ここのところよく使う言語。最近関数型に興味がありますが、Goに甘えてしまう。

  • AWS

    Amazon Web Service 本当に色々できる(知らないこといっぱい)今回使ったのは以下

    • AWS CDK

      Cloud Development Kit プログラミング言語を用いてAWSのリソースを定義しあれこれできる代物

    • AWS AppSync

      GraphQLを用いてAPIのマネジメントができる代物

    • AWS DynamoDB

      NoSQLのデータベース

    • AWS IAM

      権限まわりを操作できる代物

やったこと

  • とりあえず、CDKプロジェクトを構築する
  • スタックを作成
  • DynamoDBを作成
  • AppSyncを作成し、クエリを投げられるようにする。

CDKプロジェクトを構築

CDKのinstall

AWS CDKのコマンドラインツールに cdk というものがあります。これ自体はnpmで提供されています。以下のコマンドを実行します

$ npm install -g aws-cdk
...
$cdk --version
2.60.0 (build 2d40d77)

プロジェクトの作成

以下のコマンドを実行すると、GoのCDKプロジェクトが作成できます。

$ mkdir aws-cdk-go
$ cd aws-cdk-go
$ cdk init --language=go

プロジェクト内には、次のファイルが作成されます。

- README.md            
- .gitignore           
- aws-cdk-go.go       
- aws-cdk-go_test.go  
- cdk.json             
- go.mod               

不要な依存関係を削除するためにgo mod tidyを実行しておきます。

スタックを作成

aws-cdk-exp.go には自動でコードが生成されますが、一度消して簡単なコードを書き直します。

package main
import (
    "github.com/aws/aws-cdk-go/awscdk/v2"
    "github.com/aws/jsii-runtime-go"
)

func main(){
    app := awscdk.NewApp(nil)
    //stackの作成
    //stack := としているがこれは後で使うからで、このままではerrorを吐く
    stack := awscdk.NewStack(app, jsii.String("AwsCdkGo"), &awscdk.StackProps{})
    app.Synth(nil)
}

空のスタックをawscdk.NewStack で作成しています。

ここで一度デプロイしてみます。

go mod tidy が必要であれば実行しておきます。

以下のようにコマンドを実行します。

$ cdk bootstrap //最初のデプロイの前だけでOK
$ cdk deploy 

bootstrapはAWS特定の環境 (アカウントとリージョン)AWS CloudFormation にテンプレートをデプロイすることです。これをしないとdeployで必要だという旨のエラーが出る。

こんな感じでWebコンソール上に作成することができました。

DynamoDBを作成

DynamoDBのテーブルを作成します。基本的には、先ほどのスタックのコードに追加していきます。

package main
import (
    "github.com/aws/aws-cdk-go/awscdk/v2"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
    "github.com/aws/jsii-runtime-go"
)
func main(){
    app := awscdk.NewApp(nil)
    //stackの作成コード
    //DynamoDBのテーブル作成
        //table := としているが後で使うからで、このままではerrorを吐く
    table := awsdynamodb.NewTable(stack, jsii.String("demo-table"), &awsdynamodb.TableProps{
        TableName: jsii.String("booktable"),
        PartitionKey: &awsdynamodb.Attribute{
            Name: jsii.String("id"),
            Type: awsdynamodb.AttributeType_STRING,
        },
        BillingMode: awsdynamodb.BillingMode_PAY_PER_REQUEST,
        Stream:      awsdynamodb.StreamViewType_NEW_IMAGE,
    })
    //テーブルの権限を作成
    tablerole := awsiam.NewRole(stack, jsii.String("dynamodb-role"), &awsiam.RoleProps{
        AssumedBy: awsiam.NewServicePrincipal(jsii.String("appsync.amazonaws.com"), &awsiam.ServicePrincipalOpts{}),
    })
    tablerole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AmazonDynamoDBFullAccess")))
    app.Synth(nil)
}

awsdynamodb.NewTable で新しくテーブルを作成できます。

引数は順にstack、作成するリソース名、プロパティとなっています。

TableNameは、作成するテーブル名。

PartitionKeyは、データがどのパーティションに保存されるか決めるもの。

BillingModeは、DynamoDBの課金方式を決めています。

Streamは、DBに変更があったときに変更情報を保存しておくもの。

ここで一度デプロイすると、

きちんとtableが作成されています。

AppSyncを作成しクエリを投げる

この手順通りにmain関数にコードを追記していきます。

"github.com/aws/aws-cdk-go/awscdk/v2/awsappsync" をimportします。

まず、appsyncを作成します。

//mainの中に追記していく
//api := としているが後で使うからで、このままではerrorを吐く
api := awsappsync.NewCfnGraphQLApi(stack, jsii.String("booksApi"), &awsappsync.CfnGraphQLApiProps{
        Name:               jsii.String("books-api"),
        AuthenticationType: jsii.String("API_KEY"),
})
awsappsync.NewCfnApiKey(stack, jsii.String("BooksApiKey"), &awsappsync.CfnApiKeyProps{
        ApiId: api.AttrApiId(),
})

awsappsync.NewCfnGraphQLApi で新しくGraphQLApiを作成します。

引数はDynamoDBの時と同様です。

Nameは、api名。

AuthenticationTypeは、権限の認証方法。

awsappsync.NewCfnApiKeyAPI KEYを作成します

引数は他と同様です。

ApiIdは、APIに割り当てられた識別子。

次に、作成したDynamoDBをAppsyncのデータソースに割り当てます。

//mainの中に追記していく
//Ds := としているが後で使うからで、このままではerrorを吐く
Ds := awsappsync.NewCfnDataSource(stack, jsii.String("DataStore"), &awsappsync.CfnDataSourceProps{
        ApiId: api.AttrApiId(),
        Name:  jsii.String("BookDataSource"),
        Type:  jsii.String("AMAZON_DYNAMODB"),
        DynamoDbConfig: awsappsync.CfnDataSource_DynamoDBConfigProperty{
            TableName: table.TableName(),
            AwsRegion: stack.Region(),
        },
        ServiceRoleArn: tablerole.RoleArn(),
    })

awsappsync.NewCfnDataSource で新しくデータソースを作成します。

引数は他と同様です。

Typeは、データソースが何かを指定できます。今回はDynamoDBですが、Lambda等でもOK。

DynamoDbConfigは、DynamoDBへの接続を定義しています。

ServiceRoleArnは、Roleを参照しています。

その次は、スキーママッピングテンプレートを別ディレクトリで定義し、それをとってきます。

   //mainの中に追記していく
    //schemaを、ファイルからとってくる
    //def := としているが後で使うからで、このままではerrorを吐く
    def, err := os.ReadFile(filepath.Join(".", "resource", "schema.graphql"))
    if err != nil {
        fmt.Println("failed to load graphql definition " + err.Error())
    }
 
    //schemaを定義
    //schema := としているが後で使うからで、このままではerrorを吐く
    schema := awsappsync.NewCfnGraphQLSchema(stack, jsii.String("GraphSchema"), &awsappsync.CfnGraphQLSchemaProps{
        ApiId:      api.AttrApiId(),
        Definition: jsii.String(string(def)),
    })

    getitem, err := os.ReadFile(filepath.Join(".", "resource", "getitem.vtl"))
    if err != nil {
        fmt.Println("failed to load  getitem.vtl " + err.Error())
    }
    putitem, err := os.ReadFile(filepath.Join(".", "resource", "putitem.vtl"))
    if err != nil {
        fmt.Println("failed to load  putitem.vtl " + err.Error())
    }

./resouce/schema.graphqlに定義しておいた、スキーマをとってきてawsappsyncに渡す。

awsappsync.NewCfnGraphQLSchema で新しくスキーマを作成します。

引数は他と同様です。

Definitionは、スキーマの中身を定義。

schema.graphqlの中身

schema {
    query: Query
    mutation: Mutation
}

type Query {
    getPost(id: ID): Post
}

type Mutation {
    addPost(
        id: ID!
        author: String!
        title: String!
        content: String!
        url: String!
    ): Post!
}

type Post {
    id: ID!
    author: String
    title: String
    content: String
    url: String
    ups: Int!
    downs: Int!
    version: Int!
}

getitem.vtlの中身

{
    "version" : "2017-02-28",
    "operation" : "GetItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
    }
}

putitem.vtlの中身

{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
    },
    "attributeValues" : {
        "author" : $util.dynamodb.toDynamoDBJson($context.arguments.author),
        "title" : $util.dynamodb.toDynamoDBJson($context.arguments.title),
        "content" : $util.dynamodb.toDynamoDBJson($context.arguments.content),
        "url" : $util.dynamodb.toDynamoDBJson($context.arguments.url),
        "ups" : { "N" : 1 },
        "downs" : { "N" : 0 },
        "version" : { "N" : 1 }
    }
}

このチュートリアルスキーママッピングテンプレートを流用しました。

チュートリアル:DynamoDB リゾルバー

最後に、QueryとMutationを投げるリゾルバーを作成。

//mainのなかに追記
//Queryを投げられるようにする
    awsappsync.NewCfnResolver(stack, jsii.String("GetResolver"), &awsappsync.CfnResolverProps{
        ApiId:                   api.AttrApiId(),
        TypeName:                jsii.String("Query"),
        FieldName:               jsii.String("getPost"),
        DataSourceName:          Ds.Name(),
        RequestMappingTemplate:  jsii.String(string(getitem)),
        ResponseMappingTemplate: jsii.String(`$util.toJson($ctx.result)`),
    }).AddDependency(schema)

    //Mutationを投げられるようにする
    awsappsync.NewCfnResolver(stack, jsii.String("AddResolver"), &awsappsync.CfnResolverProps{
        ApiId:                   api.AttrApiId(),
        TypeName:                jsii.String("Mutation"),
        FieldName:               jsii.String("addPost"),
        DataSourceName:          Ds.Name(),
        RequestMappingTemplate:  jsii.String(string(putitem)),
        ResponseMappingTemplate: jsii.String(`$util.toJson($ctx.result)`),
    }).AddDependency(schema)

awsappsync.NewCfnResolver は新しくリゾルバーを作成します。

TypeNameは、Query,Mutationなどを指定できます。

FieldNameは、スキーマに含まれているフィールドを指定します。

DataSourceNameは、今回は先ほど定義したデータソースの名前をとってきます。

RequestMappingTemplateは、putiem、getitemから指定しています。

ResponseMappingTemplateは、context.resultをJson形式にして返しています。

Webコンソールで実行できるようになりました。

まず、mutationで情報を追加。

その後、queryで情報を取得。

無事にDynamoDBとAppSyncを接続し実行するためのCDKを作成できました!!!

感想と展望

AWS CDK V2をGoで書く場合に参考になる公式ドキュメントが少なく、割と苦労しながらの実装になりました。vscodeエスパーしながら進めていたので、vscodeの補完能力には感謝しかありません。GraphQLに関しては、まだまだ知識が足りていないのでGraphQL単体でも学びたいと思っています。他の人のコードを読んだり、探したりすることが今までよりも多く学びが深まったことは大きな収穫だと思います。何かの参考になれば幸いです。ここまでお付き合い頂きありがとうございました!

所属サークル

A-PxL (@aizu_PxL) / Twitter

私のTwitter

HAMADA (@AHMOS_HMD) / Twitter

私のgithub

ahmos0 (HAMADA) · GitHub

Alche株式会社

alche.studio

参考リンク

maku.blog

pkg.go.dev