ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

aws-sdk-goのMockを使ったテスト

Mocking Techniques for Go. Go provides you all the tools you need… | by kyleyost | CBI Engineering | Jun, 2020 | Medium

こちらの記事を読んでいたところ、以下のような記述がありました。

When working with the aws-sdk, they provide interfaces for all of their major services

(https://medium.com/cbi-engineering/mocking-techniques-for-go-805c10f1676b より)

上記の記事ではDynamoDBを使う場合のテストについて書いてありますが
自分は最近S3を使ったので、S3のinterfaceを使ったテストを書いてみました。

以下、ソースについてです。

ソース

「ListObjectsPages」を使い、指定された「フォルダ」内の.logファイルを取得するメソッドと、そのテストです。
(厳密にはS3には「フォルダ」はないですが、まあサンプルということで・・・)

メソッドを実装した「logfile.go」と、テストの「logfile_test.go」から成ります。

logfile.go

package logfile

import (
	"errors"
	"fmt"
	"path"
	"path/filepath"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3iface"
)

// FindLogFiles ...
func FindLogFiles(svc s3iface.S3API, bucket, folder string) ([]string, error) {
	fmt.Printf("find logFiles start. srcBucket = %s, folder = %s\n", bucket, folder)

	var logFiles []string
	params := &s3.ListObjectsInput{
		Bucket: aws.String(bucket),
		Prefix: aws.String(folder),
	}

	err := svc.ListObjectsPages(params, func(p *s3.ListObjectsOutput, last bool) (shouldContinue bool) {
		for _, obj := range p.Contents {
			fileName := path.Base(*obj.Key)
			if filepath.Ext(fileName) != ".log" {
				continue
			}
			logFiles = append(logFiles, *obj.Key)
		}
		return true
	})
	if err != nil {
		return nil, errors.New("failed to list objects")
	}

	fmt.Println("find logFiles complete.")
	return logFiles, nil
}

logfile_test.go

package logfile

import (
	"fmt"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3iface"
)

type mockS3Client struct {
	s3iface.S3API
	objects []*s3.Object
}

func (m *mockS3Client) ListObjectsPages(input *s3.ListObjectsInput, fn func(p *s3.ListObjectsOutput, last bool) bool) error {
	output := &s3.ListObjectsOutput{}
	output.Contents = m.objects
	last := false
	fn(output, last)
	return nil
}

func TestFindLogFiles(t *testing.T) {
	test := func(objects []*s3.Object, expectCount int) {
		svc := &mockS3Client{objects: objects}
		logFiles, err := FindLogFiles(svc, "sample-bucket", "log-folder/sample-folder")
		if err != nil {
			t.Fatalf("error raise. %#v", err)
		}
		if len(logFiles) != expectCount {
			t.Fatalf("not expect logs, result count = %v\n", len(logFiles))
		}
		fmt.Printf("logFiles = %v\n", logFiles)
	}

	objects := []*s3.Object{}
	objects = append(objects, &s3.Object{Key: aws.String("log_test1.log")})
	objects = append(objects, &s3.Object{Key: aws.String("log_test2.log")})
	objects = append(objects, &s3.Object{Key: aws.String("log_test3.log")})
	objects = append(objects, &s3.Object{Key: aws.String("log_test4.err")})
	test(objects, 3)
}

FindLogFiles()ではS3のサービスのオブジェクトを「s3iface.S3API」インターフェース型の引数として受け取り、そのメソッドを呼び出して一覧を取得するようにしています。
テストではこの「s3iface.S3API」インターフェースを満たすMockを作り(「mockS3Client」)、これをFindLogFiles()に渡すことで実際にS3を呼び出さずにテストをパスするようにしています。

NuxtJSのことはじめ

タイトルにあるようにNuxtJSを始めてみました。
Visual Studio Codeでいくつか入門ページをやってみましたが、以下を覚えておくと便利そうだと思ったのでメモ代わりに残しておきます。

NuxtJSのプロジェクト作成~起動

以下のコマンドで作成します。(NuxtJSはインストール済である前提で)

$ npx create-nuxt-app your-favorite-project

インストール時に選択肢が出てきますが、今回は以下のように選択してみました。

? Project name: your-favorite-project
? Programming language: TypeScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: ESLint
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code)                           
  • modulesには「Axios」を選択
  • Linting toolsには「ESLint」

を選びました。(Rendaring modeや言語は適宜変えることになるかと思います。)

起動は以下となります。

$ npm run dev

Visual Studio Code の 設定

上記でLinting toolsに「ESLint」を選んだため、ソースを保存するたびにESListのチェックが入ります。
手動でフォーマットを変更するのは面倒なので、プロジェクトフォルダ内のVisual Studio Code の 設定ファイルに以下の記述を行いました。

.vscode/settings.json

{
    "editor.tabSize": 2,
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    }
}

Visual Studio CodeのESListプラグインはインストールしてある前提です。
見ての通り、タブのインデントの指定と、ソース保存時にESLintのフォーマットを行っています。

まとめ

必要最小限のコマンドや設定ですが、上記を覚えておくことでストレスなくHello World的なものは進められると思います。
また便利な設定などあれば追記していくつもりです。

Golangで2つのgzファイルを連結してみる

gzファイルはコマンドだと以下のように連結することができます。

cat sample1.gz sample2.gz > result.gz

これと同じようにgzファイルを連結する処理をGolangで書いてみました。
検索すれば似たようなことをやっている記事は沢山出てくるかと思いますが、備忘録替わりに載せておきます。

ソースについて

「sample1.json.gz」「sample2.json.gz」というgzファイルをあらかじめ用意しておき、実行すると中身が連結されて「result.json.gz」というファイルに出力されます。
今回はファイルの中身をjsonとしたのですが、特にjson形式を解析して何かを行っているわけではないので、中身の形式は問わないと思います。

package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"os"
)

func write(zr1 *gzip.Reader, zr2 *gzip.Reader) error {
	writeFile, err := os.OpenFile("./result.json.gz", os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer writeFile.Close()

	zw := gzip.NewWriter(writeFile)
	defer zw.Close()

	_, err = io.Copy(zw, zr1)
	if err != nil {
		return err
	}

	_, err = zw.Write([]byte("\n"))
	if err != nil {
		return err
	}

	_, err = io.Copy(zw, zr2)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	readFile1, err := os.Open("./sample1.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile1.Close()

	zr1, err := gzip.NewReader(readFile1)
	if err != nil {
		log.Fatal(err)
	}
	defer zr1.Close()

	readFile2, err := os.Open("./sample2.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile2.Close()

	zr2, err := gzip.NewReader(readFile2)
	if err != nil {
		log.Fatal(err)
	}
	defer zr2.Close()

	err = write(zr1, zr2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("-----finish-----")
}

簡単にやっていることを書くと、main()で行っていることは以下となります。

  • 2つのjsonを読み込むReader(zr1、zr2)を作成する
  • 2つのReaderをwrite()メソッドに渡して、中身を連結する

となります。

write()では以下の流れでに2つのReaderの中身を連結します。

  • 最初に結果を「result.json.gz」として出力するためのWriter(zw)を作成する
  • io.Copy()を使い、1つ目のReader(zr1)から読み込んだ内容をWriter(zw)に出力する
  • zw.Write()で改行を出力する
  • io.Copy()を使い、2つ目のReader(zr2)から読み込んだ内容をWriter(zw)に出力する


以上になりますが、io.Copy()を使ってio.Readerをio.Writerにコピーしているところや、io.WriterのWrite()で改行を書き込んでいるところなどは
Golangのio周りを意識するのにちょうど良かったように感じました。

Golangで構造体を定義せずにjsonを読み込んでみる

ふとした時にGolangjsonを読み込む話になり
事前に構造体を定義しないでreflectで読み込んで云々・・・みたいな話になりました。

そう言えばGolangでreflectをまともに使ったことがないなあ、と思い
勉強がてらサンプルソースを作ってみました。

以下、サンプルソースとその機能について、備忘録替わりに書いておきます。

機能について

サポートされるログと検出されるフィールド - Amazon CloudWatch Logs

こちらにある CloudWatch のjson形式のログを読み込むことにしました。
最初に書いたようにjsonに対応する構造体は定義せず、reflectを使いjsonを読み込みます。
ネストした任意のjsonを読み込み、キー・値・型を取得する機能を想定しています。

実行には上記のjsonを「sample.json」という名前で保存しておく必要があります。

サンプルプログラム

以下のようになりました。

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"reflect"
)

func doReflect(interfc interface{}) {
	r := reflect.ValueOf(interfc)

	if r.Kind() == reflect.Map {
		iter := r.MapRange()
		for iter.Next() {
			k := iter.Key()
			v := iter.Value()
			_, isStr := v.Interface().(string)
			_, isNum := v.Interface().(float64)
			if isStr || isNum {
				fmt.Printf("keys = %v, val = %v, type = %v\n", k, v, reflect.TypeOf(v.Interface()))
			} else {
				doReflect(v.Interface())
			}

		}
		return
	} else if r.Kind() == reflect.Slice {
		for i := 0; i < r.Len(); i++ {
			doReflect(r.Index(i).Interface())
		}
	}
}

func main() {
	bytes, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Fatal(err)
	}

	var contents interface{}
	err = json.Unmarshal(bytes, &contents)
	if err != nil {
		log.Fatal(err)
	}

	doReflect(contents)
}

実行すると、jsonのキー・値・型が出力されるかと思います。
型は今回はstring、floatのみですが、jsonによっては型を増やすことも可能かと思います。

gorunでGolangのソースをシェルスクリプトのように実行してみる

https://github.com/erning/gorun
gorunをちょっと試してみました。試した内容としては

  • gorunをローカルにgo getで入れる
  • Golangでソースを書くが、importで他のライブラリを使うものとする
  • ソースをGOPATHの外の場所に置き、シェルスクリプトのようにソースを指定して実行してみる

となります。

他のライブラリを使ってみたのは、Golangで処理を書く場合に単体で書くことは少ないと思ったためです。
以下、やってみたことについて纏めてみます。

やってみたこと

1. gorunなど必要なものを取得

「go get」で必要なものを取得します。今回は「gorun」と、ソースの中で「go-linq」を使ったので、この2つを「go get」で取得します。
https://github.com/ahmetb/go-linq

$ go get github.com/erning/gorun
$ go get github.com/ahmetb/go-linq/v3

2. gorunの取得先を確認

「go get」で取得すると、gorunの本体が「$GOPATH/bin/」配下に取得されます。この「$GOPATH/bin/」にパスが通っているか、もしくは「gorun」がパスが通っている任意の場所に入っていることを確認します。

3. ソースを書く

任意の処理を通常のGolangのように「~.go」のファイル名で実装します。ただし先頭行には「#!/usr/bin/env gorun」を記載します。
これ以外は通常のGolangの処理そのままです。私は 先に挙げた「go-linq」のREADMEのソースをそのまま使いました。

なお、エディタによっては先頭行の「#!/usr/bin/env gorun」があると入力補完やフォーマットが効かないこともあるようです。
この場合は先頭行をコメントアウトして実装し(実装中は「$ go run ~.go」などで動作確認し)、実装が終わったら先頭行を復活させればいいでしょう。

4. ソースの実行

GOPATH以外でも動くかを確認するため、ソースを任意の場所にコピーします。
ソースに実行権限をつけ、「$ ./~.go」とシェルスクリプトのようにファイル名を指定して実行してみます。

まとめ

上記のような手順で、Golangのソースをビルドしてバイナリを作成することなく、シェルスクリプトのようにファイル名で実行することができました。
シェルで書くには複雑だが、インフラ作業などで処理手順としていつでも見れるようにソースとして配置しておいて実行したい、みたいなケースでは使えるかもしれません。

「よくわかるHTTP/2の教科書」を読んだ

GWの最後の2日で「よくわかるHTTP/2の教科書」を読みました。
http://www.ric.co.jp/book/contents/book_1177.html

HTTP/2に限らず、HTTP周りについて非常に分かりやすく書かれていると感じました。

以下、ほぼ「分かりやすかった」の羅列になっていますが、備忘録替わりに各章ごとに注目した部分の感想を書いておきます。

感想

第2章

HTTP・TLSTCPという、「裏」で動いているプロトコルについて、非常に分かりやすかったです。

第3章

HTTPについて。初期バージョンは古く1990年代には使われていましたとあります。
余計な感想かもしれない、かつインターネットがメジャーになった年代を考えれば当然だが、工業製品の歴史と比べると新しいと感じました。

CookieのセッションIDの例や、KeepAliveについては分かりやすいと感じました。

第5章

HTTP/2を解説している、本書のメイン部分。
最初のHTTP/2の概要で、HTTP/2の独自機能が分かりやすく書かれています。
HTTP/2のストリームとフレーム、コネクションについて分かりやすかったです。

第6章

WebSocket、HTTP Live Streaming、Open ID Connectなどについて分かりやすく書かれています。
QUICについてはほぼ初見だったが、こちらも非常に分かりやすかったです。

まとめ

アプリケーション周りの実装を行っているだけだと、HTTP周りのプロトコルについては
フレームワークなどで隠蔽されており、詳細までは意識しないことが多いかと思います。
この辺りを改めて意識する or HTTP周りの知識を整理するのに、いい本だと思いました。

aws cdk でAthenaのNamed Queryを作成する

AWS Athenaにはよく使うクエリを保存する Named Queryという機能があります。マネージメントコンソール上では「Saved Queries」というタブに表示されます。

今回はこのNamed Queryをaws cdkでデプロイしてみました。aws cdkを使ったのは、クエリは特に開発時は何度も変更するため、cdkを使って繰り返しデプロイできるようにしたら開発効率が上がるのではないかと思ったためです。(もちろん画面にて更新することもできますが)

Named Query以外の、データベースやテーブルなどは、今回はマネージメントコンソールから手動で作成しました。以下、今回の作業の流れとなります。

  • S3のバケットを作成し、検索元データのCSVをアップロードする。
  • Athenaの画面にてデータベースを作成する。
  • Athenaの画面にてテーブルを作成する。
  • クエリ結果の保存先「Query result location」をAthenaの画面にて作成する。
  • Named Queryをaws cdkで作成、デプロイする。

以下、それぞれについて書いていきます。

データベース作成

最初のS3のバケット作成、データのCSVのアップロードについては省きます。データのCSVを事前にS3にアップロードしておきます。
データベースについては、Athenaの「Query Editor」に以下のcreate database文を流すことで作成しました。(今回はsample_carsというデータベース名とします。センス無いですが、テーブル名も同じにしました。)

create database sample_cars;

なおデータベースを作成しないと、以下のテーブル作成で使う「Create table」のリンクが出てきませんでいた(私の作業ミスの可能性もありますが。明示的にデータベースを選ばなければならなかったのか?)

テーブル作成

Athenaの画面の左側に「Create table」というリンクがあるので、クリックします。カラム名と型を指定できるので、CSVの中から取得したい対象のカラムを指定して、画面にてテーブルを作成します。画面に表示されるCreate文は以下で使うので保存しておきます。

画面にて作成されたデフォルトの状態だと、CSVの一行目が列名を示すヘッダーとなっている場合、ヘッダーが検索結果として抽出されてしまいます。これを防ぐ為には、画面にてテーブルを作成したときに表示されたCreate文を保存しておき、「'skip.header.line.count'='1'」をプロパティに追加して再度実行します。Create文は以下のようになります。

CREATE EXTERNAL TABLE IF NOT EXISTS sample_cars.sample_cars (
  `id` int,
  (中略)
)
(中略)
TBLPROPERTIES (
  'skip.header.line.count'='1', -- これを追加
  'has_encrypted_data'='false'
);

Query result location

クエリの実行結果の保存先を「Query result location」で指定します。画面右上の「Settings」をクリックするとダイアログが出てくるので、そこで指定します。

aws cdkでNamed Queryを作成

プロジェクト作成

さて本題のNamed Queryです。aws cdkでのプロジェクト作成は、以下の公式チュートリアルや、以前書いた記事を参考にしてください。
Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK)
aws-cdkを触ってみた - ソースコードから理解する技術-UnderSourceCode

aws cdkのプロジェクトフォルダを作成し、以下を実行します。

$ npx cdk init app --language=typescript
$ npx cdk --version

今回はaws cdkの「aws-athena」モジュールを使うので、以下のコマンドでインストールします。

$ npm install @aws-cdk/aws-athena

cdkのプロジェクトが作成されると、「lib」フォルダ内の.tsファイルに「The code that defines your stack goes here」というコメントがあります。ここに任意の作成したいコードを記述します。今回はAthenaのNamed Queryを作成するコードを記載します。.tsファイルは以下のようになりました。

import * as cdk from '@aws-cdk/core';
import athena = require('@aws-cdk/aws-athena');

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

    // The code that defines your stack goes here
    new athena.CfnNamedQuery(this, 'MySampleCars', {
      database: 'sample_cars',
      description: 'this is a sample named query.',
      name: 'sample-cars-query',
      queryString: 'select id, maker, model, transmission_type from sample_cars.sample_cars limit 10;',
    })
  }
}

「sample_cars」データベースを指定し、「queryString」にてSelect文を定義しているのが分かるかと思います。次にこれをデプロイしてみます。

Named Queryのデプロイ

デプロイについては標準的なaws cdkの手順通りです。私は以下のコマンドで実行しました。

$ npm run build
$ npx cdk synth
$ npx cdk deploy

デプロイを実行すると、Athenaの画面の「Saved Queries」タブに、クエリが出来ているはずです。選択して実行できることを確認してみてください。

次にクエリの変更についてです。cdkの「queryString」のSelect文を変更し、以下のコマンドを実行します。

$ npm run build
$ npx cdk synth
$ npx cdk diff
$ npx cdk deploy

最初の「npm run build」を忘れると変更が反映されないので注意です。「diff」を使うと変更箇所が分かります(今回はクエリになるはずです)。デプロイして、画面からクエリを実行して結果が変わっていれば成功です。