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

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

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

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