miyazi888の覚え書き日記

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

GoのAPIサーバーでmiddlewareを適用する時にaliceを使って楽をする

GoでAPIサーバを作っている時にmiddlewareを適用することはよくあると思うのですが、数が多いと下のようにゴテゴテした感じになると思います。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/handler1", handler1)
    handler := middle1(middle2(mux))
    log.Fatal(http.ListenAndServe(":8080", handler))
}

この例ではmiddlewareが2つだけなんでこれで済んでますが、増えてくるとゴテゴテ度は増大していくと思います。

この部分、なんかいい方法ないか、探していたらやっぱりありました。

github.com

早速、試してみました。

インストール

go get github.com/justinas/alice

aliceを使ったサンプル

alice.New(の部分がそうでこういう風に手軽にmiddlewareを適用できるようになる。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/handler1", handler1)
    handler := alice.New(middle1, middle2).Then(mux)
    log.Fatal(http.ListenAndServe(":8080", handler))
}

func middle1(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("middle1 start")
        next.ServeHTTP(w, r)
        fmt.Println("middle1 end")
    })
}

func middle2(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("middle2 start")
        next.ServeHTTP(w, r)
        fmt.Println("middle2 end")
    })
}

実行結果

上記のサーバを起動して、別ターミナルから以下のようにリクエストを飛ばす。

 curl http://localhost:8080/handler1

サーバ側のログに以下のように表示され、無事middlewareが適用されていることが確認できる。 便利。

middle1 start
middle2 start
middle2 end
middle1 end

GoのAPIサーバのレスポンスをgzip圧縮する

今、仕事で作っているGoで作られたAPIサーバは比較的、レスポンスが巨大なものが多く、gzip圧縮した方が良さそうに感じた。

APIサーバはGCP上のAPI gatewayが前段にあり、その後ろでCloud Runで実際の処理が動いている構成。 最初はAPI gatewayとかにgzip圧縮する機能とかありそうとか思ってたが、そういう機能はないみたいなのでAPIサーバ側で実装してみた。

使ったライブラリ

下のリンク先のものを使った。 基本的には1024バイトを超えるレスポンスでないとgzip圧縮しないようになっていたので、小さい大きさのレスポンスの時の圧縮・解凍による性能劣化みたいなことは少ないと思う。

github.com

インストール

go get github.com/klauspost/compress

組み込み例

package main

import (
    "log"
    "net/http"

    "github.com/klauspost/compress/gzhttp"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte("{\"test\": \"--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\"}"))
    })

    handler := gzhttp.GzipHandler(handler)

    log.Fatal(http.ListenAndServe(":8080", handler))
}

動作確認

curlで以下のようにして確認。 --compressedオプションを付けることでAccept-Encoding: gzipみたいな感じなる模様。

リクエス

curl -v --compressed http://localhost:8080/

レスポンス

レスポンスヘッダーにContent-Encoding: gzipが意図どおりに付いているので、gzip圧縮されていることがわかる。

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.86.0
> Accept: */*
> Accept-Encoding: deflate, gzip, br, zstd
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Encoding: gzip
< Content-Type: application/json
< Vary: Accept-Encoding
< Date: Sun, 10 Dec 2023 11:15:36 GMT
< Content-Length: 48
<
{"test": "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------* Connection #0 to host localhost left intact
------------"}%

Cloud Run jobsを試してみた

タイトルどおり、Cloud Run jobsを使ったことがなかったので試してみた。

手順

検証用のアプリ作成

main.go

package main

import "fmt"

func main() {
    fmt.Println("start process")
    fmt.Println("something process")
    fmt.Println("finish process")
}

Dockerfile

FROM golang:1.21-alpine AS builder
WORKDIR /usr/local/src/
ADD go.mod ./
RUN go mod download
ADD . .
RUN go build -o /go/bin/app ./main.go

FROM alpine:latest
COPY --from=builder /go/bin/app /
CMD ["/app"]

動作確認

下のように3つの文字列が表示されたら、OK

# 動作確認
> docker build . -t test-app
> docker run --rm test-app

start process
something process 
finish process

イメージのpush

artifact registoryにレポジトリを作成し、イメージをpushする

> PROJECT=$(gcloud config get-valud project)
> REGION=$(gcloud config get-value compute/region)


# dockerとの連携(docker pushするために必要)
> gcloud auth configure-docker asia-northeast1-docker.pkg.dev

# レポジトリの作成
> gcloud artifacts repositories create docker-repo --repository-format=docker --location="${REGION}" --description="Docker repository"

# レポジトリのリスト確認
> gcloud artifacts repositories list

# tag付けしてpush
>  REPO="asia-northeast1-docker.pkg.dev/${PROJECT}/docker-repo"
> docker tag test-app "${REPO}/test-app"
> docker push "${REPO}/test-app"

ジョブを作成し、実行

> gcloud run jobs create sample-job --image "${REPO}/test-app:latest" --region "${REGION}"
> gcloud run jobs execute sample-job --region "${REGION}"

# 実行結果の確認
> gcloud beta run jobs logs read sample-job --region "${REGION}"

2023-10-15 04:09:19 start process
2023-10-15 04:09:19 something process
2023-10-15 04:09:19 finish process

とりあえず、jobを動作させることはできた。 実際にはCloud Schedulerでの定期実行とは何かのPub/Subあたりでのイベント駆動をさせるのが使い方になるんだとは思う。

後片付け

gcloud run jobs delete sample-job --region "${REGION}"
gcloud artifacts repositories delete docker-repo --location "${REGION}"

参考

cloud.google.com

zenn.dev

GCP Cloud RunでDockerfileを作らなくてもデプロイして動作が可能か検証

Cloud Runで何かのアプリをデプロイする時にはコンテナイメージを作ったり、githubと連携したりする必要がある、と誤解していましたが、デプロイコマンドの--sourceオプションを使用することでローカルのソースからデプロイまで可能という記事を読んで、自分でも試してみました。

手順

検証用のアプリ作成

アクセスするとJSONを返却するだけの検証用APIサーバを作成します。

main.go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
)

type Hello struct {
    Title string
    Desc  string
    Ver   string
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(Hello{Title: "Hello", Desc: "BuildPack", Ver: "0.0.1"})
    })
    port := os.Getenv("PORT")

    log.Printf("Listening on %v", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

動作確認

まずはローカルで動作確認します。 ターミナルを開いて上記のコマンドでAPIサーバを起動。

export PORT=8080
go run main.go

別のターミナルを開き、APIサーバにリクエストを投げてみます。

> curl http://localhost:8080/
{"Title":"Hello","Desc":"BuildPack","Ver":"0.0.1"}

意図したレスポンスが返却されてきました。

Cloud Runへのデプロイ

以下のコマンドでcloudrunへデプロイする。 ポイントは--sourceオプション。

PORT番号については自動的にPORT環境変数が設定されており、デフォルトで8080が設定されている為、ここでは特に指定しない。

gcloud run deploy sample-app --region asia-northeast1 --platform managed --allow-unauthenticated --source .

しばらくするとデプロイ完了と共にURLが表示されると思うのでそこにリクエストを投げます。

Service URL: https://sample-app-aoytzwylua-an.a.run.app
> curl https://sample-app-aoytzwylua-an.a.run.app                                                                                                                        main
{"Title":"Hello","Desc":"BuildPack","Ver":"0.0.1"}

無事にデプロイできていました。

感想

裏側ではbuildpackによる、コンテナイメージが生成され、それがCloud Runにデプロイされているようでした。 便利。

entでgroup byで集約した結果のcountでorder byする方法

タイトルどおり。

select count(*) as cnt from items group by type order by cnt desc

みたいにgroup byして指定した項目毎に件数で並び替えたい、というケースがある場合、entではどうやったら記述できるのか調べた。

本当にこれが正解なのかはわからなかったけど、一応上のようなクエリをentで発行することはできた。

var results []struct {
  Count  int
  Type   string
}
err = r.ent().Item.Query().Where(apply).Order(
        func(s *sql.Selector) {
            s.OrderExpr(sql.ExprP("Count(*) Desc"))
        },
    ).GroupBy(item.FieldType).Aggregate(ent.Count()).Scan(ctx, &results)

でも、もっといいやり方がありそうで、少しモヤル。

追記: 特に試してはいないけど、entでコード生成した時にすでに上記のようなものが生成されている模様。 だから、自前で上記のようなコードは記述する必要はなさそう。

参考

entgo.io

yarnでバージョンを切り替えられない現象への対応

yarnコマンドについては自前のPCでは特にv3系のしていて特に不便は感じてなくてそのままだった。 ただ、仕事関連のフロントエンドがv1系だったので、仕事の時だけ切り替える必要が出てきた。

最近のyarnは自前でバージョンを切り替え可能なようで、早速切り替えてみても切り替わらなかった。 下のような状況。

yarn set version 1.22.19
yarn -v 
3.4.1

なぜだー、と思いながら、調べていた結果、$HOMEの.yarnrcと.yarnrc.ymlが原因っぽいことがわかった。

stackoverflow.com

思い切って$HOMEの.yarnrcと.yarnrc.ymlを削除してみたら、やっと意図どおり動作した。 1時間ぐらいこれだけで時間が溶けてしまった・・・

yarn set version 1.22.19
yarn -v
1.22.19

viteで開発サーバを起動しても何も表示されなくなる現象への対応

仕事のフロントエンド開発ではviteを使用しています。

ただ最近viteコマンドで開発サーバを起動しても何も表示されなくなる(画面は真っ白)、という事象に遭遇しました。 chromeのコンソールを確認すると以下のような文字列だけが表示されていました。

Failed to load resource: the server responded with a status of 504 (Outdated Optimize Dep)

対応1

とりあえず、再インストールしたらどうにかなるかも、と思い、以下のような手順で再インストールし、viteを起動しました。

rm -fr node_modules
yarn install
yarn dev (viteコマンドを叩いているだけ)

これで画面が表示され、めでたしめでたし・・・とはなりませんでした。 一度、開発サーバを止めるとまた、この手順で起動しないと何も表示されない、という状態です。 これではツライ、ということでさらに調査。

対応2

結論としてはvite --forceでとりあえずは解決しました。

vite --force

根本的な原因についてはよくわからず、というのが正直なところ。

--forceオプションは強制的に再バンドルさせる指定のようです。 ただ、このオプションを指定するとviteの良さをなくすことになりそう・・・

ja.vitejs.dev

参考

stackoverflow.com

github.com