20210122のGoに関する記事は3件です。

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の実装は完了です! 参考サイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/VERSION

Alpineではssmエージェントのパッケージがないので、ソースから拾いビルドして設置する必要があります。
https://github.com/aws/amazon-ssm-agent/issues/140

ECRスキャンはscratchベース非対応

FROM alpine:3.12 as prod

Dockerイメージ軽量化のため、最初は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で動かしてるコンテナの中に入る方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む