AWS上で高速に動くETLアプリケーションを、さっくり書く必要があったので調査結果をまとめつつ。
(goは触ったばかりなので非効率なところがあるかも)
前提
利用シーン
S3から複数のオブジェクトをダウンロード、加工、格納するアプリケーションを想定。
環境調査が目的なので、まずはlistobjectが出来る状態を目指す。
検証データには、s3に格納されているオープンデータ、Global Surface Summary of Dayを利用。
環境類
- go v1.12
-
aws sdk for go v2
現在Developer previewだが調査も兼ね(S3周りはそこまでIF変わっていなそうな印象を受けた)
dockerやaws cli、クレデンシャル類も設定しておく。
準備
1. ディレクトリ構成
https://github.com/golang-standards/project-layout を参考。
小さなプロジェクトにはOverkillだぜ!と書いてあるけど、変な並びになるのも気持ち悪いので、必要最低限だけ参考に。
.
├── README.md
├── build // CI/CDなどに利用するスクリプト類
│ └── docker
│ ├── Dev.Dockerfile
│ └── Prod.Dockerfile
├── cmd // アプリケーションのメイン
│ └── simple-etl-go-aws
│ └── main.go
└── internal // プロジェクト内部でしか使わないコード類
└── s3
└── listobjects.go
外部から利用されるコード類は /pkg
を作ってその配下に置くとのこと。
2. 開発用docker環境
https://hub.docker.com/_/golang よりバージョンタグのみのものを利用。
単にバージョン指定のみの場合、 x.x.x-streach
という名称のdebianベースのものが選択される。
FROM golang:1.12
RUN apt-get update && apt-get install -y \
python3-dev \
python3-pip
RUN pip3 install \
awscli # デバッグ用
WORKDIR /simple-etl-go-aw
ルートディレクトリから下記コマンドで開発用イメージビルド。
docker build -t simple-etl-go-aws:dev -f build/docker/Dev.Dockerfile .
さらにルートディレクトリから、下記コマンドでコンテナ内へ。
(~/.aws
以下にクレデンシャルが設定されていることを前提。環境変数として注入してローカルで開発をする)
docker run -it --rm \
-e AWS_DEFAULT_REGION=ap-northeast-1 \
-e AWS_ACCESS_KEY_ID=$(aws --profile default configure get aws_access_key_id) \
-e AWS_SECRET_ACCESS_KEY=$(aws --profile default configure get aws_secret_access_key) \
-v $(pwd):/simple-etl-go-aws \
simple-etl-go-aws:dev \
/bin/bash
3. go modules初期化
v1.13からはコレが標準の外部モジュール管理のツールになるとのこと。v1.12でも使えるので利用する。
# 初期化
go mod init github.com/leo-mon/simple-etl-go-aws
これによりgo.mod
が生成される。GitHubなどのユーザー名+プロジェクト名を使うと、後ほど必要なモジュールを入れ込んだ際にいい感じに名前が揃うが、公開するプロジェクトでなければ階層は深くしなくてもいい気がする。
開発
コード類
https://github.com/aws/aws-sdk-go-v2/tree/master/example/service/s3/listObjects
を元に、2ファイルへ分割(配置場所は上記ディレクトリ)
package main
import (
"fmt"
"github.com/leo-mon/simple-etl-go-aws/internal/s3"
"os"
)
func exitErrorf(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, msg+"\n", args...)
os.Exit(1)
}
func main() {
if len(os.Args) < 2 {
exitErrorf("you must specify a bucket")
}
s3.ListObjects(os.Args[1])
}
package s3
import (
"fmt"
"os"
"github.com/aws/aws-sdk-go-v2/aws/endpoints"
"github.com/aws/aws-sdk-go-v2/aws/external"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
func exitErrorf(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, msg+"\n", args...)
os.Exit(1)
}
// Lists all objects in a bucket using pagination
//
// Usage:
// listObjects <bucket>
func ListObjects(bucketname string) {
cfg, err := external.LoadDefaultAWSConfig()
if err != nil {
exitErrorf("failed to load config, %v", err)
}
cfg.Region = endpoints.UsEast1RegionID
svc := s3.New(cfg)
req := svc.ListObjectsRequest(&s3.ListObjectsInput{Bucket: &bucketname})
p := req.Paginate()
for p.Next() {
page := p.CurrentPage()
for _, obj := range page.Contents {
fmt.Println("Object: ", *obj.Key)
}
}
if err := p.Err(); err != nil {
exitErrorf("failed to list objects, %v", err)
}
}
main.go
のimport文で分かるように、go mod init
で設定したパスを起点にimportを行なっている。
走行
go run cmd/simple-etl-go-aws/main.go aws-gsod
と打つと、
go: finding github.com/aws/aws-sdk-go-v2 v0.7.0
...
go: downloading github.com/aws/aws-sdk-go-v2 v0.7.0
go: extracting github.com/aws/aws-sdk-go-v2 v0.7.0
...
Object: 1929/030050-99999.csv
Object: 1929/030750-99999.csv
Object: 1929/030910-99999.csv
...
のように、import文に含めたモジュールを自動で検知、ダウンロードしてくれる。
(この際、このコンテナ内では/go/pkg以下に入る)
ビルド
go build cmd/simple-etl-go-aws/main.go
とするとmain
がビルドされる。
コンテナを破棄するたびに再度パッケージをダウンロードする必要があるのがネックではあるが、コンテナ内/go/pkg
を適当なディレクトリにマウントしてあげれば良い、はず。