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

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

Golangのx.text.transformが便利だった

タイトルにある様にGolangのx.text.transformパッケージについてです。
このパッケージを使うと、readerやwriter内のデータを変換することができます。
良くある用途としては、readerで読み込んだ文字列の一部を変換して出力する、などがあると思います。

このパッケージを知った切っ掛けは以下のSlideShareです。
オススメの標準・準標準パッケージ20選

SlideShareで言及されていますが、パッケージのドキュメントは以下となり、Exampleを見れば使い方は分かるかと思います。
https://godoc.org/golang.org/x/text/transform#Transformer
https://godoc.org/github.com/tenntenn/text/transform#example-ReplaceTable

transformパッケージ自体については上記のリンクを見れば分かるのですが
自分が理解するために書いたサンプルソースもメモ代わりに上げておきます。

Golangで2つのgzファイルを連結してみる - ソースコードから理解する技術-UnderSourceCode
以前書いた上記のソースを改修したもので、2つの圧縮ファイル(中身はjson)を読み込み、値を変換して、圧縮ファイルに出力するものです。

サンプルソース

「sample1.json.gz」「sample2.json.gz」というgzファイルをあらかじめ用意しておき、実行すると中身が連結されて「result.json.gz」というファイルに出力されます。

sample1、sample2のjsonの中身は以下のような形式で、「abc」「fgh」をそれぞれ大文字に変換して出力しています。

sample1

{
    "data":"abcde"
}

sample2

{
    "data":"fghij"
}

メインの処理は以下のようになります。

package main

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

	. "github.com/tenntenn/text/transform"
	"golang.org/x/text/transform"
)

func write(br1 io.Reader, br2 io.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, br1)
	if err != nil {
		return err
	}

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

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

	return nil
}

func doTransform(zr *gzip.Reader, old, new []byte) io.Reader {
	t := ReplaceByteTable{
		old, new,
	}
	return transform.NewReader(zr, ReplaceAll(t))
}

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()

	tr1 := doTransform(zr1, []byte(`abc`), []byte(`ABC`))
	tr2 := doTransform(zr2, []byte(`fgh`), []byte(`FGH`))

	err = write(tr1, tr2)
	if err != nil {
		log.Fatal(err)
	}

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

transformパッケージを使っているのはdoTransform()メソッドになります。
.gzファイルを読み込んだreaderを受け取り、変換するルールをReplaceByteTableに定義し、新たに別のreaderを生成して返却しています。

返却したreaderはwrite()に渡し、そのままresult.json.gzに出力しています。

このような感じにreaderの中身を変換して出力できることが確認できました。

localstackのローカル環境にAWS CDKでLambdaを配置してみた & Windows 10 でGolangのLambdaを作った

前回に続き、またlocalstackについてです。タイトルにある様にLambdaをデプロイしてみたのですが、Windows 10 で GolangのLambdaを作ったため、そこでも知らないことが出てきたのでメモ代わりに書いておきます。

localstackの起動

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!

localstackをgitでcloneし、以下のコマンドで起動しておきます。

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ docker-compose up

CDKのプロジェクトの構成

aws cdk でGolangのLambdaをデプロイしてみる - ソースコードから理解する技術-UnderSourceCode

この時に書いたときと同じ構成ですが、後に書くlambdaのビルド用バッチファイルを「main.go」と同階層に置きました。以下のような構成となります。

- lambda/ ・・・ 新規で作るフォルダ
  - bin/
     - main ・・・ main.goをビルドしたバイナリ。後述します。
  - main.go ・・・ Lambdaのソース
  - build.bat ・・・ Lambdaビルド用バッチファイル
- lib/
  - cdk-lambda-stack.ts
- node_modules/
- README.md

Lambda本体である「main.go」は、先の記事と同じものです。

lambdaのビルド

先に書いたようにWindows 10 で GolangのLambdaを作ったのですが、以下を参考に「build-lambda-zip」を使って圧縮してやる必要があるようでした。
Go の AWS Lambda デプロイパッケージ - AWS Lambda

「build-lambda-zip」を go get~で取得したあと、以下のようなバッチファイルをつくり、Lambdaのビルドは一発でできるようにしました。

build.bat

env GOOS=linux go build -o bin/main main.go
cd bin
build-lambda-zip.exe -output main.zip main

CDK

CDKについてはlambdaのビルド時に圧縮したzipをデプロイできるよう、「lib/cdk-lambda-stack.ts」を以下のようにしました。

lib/cdk-lambda-stack.ts

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';

export class CdkLambdaStack 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 lambda.Function( this, 'GoFunction', {
      functionName: 'GoFunction',
      runtime: lambda.Runtime.GO_1_X,
      handler: 'main',
      code: lambda.Code.fromAsset('./lambda/bin/main.zip')
    })
  }
}

デプロイと動作確認

localstackにデプロイするので、「cdklocal」コマンドを使用します。「cdklocal」については前回などを参考にしてください。以下のようなコマンドとなります。

# デプロイ
$ cdklocal bootstrap --endpoint-url=http://localhost:4566 --profile=localstack
$ cdklocal deploy --endpoint-url=http://localhost:4566 --profile=localstack

# Lambdaの動作確認
$ aws lambda invoke --function-name GoFunction --endpoint-url http://localhost:4566 --profile localstack response.json

localstackのローカル環境にAWS CDKでS3バケットを作成してみる

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!
「localstack」というAWSを仮想的にローカルマシン内のDockerで動かすものがあります。

このlocalstack内に、AWS CDKを使ってS3バケットを作成してみました。

localstackの準備

localstack と Visual Studio Code の Remote-Containers でAWSの開発環境を構築してみる - ソースコードから理解する技術-UnderSourceCode
以前にも書いたように、git cloneでlocakstackを取得し、dockerで起動します。
詳細な手順は公式や上記を参照して欲しいですが、コマンドだけ書くと以下のようになります。

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ docker-compose up

インストール後、クレデンシャルファイルに プロファイル名「localstack」の定義を作成しておきます。

AWS CDKとAWS Cloud Development Kit (CDK) for LocalStack(aws-cdk-local)

AWS CDKだけ先に用意し、CDKから直接 endpoint と プロファイルを指定してデプロイしようとしたのですが、以下のようなエラーメッセージができませんでした。

Unable to resolve AWS account to use. It must be either configured when you define your CDK or through the environment

locakstackより、「AWS Cloud Development Kit (CDK) for LocalStack」(aws-cdk-local)というのが出ているようなので、こちらを使うことにしました。
GitHub - localstack/aws-cdk-local: Thin wrapper script for using the AWS CDK CLI with LocalStack

公式ではローカルにnpmでインストールしているようですが、今回はAWS CDK・aws-cdk-local ともグローバルにインストールしました。

$ npm install -g aws-cdk
$ npm install -g aws-cdk-local
$ cdklocal --version

AWS CDKのプロジェクトは、以前の記事や公式サイトを元に作成しました。CDKでS3バケットを作成するだけのプロジェクトです。
aws-cdkを触ってみた - ソースコードから理解する技術-UnderSourceCode
Your first AWS CDK app - AWS Cloud Development Kit (AWS CDK)

npmによるビルドや、synthによるCloudFormationの確認などは通常のAWSをCDKで操作するときと同じように行うことが出来ます。
endpoint と プロファイルを localstackのものを指定してます。

$ npm run build
$ cdk synth --endpoint-url=http://localhost:4566 --profile=localstack

デプロイについては、aws-cdk-locakのコマンド「cdklocal」を使い、以下のように行います。

$ cdklocal deploy --endpoint-url=http://localhost:4566 --profile=localstack

デプロイされたことを確認するため、aws cliのコマンドを使って確認してみます。
ここでも endpoint と プロファイルを localstackのものを指定してます。

$ aws s3 ls --endpoint-url=http://localhost:4566 --profile=localstack
2020-11-15 15:21:50 hello-cdk-20201115

以上です。

JavaScriptにTypeScriptの型チェックを導入してみる

TypeScript: Handbook - Type Checking JavaScript Files
このようなサイトを見つけたので試してみました。公式なので当然かもしれませんが、上記のサイトに書いてあるようなJavaScriptファイルにTypeScriptの型チェックをできました。

以下、やったことのメモ書きです。

前提条件

Win10 + Visual Studio CodeJavaScript(TypeScript)の実行環境はVisual Studio CodeのRemote Containersとしました。ですが、Remote Containers以外はOS、エディタに関わらずできるはずです。

環境構築

JavaScriptの実行環境として「vscode-remote-try-node」をgit cloneし、Visual Studio CodeのRemote Containersで起動します。この時点でコンテナにはnode、npmがインストールされており、JavaScirptがデバッグ実行できるはずです。

次にTypeScriptを導入するために、以下のコマンドを実行します。

$ npm install -g typescript
$ tsc --init

tsc --init」で「tsconfig.json」が作成されますが、今回はJavaScriptに対してtscでのチェックを行いたいので、ファイル名を「jsconfig.json」に変更します。JavaScriptに対応させるため「jsconfig.json」を以下のように変更しました。(コメントは変更した理由など)

{
  "compilerOptions": {
(中略)
    "checkJs": true, // JavaScirptのチェックを行う
(中略)
    "noImplicitAny": false, // メソッドの引数でany型を指定していない場合のエラーを回避する
(中略)

tscコマンドを実行するか、Visual Studio CodeならTypeScript用のプラグインを入れることで、型チェックが行われて(エラーがあれば)適切なエラーが表示されると思います。

とりあえず最初に紹介した公式のサンプルソースで試してみましたが、「this.constructorOnly」の型エラーは表示されることを確認しました。またメソッドに引数を追加してみたところany型を指定していない旨のエラーとなったので、これについては表示されないようjsconfig.jsonを修正しています。

色んなパターンのJavaScriptのプログラムを試したわけではありませんが、一応、TypeScriptの型チェックがJavaScriptファイルに対してできたみたいです。

localstack と Visual Studio Code の Remote-Containers でAWSの開発環境を構築してみる

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!
「localstack」というAWSを仮想的にローカルマシン内のDockerで動かすものがあります。

この「localstack」で起動した仮想のAWSに S3 バケットを作り、Visual Studio Code の Remote-Containers のGolangのコードから取得できる環境をつくってみました。

localstack、Remote-Containers を使うメリットは以下があると考えています。

  • locakstackを使うことで実際のAWSに触る必要がない(つまり料金もかからない、credentialsの後述のとおりダミーで可能)
  • Remote-Containers を使うことで開発言語毎の環境をローカルにインストールする必要がなくなる

今回はWindows 10 Pro、Dockerはインストール済、AWS CLIについてはローカルのWindowsにインストール済という状態で始めました。
以下、今回やったことの手順となります。

1. localstackの起動、S3バケットの用意

AWSを仮想的に動かすため、localstackを起動します。とはいっても git clone して Dockerで起動するだけです。以下になります。

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ docker-compose up

起動したら(ローカルの) AWS CLIにlocalstackに接続するためのcredentialsを定義します。ここに記載するcredentialsのアクセスキー・シークレットキーはダミーで大丈夫なので、今回は「dummy」という値としました。以下をAWSの「credentials」ファイルに記載します。

[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy

プロファイル名は「localstack」、アクセスキーとシークレットキーは先述の通りダミーの値です。この定義を使い、AWS CLIバケットを作成してみます。

$ aws s3 mb s3://sample-bucket --endpoint-url=http://localhost:4566 --profile=localstack
$ aws s3 ls --endpoint-url=http://localhost:4566 --profile=localstack

2. Remote-Containers

Golangの開発環境を用意し、先に作ったバケットにアクセスしてみます。開発環境の用意はいくつかやり方があるかと思いますが、今回は以下の公式のサンプルを git clone して、ソースを書き替えました。
https://github.com/microsoft/vscode-remote-try-go

$ git clone https://github.com/microsoft/vscode-remote-try-go

vscode-remote-try-go」をVisual Studio Codeで開き、Remote-Containersでコンテナとして開くと、デバッグ実行や「go run」コマンドでソースの実行ができるはずです。

今回は先に用意したlocalstackの(仮想の)AWS環境にアクセスするため、「Dockerfile」に以下を追記して環境変数を追加しました。

ENV AWS_ACCESS_KEY_ID dummy
ENV AWS_SECRET_ACCESS_KEY dummy
ENV AWS_ENDPOINT "http://host.docker.internal:4566"

最初の2行はAWSのアクセスキーとシークレットキーですが、こちらもダミーの値で大丈夫です。これらがないと後述のGolangのソースを実行するときに「NoCredentialProviders: no valid providers in chain. Deprecated.」というエラーとなり、AWSにアクセスできなくなるので注意してください。
3行目はlocalstackで起動したローカルのAWSに接続するためのエンドポイントです。ポートはlocalstackで起動したコンテナのものとなり、先にAWS CLIバケットを用意したときのエンドポイントと同じとなります。「host.docker.internal」ですが、これはGolangを実行するコンテナ内からホストマシンを参照すすためのものです。「localhost」とか書いてしまうと、Golangを実行するコンテナ自身を指してしまい、「localstack」のコンテナを見ることができなくなります。

最後にGolangのソースです。AWS SDK Goを使い、バケットの一覧を取得しています。今回は「main.go」という名前にしました。

package main

import (
	"fmt"
	"log"
	"os"

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

func main() {
	sess, err := session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Region:   aws.String("ap-northeast-1"),
			Endpoint: aws.String(os.Getenv("AWS_ENDPOINT")),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	svc := s3.New(sess)
	result, err := svc.ListBuckets(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result)
}

awsのConfigの作成で、環境変数の「AWS_ENDPOINT」を参照しているのが分かるかと思います。実案件では「AWS_ENDPOINT」があれば使い、なければ使わないというような処理になるかと思います。
実行すると以下のようにAWS CLIで作成したバケット名が見れると思います。

$ go run main.go
{
  Buckets: [{
      CreationDate: 2020-10-02 09:57:33.988551 +0000 UTC,
      Name: "sample-bucket"
    }],
  (中略)
}

Windows 10 で Visual Studio Code の Remote development in Containers を試してみた

タイトルにあるように、WindowsVisual Studio Code Remote を試してみました。意図としては、開発環境ごとにコンテナを用意し、ローカルにいろいろ入れるのは止めたい、というのが希望です。

以下、その時の作業の備忘録です。

事前準備

Developing inside a Container using Visual Studio Code Remote Development
こちらにあるように、Windowsの場合は

をあらかじめインストールしておく必要があるようです。(WSL2が必要なのは意外でした。裏で使っているようですが・・・)

デバッグ実行できるかを試す

事前準備にてインストールしたら
Get started with development Containers in Visual Studio Code
の公式のチュートリアルをやってみました。

一点、チュートリアルでは「vscode-remote-try-node」をVSCode上から落としてきていますが、今回はgithubからgit cloneでローカルのフォルダに直接ダウンロードしました。

ダウンロードしたフォルダをVSCodeで開くと「Folder contains a Dev Container configuration file. Reopen folder to develop in a container (learn more).・・・」というダイアログが右下に表示されるので「Reopen in Container」を押下します。プロジェクト内に「.devcontainer/devcontainer.json」があることによりコンテナ内での開発環境だと判断されるためでしょう。

後はメインのプログラムである「server.js」の適当な箇所にブレークポイントを置き、「F5」を押下すると起動し、ブラウザからアクセスすることでデバッグできることを確認できました。

同じような流れでGolangのサンプルである「vscode-remote-try-go」もローカルにgit cloneしてデバッグを試してみましたが、こちらも「F5」で起動してデバッグすることができました。

まとめ

コンテナでのリモート開発を行うサンプルの「vscode-remote-try-~」はnodeやGolang以外にもJavaPythonなど良く使いそうなものはあるみたいです。まっさらな環境から特定言語の動作環境を作るのは意外と面倒 & ローカル環境が汚れると感じることも多いのですが、開発を行う環境ごとにコンテナを立てれば、そういった懸念も少なるかもしれません。

追記

VSCodeとDockerでMacにGolangの開発環境を作成する | Developers.IO

こちらで紹介されている手順だとコマンドパレットから直接、作りたい環境を指定してコンテナ環境を作ることができそうです。(サンプルではないですが)

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を呼び出さずにテストをパスするようにしています。