miyazi888の覚え書き日記

学習したことを書き留めてます。

entを試してみた

仕事で使いそうだったのでORマッパーのentを試してみた。 公式はこちら。

https://entgo.io/ja

検証用のディレクトリ作成

mkdir test
cd test
go mod init test

検証用のDB(postgres)を作成

検証用のDBを作成する

touch docker-compose.yml

以下のようにDBを起動するだけの定義を作成する。

docker-compose.yml

version: "3.8"

services:
  db:
    image: postgres:15.1-alpine
    ports:
      - 5432:5432
    volumes:
      - ./.data/postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: testdb

DBを起動する。

docker-compose up -d

ライブラリのインストール

entのインストール

go get -d [entgo.io/ent/cmd/ent](http://entgo.io/ent/cmd/ent)

postgresのドライバのインストール

go get github.com/lib/pq

スキーマを追加

以下のコマンドでUserスキーマの元になるファイルが生成することができる。

今回はuserテーブルを生成するので、Userを指定。

スキーマファイルはent配下に作成される。

ent new User

ent/schema/user.goなどが作成されている。

生成されたファイルを修正してスキーマを定義

ent/schema.user.goを以下のように修正し、userテーブルにnameとageの項目を追加する。

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
      field.String("name").Default("unknown"),
        field.Int("age").Positive(),
    }
}

定義したスキーマから各テーブル毎の実装を生成する

go generate ./ent

上記のコマンドでgenerate.goやuser_create.goなどの実装ファイルが生成される。

マイグレーション用のプログラムを実装

entはスキーママイグレーションする機能があるので、マイグレーション用の処理を記述

DB接続を取得する実装

db/db.go

package db

import (
    "fmt"
    "test/ent"
)

func NewDBClient() *ent.Client {
    user := "user"
    password := "pass"
    port := "5432"
    host := "localhost"
    dbName := "testdb"
    url := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, dbName)
    client, err := ent.Open("postgres", url)
    if err != nil {
        fmt.Printf("failed connecting to postgres: %v", err)
    }
    return client
}

func CloseDB(client *ent.Client) {
    err := client.Close()
    if err != nil {
        fmt.Printf("failed close to db: %v", err)
    }
}

マイグレーション処理の実装

go runコマンドで実行する予定なので、packageをmainにしています。

migrate/migrate.go

package main

import (
    "context"
    "fmt"
    "test/db"
    "test/ent/migrate"

    _ "github.com/lib/pq"
)

func main() {
    client := db.NewDBClient()
    ctx := context.Background()

    err := client.Schema.Create(
        ctx,
        migrate.WithDropIndex(true),
        migrate.WithDropColumn(true),
    )
    if err != nil {
        fmt.Printf("failed creating schema resources: %v", err)
    }

    db.CloseDB(client)
}

スキーママイグレーション実行

以下のようにしてマイグレーションプログラムを実行

go run migrate/migrate.go

実際にマイグレーションされたか確認する

マイグレーションを下のように確認

docker-compose exec db /bin/sh
# psql -h localhost -U user -d testdb
testdb=# \d users;

Table "public.users"
 Column |       Type        | Collation | Nullable |             Default              
--------+-------------------+-----------+----------+----------------------------------
 id     | bigint            |           | not null | generated by default as identity
 name   | character varying |           | not null | 'unknown'::character varying
 age    | bigint            |           | not null | 
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

CRUD操作

下記のようなプログラムで基本的なCRUDを確認。

main.go

package main

import (
    "context"
    "fmt"
    "log"
    "test/db"
    "test/ent/user"

    _ "github.com/lib/pq"
)

func main() {
    client := db.NewDBClient()
    ctx := context.Background()

    // 1件追加
    usr, err := client.Debug().User.
        Create().
        SetName("user1").
        SetAge(33).
        Save(ctx)
    if err != nil {
        fmt.Printf("failed creating user: %v", err)
        return
    }

    // 1件更新
    updatedUser, err := client.Debug().User.Update().Where(user.ID(usr.ID)).SetAge(29).Save(ctx)
    if err != nil {
        fmt.Printf("failed updating user: %v", err)
        return
    }
    log.Printf("user: %+v", updatedUser)

    // 名前がuser1のユーザーを取得
    users, err := client.Debug().User.Query().Where(user.Name("user1")).All(ctx)
    if err != nil {
        fmt.Printf("failed getting users: %v", err)
        return
    }

    for _, usr := range users {
        fmt.Printf("user: %+v", usr)
    }

    // 1件削除
    _, err = client.Debug().User.Delete().Where(user.Name("user1")).Exec(ctx)
    if err != nil {
        fmt.Printf("failed deleting user: %v", err)
        return
    }

    // DB接続を閉じる
    db.CloseDB(client)
}

上記を実行するとログとして各処理で発行されるクエリを確認して意図したクエリが発行されたことが確認できる。 最後にデータを削除しているのでDB上にはデータは残らないことに注意。

>> go run main.go
2023/04/28 22:32:37 driver.Query: query=INSERT INTO "users" ("name", "age") VALUES ($1, $2) RETURNING "id" args=[user1 33]
2023/04/28 22:32:37 driver.Exec: query=UPDATE "users" SET "age" = $1 WHERE "users"."id" = $2 args=[29 1]
2023/04/28 22:32:37 user: 1
2023/04/28 22:32:37 driver.Query: query=SELECT "users"."id", "users"."name", "users"."age" FROM "users" WHERE "users"."name" = $1 args=[user1]
user: User(id=1, name=user1, age=29)2023/04/28 22:32:37 driver.Exec: query=DELETE FROM "users" WHERE "users"."name" = $1 args=[user1]

次に

次の記事でテーブル同士を関連させて動作確認してみた。 https://miyazi888.hatenablog.com/entry/2023/05/07/221705

参考

GolangにORM導入(ent migration編) - Qiita

100%型安全なgolangORM「ent」を使ってみた | フューチャー技術ブログ

samber/loを試した

すでにgo1.19がリリースされてしまっていますが、1.18がリリースされた際にGoにGenericsが導入されて大きな話題になりました。

で、Genericsが導入されたら、絶対にどこかの誰かがfilterやmap関数みたいなののライブラリを作ると思っていたら、やっぱりありました。 その名もlo。

github.com

lodashを意識して開発された上に名前も短くしたくて、loになった模様。 lodashだと_.filterみたいな感じにしたかったんでしょうね。

install

go get github.com/samber/lo@v1

使ってみた

正直、公式のREADMEを読めばすぐに使い方はわかると思います。 それでも使い心地を試してみました。

   // 奇数のみを抽出
    results := lo.Filter([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, func(x int, _ int) bool {
        return x%2 != 0
    })
    fmt.Println(results) // [1, 3, 5, 7, 9]

今までforでがんばるしかなかったことを考えると、すっきり記述できてうれしい。

次は普通のオブジェクトでも絞り込んでみました。

type Item struct {
    Name string
    Type string
}

func main() {
    // オブジェクトの抽出
    items := []Item{{Name: "item1", Type: "foo"}, {Name: "item2", Type: "bar"}, {Name: "item3", Type: "foo"}}
    filteredItem := lo.Filter(items, func(e Item, _ int) bool { // 第二引数の関数の二番目の引数はindexです
        return e.Type == "foo"
    })
    fmt.Println(filteredItem) // [{item1 foo} {item3 foo}]
}

ちょっとJavaScriptっぽい感じで絞り込みが記述できました。

ちなみに今回は使いませんでしたが、Filter関数に渡す関数の第2引数はindexでした。

感想

上にも記述しましたが、これまでforでがんばるしかなかったことを思うと、だいぶ楽ですっきり記述できて良いな、というのが本音です。

あと、Genericsのサンプル集的に眺めることもできます。 ソースもだいぶシンプルで楽に読めると思います。 Genericsの使い方を忘れた時なんかにこのライブラリを思い出すと参考になりそうに思いました。

使う機会があれば、積極的に使いたいライブラリだと思いました。

ところでissueでこのライブラリの名前をgodashに変更したら? 的なissueが上がっててちょっと面白かったです。 github.com

その他

同じ作者の方がやっぱりGoのGenericsに対応したDIライブラリを公開していて、それもちょっと気になりました。 これも時間を見つけて試してみようと思います。

minikubeでingressを使う

k8sを学習していく中でingressという単語に遭遇し、どうやらLoad Balancer的なものであるらしい、ということまでは理解した。
ただ、実際に触ってみた方が理解しやすいかと思い、minikubeでingressを動作させてみた。

前提

以下の環境で動作確認した。

  • ubuntu 20.04
  • minikube v1.24.0
  • kubectl v1.20.8-dispatcher

それからdockerとdocker-composeが動作する環境であること。

動作確認用のdockerファイル作成

まずは動作確認用に2つのdocker imageを作成する。

mkdir html1
touch html1/index.html

html1/index.html

<html>
  <h1>sample page1</h1>
</html>
mkdir html2
touch html2/index.html
<html>
  <h1>sample page2</h1>
</html>
touch Dockerfile1

Dockerfile1

FROM 'nginx:latest'

COPY html1/index.html /usr/share/nginx/html/index.html

RUN service nginx start
touch Dockerfile2

Dockerfile2

FROM 'nginx:latest'

COPY html2/index.html /usr/share/nginx/html/index.html

RUN service nginx start
touch docker-compose.yaml

docker-compose.yaml

version: '3'
 
services:
  nginx1:
    build:
      context: ./
      dockerfile: Dockerfile1
    image: nginx-sample1
    ports:
      - 8080:80
  nginx2:
    build:
      context: ./
      dockerfile: Dockerfile2
    image: nginx-sample2
    ports:
      - 8081:80

Docker image作成と動作確認

ここまで出来たら、とりあえず上記の設定でdockerコンテナが狙ったとおりに動作するか確認。

docker-compose up -d
curl localhost:8080

以下のようなレスポンスが返ってきたら成功
<html>
  <h1>sample page1</h1>
</html>

curl localhost:8081
<html>
  <h1>sample page2</h1>
</html>

正しく動作することがわかったらコンテナを停止。

docker-compose stop

ここまでで正しく動作するdocker imageが作成されていることになる。

docker imageにタグを付ける

docker imageは出来たものの、このままではminikubeで扱えない(latestタグのimageがうまく扱えない模様)のでタグを付ける。
まずは作成されたdocker imageのIDを確認。

docker images

IDがわかったらタグ付けする。

docker tag <nginx-sample1のimage id> nginx-sample1:v1
docker tag <nginx-sample2のimage id> nginx-sample2:v1

minkube起動

minikubeを起動。

minikube start

イメージ取り込み

先程、作成したdocker imageをminikubeにロードする。
最後のminikube image lsはminikubeのimageの一覧を表示するコマンド。

minikube image load nginx-sample1:v1
minikube image load nginx-sample2:v1
minikube image ls

取り込んだイメージが使えるか確認

minikubeに取り込んだimageが本当に動作するか、ここでも動作確認する。

kubectl create deploy test1 --image=nginx-sample1:v1
kubectl expose deploy test1 --type=NodePort --port=8080 --target-port=80
minikube service test1 --url

ターミナルにURLが表示されるので、ブラウザで確認。
sample page1が表示されたら成功。

もう1つのイメージも動作確認する。

kubectl create deploy test2 --image=nginx-sample2:v1
kubectl expose deploy test2 --type=NodePort --port=8080 --target-port=80
minikue service test2 --url

こちらも表示されたURLにアクセスし、sample page2が表示されたら、OK。

ingressがminikubeで有効かどうかを確認

ingressはminikubeのaddonとして提供されている模様。
まずはaddonのリストを調べる。

minikube addons list

もしingressがenabledになっていなかったら有効にする。

minikube addons enable ingress
minikube addons list

ingressを設定する

やっと本番。ingressを追加する。

touch ingress.yaml

下の設定ではhttp://test.local/にアクセスするとsample page1のpodにアクセスし、http://test.local/test2にアクセスするとsample page2のpodにアクセスする設定。

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-test
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - host: test.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: test1
                port:
                  number: 8080
          - path: /test2
            pathType: Prefix
            backend:
              service:
                name: test2
                port:
                  number: 8080

下記のコマンドでingressを適用する。

kubectl apply -f ingress.yaml
watch kubectl get ingress

watchで表示されるingressでaddressの部分にloocalhostが表示されるまで待つ。
上で作成したingressはhostを指定しているので/etc/hostsも以下ように追記する。

/etc/hosts

127.0.0.1 test.local

これで準備完了。

動作確認

別のターミナルを起動して以下のコマンドを実行する。

minikube tunnel

元のターミナルに戻ってからcurlコマンドでレスポンスを確認する。
ブラウザでももちろん、OK。

curl http://test.local

sample page1が返ってきたら動作OK。

<html>
  <h1>sample page1</h1>
</html>
curl http://test.local/test2

こちらはsample page2が返ってきたらOK。

<html>
  <h1>sample page2</h1>
</html>

というわけでminikubeでingressを動作させることが出来た。

後片付け

下記のコマンドでingressとserviceとdeploymentを削除。

kubectl delete ingress ingress-test
kubectl delete svc test1 test2
kubectl delete deploy test1 test2

次に/etc/hostsから下記の設定を削除。
/etc/hosts

127.0.0.1 test.local

minikubeのimageから今回追加したimageを削除。

minikube image rm nginx-sample1:v1
minikube image rm nginx-sample2:v1

最後にminikubeを停止して、後片付け完了。

minikube stop

minikubeでローカルのdockerイメージを使う

きっかけ

kubernetesの学習目的でminikubeをインストール。 学習中、minikubeでもローカルのdocker imageを使用したくなった。

ネットで検索するとだいたい、下記のような方法が記載されていた。

eval $(minikube docker-env)

ところが、自分の環境では、これではローカルのdocker imageを使うことが出来なかった。

minikubeのruntimeにcontainerdを使っていることが原因ではないかと推測。 なぜなら初回のminikube start時にruntimeにcontainerdを強制されたから。

そこで他に方法はないかとネットを検索するとminikubeの公式にimageなるサブコマンドがあることに気づいた。 これを使えばどうにかローカルのイメージを使えるのでは・・・ と思い実験してみた。 https://minikube.sigs.k8s.io/docs/commands/image/

結論

minikube image loadを使うことでminikube上でもホスト側のイメージを取り込んで使用することが出来た。

minikube image load <image-name>:<image-tag>

動作確認

以下、本当にローカルのイメージをminikubeで使うことができるのか実験してみた。

前提

動作確認した環境は以下のとおり。 上にも記述したようにminikubeのruntimeはcontainerd。

  • ubuntu 20.04
  • minikube v1.24.0
  • kubectl v1.20.8-dispatcher

動作確認用のdockerイメージ作成

htmlファイル作成

まずは動作確認用に下記のようなHTMLを表示するイメージを作成。

mkdir html
touch html/index.html

html/index.html

<html>
  <h1>sample page</h1>
</html>

dockerイメージ作成

次に以下のようにDockerFileとdocker-compose.ymlを作成。

touch Dockerfile

Dockerfile

FROM 'nginx:latest'

COPY html/index.html /usr/share/nginx/html/index.html

RUN service nginx start
touch docker-compose.yaml

docker-compose.yaml

version: '3'
 
services:
  nginx:
    build: ./
    image: nginx-sample
    ports:
      - 80:80

dockerイメージの動作確認

ここで一旦、dockerイメージの作成と動作確認を行う。

docker-compose up -d

起動したら、ブラウザでlocalhostにアクセスする。 「sample page」と表示されれば、成功。

イメージにタグ付けする

最後に作成したイメージにタグ付けする。 この段階ですでにイメージそのものは作成されているけど、タグはlatestとなっていると思う。 なぜかminikubeではlatestタグのイメージは扱えなさそうなのでlatest以外のタグを付ける(latestが使えない理由はわからなかった・・・) ここでv1タグを付ける。

docker tag <docker image id> nginx-sample:v1

minikubeに作成したdockerイメージを起動できることを確認

起動

まずはminikubeそのものを起動

minikube start

イメージを取り込む

ここでminikubeにローカルのイメージを取り込み。

minikube image load nginx-sample:v1

minikubeで扱うことができるイメージの一覧は以下のコマンドで確認できた。

minikube image ls

取り込んだイメージを使う

以下のコマンドで取り込んだイメージのdeploymentの作成と公開を行う。

kubectl create deployment test --image=nginx-sample:v1
kubectl expose deployment test --type=NodePort --port=80
minikube service test --url

画面に表示されたURLにブラウザでアクセスし、sample pageが出たらアクセスOK。 自分の環境ではこれで動作した。

後片付け

動作確認が済んだので以下のような後片付けをして完了。

minikube stop

kubectl delete svc test
kubectl delete deploy test
minikube image rm nginx-sample:v1

docker-compose down
docker rmi nginx-sample:latest
docker rmi nginx-sample:v1

2020年振り返り

立ち位置

フリーでプログラマとしてプロジェクトに参加。今年は3月まではA社。4月からはB社のプロジェクトに参画中。

利用した技術

Go / Gin Frmaework

A社はGoでバックエンドを書いてた。それからGoの仕事が舞い込みそうな話しがきたので個人的にサンプル的なコードを書いてカンみたいなものを維持していたが、結局タイミング的なものもあって仕事には繋がらなかった。残念。

ES6 / Vue.js + vuex

A社のフロントエンドがvue.js + es6だったので書いた。日本でvue.jsが流行ったのはとっかかりやすさが整備されていたからなんだろうな、と改めて感じた。

Ruby / Rails

B社の仕事がRailsAPIサーバ + ReactのSPAというよくある構成。バグ調査、機能追加の為に触った。自分の主戦場はこちらだけど、今年は仕事の都合上、フロント寄りの実装の方を多く触った。

TypeScript / React + redux B社の仕事のフロントエンドがこれだった。Reactは以前にも書いてはいたけど、ほぼ忘れていたので個人的にキャッチアップをしていった。途中フロントの全面書き直しが始まり、いきなりTypeScriptになった。まだまだ手に馴染んでないけど、型定義が柔軟すぎて面白い反面、型職人がいなくなったら混乱する現場とかあるんじゃなかろうか、とも思った。たぶん、ライブラリ以外では柔軟すぎる型定義は使わない方が良いんじゃないかと思う。今年一番触っているのはこの辺り。フロントはたぶんTypeScriptが標準言語になる雰囲気を感じた。

Python

B社のサービスのある部分が画像解析を行っていて、画像解析部分がPythonで記述されていた。運用的なことをやっていたので書かなかったけど読んだ。

Docker / docker-compose

今年は使っただけ。開発環境の構築ではほぼ必須になったと思う。来年はBuildpackを触ってみることにする。

AWS lambda

B社のサービスでPDFを出力する機能があって元々使われていた。ただPDFの容量がデカくなるとレスポンス上限に引っかかってエラーになっていたので、これを全面的に直した。PDF化にはpuppeterを使った。AWS lambdaでpuppeterを使うには工夫が必要だが、この記事のおかげでなんとかなった。 https://dev.classmethod.jp/articles/run-headless-chrome-puppeteer-on-aws-lambda/

Google Cloud Platform

なぜか今年は関わった2社共にGCPメインだった。GCPは初めてだったが、特殊な使い方をしているわけではなかった為にすぐに慣れた。一時はAWSオンリーだったけど、GCPが盛り返してきているのを感じた。

Kubernetes

B社の基盤。ほんのちょっとyamlを記述した程度で本格的に触ることはなかった。だいぶ世の中に定着した印象。来年は個人的にも学習してみることにする。ここ数年の盛り上がりを受けて良い感じの教材が増えているので学習にはなにも困らないだろう。

GraphQL

個人的に学習していた。ブログにまとめたりしたかったが、うまくまとめられなかった。残念。ServerをGo、Clientをnuxt.js + apolloで実装してみて、なんとなくのイメージは掴めたのは良かった。

gRPC

これも個人的に学習していた。Hello Worldレベルのことしか出来ていないけど、型定義書いて、Goでクライアントとサーバを建ててちょっと動かしてみた。これもイメージは掴めた。

WebRTC

仕事では微妙にさわったが、未だにピンときてない。けども数年したら当たり前に使われるようになる気がした。来年あたりちゃんと触ってみようと思う。

ワークスタイル

A社までは普通に出勤してた。 B社からいきなりフルリモートになった。もちろん、コロナの影響。 以前にもひと月ほどリモートで働いたことはあったが、今回はずっとリモート。現在の継続中。未だに誰ともリアルで対面したことがない。 おかげで生活がガラッと変わった。

通勤がなくなったり、時間の拘束がだいぶなくなったことは本当に良かったけれども、成果を出す為に働き過ぎたり、通勤がなくなったおかげか、体調を崩すことが増えてしまった。

特に11月以降は体調面に加えて、謎の無気力感に襲われて仕事にも微妙に影響が出てしまった。原因は結局のところ運動不足に起因するものだったようで、現状は午後3時になったら30分程度、散歩+縄跳びをする、軽い筋トレ+ストレッチですることで改善した。 健康、本当に大事。

あとはA社でもB社でも最年長だけど、技術的には一番下っ端な気がするので、なんとかがんばるしかないと思った次第。

まとめ

来年もなんとか生き残れるようにやれることをやるしかない。 あと本当に健康大事。

echoでリクエストパラメータをチェックするには

echoでリクエストパラメータを取得し、構造体に割り当てる方法はわかったのですが、ここまで出来ると今度はvalidateの方法が知りたくなり、調べてみました。
echoにはvalidateそのものは用意されていないのですが、インターフェイスだけが用意されていて、このインターフェイスに合わせて実装していくようです。 公式にはサンプルとして、有名なバリデーションライブラリであるところのvalidatorを使ったサンプルがあったので、これを打ち込んで検証してみました。

https://github.com/go-playground/validator

プログラム

Validateというメソッドを持った関数をechoに登録し、必要なところでValidate(構造体ポインタ)を渡す感じで呼び出すのが基本のようです。
今回であれば、CustomValidatorという構造体を登録しています。
validatorは構造体のアノテーションにvalidateしたい内容を記述していくスタイルです。今回は必須項目チェックを行うので、requiredをアノテーション内で指定しています。

package main

import (
    "net/http"

    "github.com/labstack/echo"
    "gopkg.in/go-playground/validator.v9"
)

type User struct {
    Name string `json:"name" validate:"required"`
}

type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

type Error struct {
    Error string `json:"error"`
}

func main() {
    e := echo.New()
  // validatorを登録
    e.Validator = &CustomValidator{validator: validator.New()}
    e.POST("/", test)
    e.Logger.Fatal(e.Start(":1111"))
}

func test(c echo.Context) error {
    u := new(User)
  // ここでリクエストパラメータを構造体に
    if err := c.Bind(u); err != nil {
        return c.JSON(http.StatusBadRequest, err)
    }
  // Validateを実施
    if err := c.Validate(u); err != nil {
        return c.JSON(http.StatusBadRequest, &Error{Error: err.Error()})
    }
    return c.JSON(http.StatusOK, u)
}

実行

JSON形式でNameに対して空文字を送信するとエラーとなります。

curl -X POST http://localhost:1111 -H 'Content-Type: application/json' -d '{"Name":""}'
{"error":"Key: 'User.Name' Error:Field validation for 'Name' failed on the 'required' tag"}

curl -X POST http://localhost:1111 -H 'Content-Type: application/json' -d '{"Name":"test"}'
{"name":"test"}

Golangで構造体の配列を持つ構造体を初期化

微妙にわからなかったのでメモ代わりに。
Golangで以下のような構造があった時、初期化する方法がわからなかった。
構造体配列を持つ、構造体の時の初期化方法がわかってなかった。

type Store struct {
    Items []Item `validate:"dive"`
}

type Item struct {
    Name  string `validate:"required"` // 名前は必須
    Price int    `validate:"lte=100"`  // 値段は100以下
}

以下のようにすればよかった。

func main() {
    obj := Store{
        Items: []Item{
            {Name: "item1", Price: 99},
            {Name: "", Price: 100},
            {Name: "item3", Price: 101},
        },
    }
}