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

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

「UNIXという考え方」を読んだ

平成最後の日、UNIXという考え方 を読みました。
shop.ohmsha.co.jp

著者自身が書いているように分かりやすい口語体で書かれているので、内容が気になる方は本書を読むことをお勧めします。

以下、本書を読んで感銘を受けたところについて備忘録として纏めておきます。なお、鍵括弧で囲っているのは本書内の文を引用しているものです。

  • 第3章 楽しみと実益をかねた早めの試作
    • unix開発者の仕事の進め方について
      • 「1. 短い機能仕様書を書く」
      • 「2. ソフトウェアを書く」
      • 「3. テストして書き直す。満足できるまで、これを繰り返す」
      • 「4. 詳細なドキュメントを(必要なら)書く」
  • 第5章 これこそ梃子の効果!
  • 第6章 対話的プログラムの危険性
    • 「すべてのプログラムはフィルタである」
  • 第8章 一つのことをうまくやろう
    • 章のタイトルそのもの
    • MHというメーラーがUI層・アプリケーション層・小プログラム集合層の3階層になっていること
      • 3層構造は、Webなどで今でも使われている
    • 「開発者は詳細仕様書の作成に何週間も何ヵ月も費やす無駄をやめ、計画の目指すおおよその方向を文章にまとめたら、さっさと開発に取りかかった方がよい。」
      • この前後のP130からの「8.1 UNIXの考え方:総括」。

Serverless Frameworkを使ってLambdaでPDFを出力するサンプルを作ってみた

久しぶりにAWS と Serverless Frameworkを使い、LambdaにてDynamoDBからデータを取得し、PDFをS3に出力するサンプルを作ってみました。

CloudWatchなどで定期的にLambdaを起動することを考えると、割と実案件ではありがちな要件かと思われます。

今回作ったサンプルは以下になります。
GitHub - SrcHndWng/go-serverless-dynamo-report

割とサクッと作れるかと思ったのですが、意外と色々調べながら作ることになったので、今回調べた点をメモとして載せておきます。

サンプルを作るのに調べたこと

  • DynamoDB Localの使い方
  • go-assets-builderを使ってテキストファイルをバイナリに組み込む方法
  • gopdfを使ってのPDF作成

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

DynamoDB Localの使い方

serverless-dynamodb-localをインストールして「$ sls dynamodb start」でDynamoDB Localを起動すると
Unable to start DynamoDB Local process!
というエラーとなりました。

以下のURLよりDynamoDB Localをダウンロードし、解凍したフォルダごと ./node_modules/dynamodb-localhost/dynamodb/bin に入れることで解決しました。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html

go-assets-builderを使ってテキストファイルをバイナリに組み込む方法

GitHub - jessevdk/go-assets-builder: Simple assets builder program for go-assets
こちらをgo get で取得します。

今回はPDFを作成するためのフォントのフォーマットファイルである「times.ttf」ファイルをgo-assets-builderを使ってバイナリに組み込みます。
バイナリに組み込む理由は、Lambdaにシングルバイナリとしてデプロイしたいためです。

READMEにも書いたのですが、「times.ttf」ファイルを以下のサイトより取得するなどして用意し
./report/assets/times.ttf に配置します。
https://github.com/oneplus1000/gopdfsample/tree/master/ttf

その上で以下のコマンドを実行すると、「assets.go」というファイル名でtimes.ttfがビルド可能な.go ファイルに変換されます。

go-assets-builder -p main -o assets.go assets/

今回はややこしいですが、バイナリに組み込んだtimes.ttfをアプリ内で読み込み、ファイルとして復元してLambdaの実行環境内の/tmp/times.ttfに出力しています。

これはtimes.ttfをフォントのフォーマットファイルとして、PDF出力のためのライブラリに渡したいためです。
実装についてはmain.goを参照してください。

assetFile, err := Assets.Open("/assets/times.ttf")

でバイナリに組み込まれたtimes.ttfを開いています。

gopdfを使ってのPDF作成

GitHub - signintech/gopdf: A simple library for generating PDF written in Go lang
gopdfを使い、PDFファイルを出力しています。すごく簡単にですが、縦線・横線とDynamoDBから取得したデータを出力してみました。

gopdfを使った理由は、すべてGolangで書かれているため、ビルドしたバイナリ単体でPDFを出力できるためです。

こちらについては今回のソースよりも、以下のサンプルソースの方が実装方法は分かりやすいかと思いますw
GitHub - oneplus1000/gopdfsample: example of gopdf (https://github.com/signintech/gopdf)

Go Moduleでローカルパッケージを作成する

Using Go Modules - The Go Blog

Go Moduleについての上記の公式サイトではhello.go、それのテストであるhello_test.goを
GOPATHの外に実装する方法について書いてあります。

ですが実際にプログラムを作るときには、ローカルに幾つかのパッケージを作ることが多いかと思います。
例えば以下のような構成です。

.
├── hello/
│   ├── hello.go
│   └── hello_test.go
└── main.go

上記の構成で

  • hello.go、hello_test.goは公式サイトのソースのまま
  • main.goからはhello.goのメソッドを呼び出す
  • 「$ go mod init example.com/hello_main3」ようなgo mod init はmain.goと同階層で行う

をしたところ、ビルド時に以下のようなエラーとなりました。

build example.com/hello_main3: cannot find module for path ~

対応方法としては以下のサイトを参考にさせて頂きました。
https://pod.hatenablog.com/entry/2018/12/26/074944

結論を書けば、main.go内でhelloパッケージをimportする際に、相対パスではなく絶対パスで指定すればいいようです。
こんな感じになります。

import "example.com/hello_main3/hello"

このように書くことで、ビルドしての実行、helloパッケージ内でのテストの実行をすることができました。

AWS SAMでLambdaのみのFunctionを作成してみた

前回に引き続き、AWS SAMネタです。
sam init コマンドで作成されてるのはAPI Gateway + Lambdaという構成のテンプレートなのですが
Lambda単体で動かしたい or Lambdaと別のイベント(S3とかDynamoDBとか)を切っ掛けに動かしたいこともあるかと思います。

なのでsam initコマンドで作成したテンプレートを改修し、Lambda単体で動くFunctionを作成してみました。
以下、やってみた手順とソースです。

手順

既にsam initでプロジェクトが作成済の前提です。

1. プロジェクト内にフォルダを新たに作り、main.goファイルを作成する。

2. main.goを以下のように作成する。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/aws/aws-lambda-go/lambda"
)

const timeFormat = "2006-01-02 15:04:05"
const messageFormat = "Hello, now is %s!"

// MyEvent ...
type MyEvent struct {
	Name string `json:"name"`
}

// HandleRequest ...
func HandleRequest(ctx context.Context) (string, error) {
	t := time.Now()
	message := createMessage(t)
	return message, nil
}

func createMessage(t time.Time) string {
	return fmt.Sprintf(messageFormat, t.Format(timeFormat))
}

func main() {
	lambda.Start(HandleRequest)
}

今回作成したのは現在日時を返すFunctionです。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-programming-model-handler-types.html
こちらを元として作成しました。

3. (デプロイまで試すなら)template.ymlを修正して、追加したFunctionをデプロイ対象に加える。

4. Makefileを修正する。

sam init で作成したhello-world、今回作成したmy-eventの両方をビルドするようにしました。

.PHONY: deps clean build

all: deps clean build

deps:
	 go get -u github.com/aws/aws-lambda-go/events
	 go get -u github.com/aws/aws-lambda-go/lambda

clean: 
	rm -rf ./hello-world/hello-world
	rm -rf ./my-event/my-event
	
build:
	GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world
	GOOS=linux GOARCH=amd64 go build -o my-event/my-event ./my-event

5. テストを作成する

main_test.goを作成し、以下のようなテストメソッドを作成しました。
mainの中身とそのままですがw、メッセージを作成する内部処理をテストしています。

package main

import (
	"fmt"
	"testing"
	"time"
)

func TestCreateMessage(t *testing.T) {
	now := time.Now()
	message := createMessage(now)
	expect := fmt.Sprintf(messageFormat, now.Format(timeFormat))
	if message != expect {
		t.Fatal("createMessage Failed.")
	}
}

6. ローカルでの実行

ビルドして、ローカルでLambdaを実行してみます。

$ make
$ sam local generate-event cloudwatch scheduled-event > my-event.json
$ sam local invoke MyEventFunction --event my-event.json

7. テストの実行

go test -v ./my-event/

AWS SAMによるテンプレートプロジェクト作成~ローカルでの実行

AWS SAMを使い、テンプレートのプロジェクトを作成してローカルで実行するまでの
流れについて纏めてみました。

既に色々なサイトで書かれてはいますが、今後の自分のために書いておきます。
前提として

  • AWS SAMの実行環境が用意されていること
  • 動作確認は(Windows Subsysytem for Linuxの)Ubuntu上で行った

となります。

プロジェクト作成

$  sam init --runtime go1.x --name your-project-name

プロジェクトが作成されるので、以下のようにMakefileを編集しました。

.PHONY: deps clean build

all: deps clean build

deps:
	 go get -u github.com/aws/aws-lambda-go/events
	 go get -u github.com/aws/aws-lambda-go/lambda

clean: 
	rm -rf ./hello-world/hello-world
	
build:
	GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world

ローカルでの実行

AWS SAMのインストールでも必要ですが、実行にはDockerが必要となります。
なのでDockerの起動もコマンドに含んでいます。

$ make
$ sam local generate-event apigateway aws-proxy > event.json # API Gatewayのイベントを記述したjsonを作成
$ sudo service docker start    # docker起動
$ sudo service docker status  # docker起動確認
$ sam local invoke HelloWorldFunction --event event.json

テスト

テストのテンプレートとして作成されるので、その実行コマンドになります。

$ go test -v ./hello-world/

AWS CLI、GolangでS3のPre Signed URLを取得してみる

AWS CLIGolangで、それぞれS3にあるオブジェクトのPre Signed URLを取得してみました。
やってみた背景としては、リクエストがあった時にS3に配置したオブジェクトを安全にリクエスト元へ渡したい、みたいな要件を耳に挟み

  • Pre Singed URLを作ればいいんじゃないのか
  • 取得元IPは固定だろうから、バケットにIP制限を別途つける

みたいなことを思いついたためです。

バケットにIP制限を付けるあたりは、以下が参考になりそうです。
S3でIP制限 - Qiita

AWS CLIでのPre Signed URL取得

事前にバケットを用意し、ファイルを入れておきます。
またAWS CLIのインストールも必要です。

以下のコマンドで取得できました。

# バケットの中身を確認
$ aws s3 ls s3://your-bucket/

# pre signed urlを発行。--expires-inで有効期間の秒を設定
$ aws s3 presign s3://your-bucket/sample.txt --expires-in 120
https://your-bucket.s3.amazonaws.com/sample.txt?AWSAccessKeyID=xxxxx

取得できたPre Signed URLをブラウザに張り付け、ファイルの中身が表示されることを確認します。
有効期間が過ぎたら再度アクセスして、今度はAccess Deniedになることも確認します。

AWS SDKでの取得

次にaws-sdk-goで取得してみます。
公式サイトのサンプルソースを改変し、以下のようなソースで実行してみました。

package main

import (
	"log"
	"os"
	"time"

	"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() {
	region := os.Args[1]
	bucket := os.Args[2]
	key := os.Args[3]

	log.Printf("your region = %s, bucket = %s, key = %s\n", region, bucket, key)

	sess, err := session.NewSession(&aws.Config{
		Region: aws.String(region)},
	)

	svc := s3.New(sess)

	req, _ := svc.GetObjectRequest(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	urlStr, err := req.Presign(2 * time.Minute)

	if err != nil {
		log.Println("Failed to sign request", err)
	}

	log.Println("Pre signed url : ", urlStr)
}

ビルドし、引数にリージョン・バケット・キーを渡して実行してみます。

$ ./s3-pre-signed-url-get-sample region your-bucket your-key
2019/03/xx xx:xx:xx Pre signed url :  https://your-bucket.s3.amazonaws.com/sample.txt?AWSAccessKeyID=xxxxx

「Pre signed url : 」以降に出力されるPre Signed URLをブラウザに張り付け、ファイルの中身が表示されることを確認します。
こちらも無事、表示されるかと思います。

まとめ

AWS CLIaws-sdk-goを使ってPre Signed URLを取得できることが分かりました。
後はこれらをアプリに組み込み、リクエストがあり次第、取得したPre Signed URLをリクエスト元に返せばいいことになります。

今回作成したGolangのソースは以下にも載せてあります。
GitHub - SrcHndWng/s3-pre-signed-url-get-sample

参考サイト

以下のサイトを参考にさせていただきました。
ありがとうございました。

【小ネタ】AWS CLIでS3のPre-Signed URLを生成できるようになっていました! | DevelopersIO
Creating Pre-Signed URLs for Amazon S3 Buckets - AWS SDK for Go

Windows Subsystem for Linux上にAWS SAMの実行環境を構築する + Golangの開発環境を用意する

タイトルにあるように、Windows Subsystem for Linux上でAWS SAMを実行する環境を構築してみました。
単純にLinux上にAWS SAMの実行環境を構築するだけと思いきや、細々と調べながら進めることになったので
その結果をメモ代わりに書いておきます。

Windows Subsystem for Linux

こちらについては特にはまるようなポイントはないです。
今回はUbuntu 18.04で用意しました。

AWS SAM

Installing the AWS SAM CLI - AWS Serverless Application Model
こちらの公式のガイドを見ればわかるのですが、AWS SAMのCLIの実行には

が必要となります。

Windows Subsystem for LinuxでDockerを動かすのが結構癖がありました。
結論から書いてしまうと

  • Dockerは17.09.0を使った
  • Windows Subsystem for Linuxを起動するコマンドプロンプト(やパワーシェルのターミナルなど)は管理者権限で起動しておく必要がある
  • 管理者権限で起動したターミナル上でdockerを起動しておいたうえで、AWS SAMのコマンドを実行する必要がある

ということになります。

詳細については既に詳しく書かれている方がいたので、そちらを参考にしてください。
どうしても Docker on Ubuntu 18.04 on WSL したかった - Qiita
Windows10 Home の WSL で Docker を使う - Qiita

Golangの開発環境

Windows Subsystem for LinuxAWS SAMを実行するにあたり、

を実現したいと思いました。

結果

ことになりました。

詳細については既に詳しく書かれている方がいたので、そちらを参考にしてください。
WSLENVでWSLとWindowsの環境変数を共有する(Go開発環境編) · re-imagine

まとめ

こんな感じで、一つ一つは簡単なのですが、結構色々と調べつつ環境の構築ができました。
最後に、今回参考にさせていただいたサイトのURLを載せておきたいと思います。