- 投稿日:2021-01-22T16:12:38+09:00
Gormでsql-migrateとseeder
はじめに 今回の記事はGO初心者エンジニアの自分が 勉強したことを書いていく記事になります。 間違いだらけかもしれないので注意 今回は Goのパッケージである Gormとsql-migrateを使用します。 DBはmysqlを使用 ※ dockerによる開発環境準備等は省きます Dockerfileに使用するパッケージの記載 FROM golang:latest WORKDIR /go/src/api/ RUN go get -u github.com/labstack/echo \ && go get github.com/jinzhu/gorm \ && go get -u github.com/go-sql-driver/mysql \ && go get github.com/rubenv/sql-migrate/... EXPOSE 8080 gormとmysqlとsql-migrateを入れています。 dbconfig.ymlを作成 development: dialect: mysql datasource: root:rootpass@tcp(db:3306)/go_db?charset=utf8&parseTime=true&loc=Asia%2FTokyo dir: db/migrations table: migrations 補足 dir: db/migrationsの欄はmigrations ファイルを作成する場所を記載する 今回は/db/migrationsフォルダ配下にmigrations ファイルが作成される 次にコンテナを起動 $ docker-compose up -d $ docker-compose exec app bash sql-migrateのコマンド実施 sql-migrate new テーブル名 このコマンドでmigrateファイルが作成される 例えば usersテーブルのmigrateファイルを作成したいなら sql-migrate new users これで作成されている db/migrations フォルダを見に行くと 中身が空のsqlファイルがあるはずです sqlを書く CREATE TABLE IF NOT EXISTS users ( id bigint AUTO_INCREMENT NOT NULL, name VARCHAR(255), email VARCHAR(255), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP NULL DEFAULT NULL, PRIMARY KEY (id) ); -- +migrate Down DROP TABLE IF EXISTS users; 今回は id name emailのカラムを持つusersテーブルを作成 マイグレーションの実行 $ sql-migrate up 確認するには $ sql-migrate status +-----------------------------+-------------------------------+ | MIGRATION | APPLIED | +-----------------------------+-------------------------------+ | 20210115071437-user.sql | 2021-01-22 15:01:52 +0900 JST | +-----------------------------+-------------------------------+ これで実行されたmigrateファイルを確認できます。 seederの作成と実行 users_seed.goの作成 今回は3名ほどuserを追加しておきましょう package main import ( "fmt" "log" "time" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) type Users struct { Name string Email string CreatedAt time.Time UpdatedAt time.Time } //struct内は最初大文字で始まるように書く!!!! func seeds(db *gorm.DB) error { users := Users{Name: "abe", Email: "abe@abe.com", CreatedAt: time.Now(), UpdatedAt: time.Now()} if err := db.Create(&users).Error; err != nil { fmt.Printf("%+v", err) } users2 := Users{Name: "asou", Email: "asou@asou.com", CreatedAt: time.Now(), UpdatedAt: time.Now()} if err := db.Create(&users2).Error; err != nil { fmt.Printf("%+v", err) } users3 := Users{Name: "suga", Email: "suga@suga.com", CreatedAt: time.Now(), UpdatedAt: time.Now()} if err := db.Create(&users3).Error; err != nil { fmt.Printf("%+v", err) } return nil } func openConnection() *gorm.DB { db, err := gorm.Open("mysql", "root:rootpass@tcp(db:3306)/go_db?charset=utf8&parseTime=true&loc=Asia%2FTokyo") if err != nil { log.Fatalf("Couldn't establish database connection: %s", err) } return db } func main() { db := openConnection() defer db.Close() if err := seeds(db); err != nil { fmt.Printf("%+v", err) return } } 動かしましょう! $ go run user_seed.go これでmigrateとseederの実装は完了です! 参考サイト
- 投稿日:2021-01-22T04:29:13+09:00
golangでFargate運用を想定したDockerfile作成(Alpineベース)
概要
個人アプリにて、Fargateでのコンテナ運用を行うため、golang環境のDockerfileを作成しました。
当環境の特徴として、
- DockerイメージをAlpine Linuxベースを使用し、軽量化。
- マルチステージビルド機能を使用し、ビルド環境でgolangアプリやその他バイナリを作成し、軽量化。
- Fargateで運用するコンテナの中に入ってシェル操作するため、ssm-agentを導入。
です。
各種ファイル
Dockerfile
FROM golang:1.14.4-alpine3.12 as builder ARG SSM_AGENT_VERSION=2.3.1205.0 RUN apk add --no-cache \ 'make~=4.3-r0' \ 'git~=2.26.2-r0' \ 'gcc~=9.3.0-r2' \ 'libc-dev~=0.7.2-r3' \ 'bash~=5.0.17-r0' RUN wget -q https://github.com/aws/amazon-ssm-agent/archive/${SSM_AGENT_VERSION}.tar.gz && \ mkdir -p /go/src/github.com && \ tar xzf ${SSM_AGENT_VERSION}.tar.gz && \ mv amazon-ssm-agent-${SSM_AGENT_VERSION} /go/src/github.com/amazon-ssm-agent && \ echo ${SSM_AGENT_VERSION} > /go/src/github.com/amazon-ssm-agent/VERSION WORKDIR /go/src/github.com/amazon-ssm-agent RUN gofmt -w agent && make checkstyle || ./Tools/bin/goimports -w agent && \ make build-linux WORKDIR /go/src/server COPY go.mod go.sum ./ RUN go mod download RUN go get bitbucket.org/liamstask/goose/cmd/goose COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/server FROM alpine:3.12 as prod RUN apk add --no-cache \ 'aws-cli~=1.18.55-r0' \ 'sudo~=1.9.0-r0' \ 'mysql-client~=10.4.15-r0' RUN adduser -D ssm-user && \ echo "Set disable_coredump false" >> /etc/sudo.conf && \ echo "ssm-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ssm-agent-users && \ mkdir -p /etc/amazon/ssm COPY --from=builder /go/src/github.com/amazon-ssm-agent/bin/linux_amd64/ /usr/bin COPY --from=builder /go/src/github.com/amazon-ssm-agent/bin/amazon-ssm-agent.json.template /etc/amazon/ssm/amazon-ssm-agent.json COPY --from=builder /go/src/github.com/amazon-ssm-agent/bin/seelog_unix.xml /etc/amazon/ssm/seelog.xml COPY --from=builder /go/bin/server /go/bin/server COPY --from=builder /go/bin/goose /go/bin/goose RUN mkdir -p /go/bin/db COPY ./db/dbconf.yml /go/bin/db/dbconf.yml COPY ./db/migrations /go/bin/db/migrations COPY ./db/mysql/init /docker-entrypoint-initdb.d EXPOSE 8080 COPY ./aws/docker-entrypoint.sh / CMD ["sh", "/docker-entrypoint.sh"]docker-entrypoint.sh
#!/bin/sh set -e amazon-ssm-agent -register -code $SSM_ACTIVATE_CODE -id $SSM_ACTIVATE_ID -region "ap-northeast-1" amazon-ssm-agent & /go/bin/server要点
Alpineはssmエージェント未対応
※一部抜粋 RUN wget -q https://github.com/aws/amazon-ssm-agent/archive/${SSM_AGENT_VERSION}.tar.gz && \ mkdir -p /go/src/github.com && \ tar xzf ${SSM_AGENT_VERSION}.tar.gz && \ mv amazon-ssm-agent-${SSM_AGENT_VERSION} /go/src/github.com/amazon-ssm-agent && \ echo ${SSM_AGENT_VERSION} > /go/src/github.com/amazon-ssm-agent/VERSIONAlpineではssmエージェントのパッケージがないので、ソースから拾いビルドして設置する必要があります。
https://github.com/aws/amazon-ssm-agent/issues/140ECRスキャンはscratchベース非対応
FROM alpine:3.12 as prodDockerイメージ軽量化のため、最初はscratchベースで実行環境を構築しようとしましたが、ECRはイメージスキャン未対応のことなので、実行環境もAlpineを採用しました。
https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-scanning-troubleshooting.htmlマイグレーションツールのgooseを採用
RUN mkdir -p /go/bin/db COPY ./db/dbconf.yml /go/bin/db/dbconf.yml COPY ./db/migrations /go/bin/db/migrations開発環境でもお世話になった
goose
を本番環境でも採用しました。ビルド環境でバイナリ作成、実行環境にバイナリと必要なファイルをコピーして準備完了。ファイルの置き場所によって設定ファイルdbconf.yml
が読み込まれなくなってしまうので、注意が必要です。シェルスプリクトで起動コマンド設定
amazon-ssm-agent -register -code $SSM_ACTIVATE_CODE -id $SSM_ACTIVATE_ID -region "ap-northeast-1" amazon-ssm-agent &コンテナ内で実行するため、シェルスクリプトでコマンドを投げてあげます。この時アクチベーションキーも設定しないといけないので、コンテナ内の環境変数に設定します。
参考資料
Go 1.12 の開発環境と本番環境の Dockerfile を考える
Alpine Linux に amazon-ssm-agent をインストールする
AWS Fargateで動かしてるコンテナの中に入る方法
- 投稿日:2021-01-22T01:00:12+09:00
GCPのSpannerで allow_commit_timestamp = true のフィールドに現在時刻を入れてはいけない
はじめに
Spannerはスキーマに他のデータベース同様TIMESTAMP型がありますがオプションとして
allow_commit_timestamp = true
を指定でき、Spanner側の時計を基準としたタイムスタンプを打つことができます。これによりアプリケーション側でセットした時刻に依存しない厳格な履歴を作成できますが使い方を間違えると書き込みに失敗するケースがあったので紹介します。
TL;DR
allow_commit_timestamp = true
にしたフィールドに未来時刻を入れることはできない- このフィールドにOSから取得した現在時刻を入れると時刻ブレで未来時刻になり書き込みに失敗することがある
- 代わりにプレースホルダーを使おう
検証
環境構築
以下のコマンドでSpannerを構築して下さい。gcloudの認証等は済んでいる前提でプロジェクト名は各々の環境に置き換えて下さい。
-- schema.sql として保存 CREATE TABLE users ( id STRING(MAX) NOT NULL, created_at TIMESTAMP NOT NULL OPTIONS ( allow_commit_timestamp = true ), ) PRIMARY KEY (id);$ gcloud --project myproject spanner instances create myinstance --config regional-asia-northeast1 --nodes 1 --description "myinstance" Creating instance...done. $ gcloud --project myproject spanner databases create mydatabase --instance myinstance Creating database...done. $ gcloud --project myproject spanner databases ddl update mydatabase --instance myinstance --ddl "`cat schema.sql`" Schema updating...done.よくない使い方
実際に未来の時刻を書き込んでみましょう。以下はGoでの例です。
package main import ( "context" "fmt" "os" "time" "cloud.google.com/go/spanner" "github.com/google/uuid" ) type user struct { ID string `spanner:"id"` CreatedAt time.Time `spanner:"created_at"` } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { ctx := context.Background() client, err := spanner.NewClient(ctx, "projects/myproject/instances/myinstance/databases/mydatabase") if err != nil { return err } defer client.Close() m, err := spanner.InsertStruct("users", &user{ ID: uuid.New().String(), CreatedAt: time.Now().Add(3 * time.Second), // 3秒未来の時刻を書き込む }, ) if err != nil { return err } t, err := client.Apply(ctx, []*spanner.Mutation{m}, ) if err != nil { return err } fmt.Println(t.UTC().Format(time.RFC3339Nano)) return nil }CreatedAtのフィールドを見るとわかるように3秒未来の実行が指定されているのでFailedPreconditionが発生します。
今回は再現性を持たせるために手動で未来時刻を挿入しましたが、いつもの調子でうっかり
time.Now()
を入れるとコケたりコケなかったりといった厄介なバグが産まれます(サーバーの時刻
>SpannerのTrueTime
となった時にだけコケる)。$ go run main.go spanner: code = "FailedPrecondition", desc = "Cannot write timestamps in the future 2021-01-21T15:29:08.542145Z > 2021-01-21T15:29:06.988814Z (current time) because the allow_commit_timestamp column option is set to true for column users.created_at, or for a corresponding shared key column in this table's interleaved table hierarchy." exit status 1正しい使い方
SpannerのCommitTimestampに合わせることを意味するプレースホルダーが用意されているのでそれを使いましょう。Goでは
spanner.CommitTimestamp
です。先ほどのコードを以下のように修正して再度実行すると今度は正しく書き込みができ、且つAPIから返却されたCommitTimestampとCreatedAtに設定された時刻が正確に一致していることが確認できます。
- CreatedAt: time.Now().Add(3 * time.Second), + CreatedAt: spanner.CommitTimestamp,$ go run main.go 2021-01-21T15:39:38.595873Z $ gcloud --project myproject spanner databases execute-sql mydatabase --instance myinstance --sql "SELECT * FROM users" id created_at a62b5ef1-730f-4ae1-991b-bd8ce14af1f7 2021-01-21T15:39:38.595873Z参考文献