巨人の足元でたじlog

そうして言葉を軽んじるから――― 君は私の言葉を聞き逃す

Go言語初心者がハマるGo言語の細かい文法などの気付き

随時更新

goではintの計算結果は、intになる。

なので、

<int>10 / <int>100

の計算結果は、0になる。
切り捨てか切り上げか四捨五入かは、調べてない。
なので、予めfloat32とかにcastしておく必要がある。

qiita.com

ポインタは変数に入れないと使用できないケースがある

ポインタ型を渡したいときに

hoge := "hoge"  // string型の変数hogeに代入できます
fmt.Println(hoge)
p := &hoge // 変数hogeのポインタ(アドレス)を取得できます
fmt.Println(p) // 変数hogeのポインタ(アドレス)を表示できます
fmt.Println(*p)  // 変数hogeのポインタ(アドレス)の中に入っている値を表示できます
p2 := &"hoge" // これで値"hoge"が入っている値をセットしたいところですが、エラーになります。
fmt.Println(p2)

ポインタは、変数のアドレスを取得できるやつなので、一度変数に入れないとだめです。

githubのhttpsのcloneで`repository not found`のエラー

$ git clone https://github.com/my0shym/myproject
repository not found
のエラーが発生

今まではパスワードが聞かれていたはずなのだが、それもなくエラーになっている。

以前パスワードを聞かれて入力したらエラーになったことはあったが、それはパスワードではなくtokenを入力する必要があるように変わったことが原因だった。

しかし、今回はそもそもパスワード(トークン)すら聞かれない。

調べてみると、どうやらmacではbrewでinstallしたgitではkeychainにパスワードが初回に保存されていて、毎回入力しなくて良いようにそこから認証を通そうとしているとのことだった。
これも設定で変更できるのだが。

www.rail-c.com

osxkeychainなるオプションを指定すれば平文でパスワードを保存することもなく、便利に使えそうだった。

$git config --global credential.helper osxkeychain

を実行しても何も変化がなく(ここでパスワード(token)を設定するのかと思った)、依然として動かない。

色々試行錯誤してみたところ、解決。

結論としては、自分のmacに一番最初に設定したのが別のgithubアカウントのtokenだったことが原因でした。

複数アカウントsshとかで使っちゃうと運用が面倒くさくなると思ったので、そっちのアカウントはsshじゃなくhttpsで、本アカウントをsshとして、サブアカウントで毎回httpsで操作しようとするときはtoken入力すればいいやくらいに思っていて特に棲み分けは考えなくてもいいやと思っていたところ、実は裏側でtokenが勝手に設定されていてそれを使いまわそうとされていたのでした〜

zenn.dev

このやり方でパスワードを再設定したら、いけました!

しかし、となるとサブ垢で使っているgithubアカウントでまたhttps操作をしようとすると同じ問題が発生するので、これをどう対処するか。。という問題は残っていますね。

それはまた直面したときに。

たぶんそもそもhttpsを使わずに、configで分けておくのが良いのだと思っているのだがー。メモ。
それを言い出すと、そもそもgithubアカウントの複数運用自体が、無駄な工数という説もある。メモ。

goのginをdocker-composeでローカル環境で動かす

以下のような構成で作成しました。

 

.
├── backend
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── docker-compose.yml

main.go作成後、以下を実行します。

$ go mod init example.com/go-project
$ go mod tidy

 

ファイルの内容はこの通り

Dockerfile

FROM golang:1.19

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

RUN go build -o /main

CMD [ "/main" ]

main.go

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func ping(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, gin.H{"message": "pong"})
}

func main() {
    router := gin.Default()
    router.GET("/ping", ping)

    router.Run(":8080")
}

docker-compose.yml

version: '3'
services:
  api:
    platform: linux/x86_64
    build: ./backend
    ports:
      - "8080:8080"

 

 

ハマったところとしては、ginのチュートリアル

router.Run("localhost:8080")

としてサーバーを動かしていたのでそのままやっていたら、コンテナ内で

curl localhost:8080/ping

をやっている分にはレスポンスが返ってきていたのですが、ホストからアクセスしようとすると、そもそもの疎通ができていない状態になってしまっていました。

他の記事等を見ているとlocalhostなしでRun(:8080)としているコードが多かったのでやってみたらこれでいけました。

うーんしかし根本の原因はちょっとよくわからなかったです。

コンテナ力がちょっと足りていないなあ。精進します。

 

あと微妙にハマったのは、ローカルmacでgo mod tidyでgo.sumを生成していたのですが、ローカルで使っていたgoのバージョンとDockerfileで使っていたgoのイメージのバージョンが違っていたがために、go.sumが整合性取れなくなっていて動かなかった問題もありました。

本当はalpine使ったほうが良かったのかもしれないですが、動いているので大目に見てもらいます。

ともあれ、これで動きました!

ここから開発にブーストかけていきたいと思います!

 

Go言語でCloud SQLとCloud Runを連携する奮闘記

接続しているっぽいところまでは割と簡単に進めましたが、本当に接続している?っていう確認に少し手間がかかりそうな気がしました。

 



 


動作確認の手順として

 


1. goでDBに接続してレコードを取ってきて表示するようなコードを書く

2. 1をイメージとしてArtifact Registryにpush

3. Cloud SQLインスタンスを作成する、データを作っておく

4. Artifact RegistryからCloud Runサービスをデプロイ(Cloud SQLに接続する設定)

5. Cloud RunがDBからレコードを取得できていることをブラウザから確認

 


という手順で進めていきたいです。

 

 

cloud.google.com

 

cloud sql自体の作成やデータアクセスはこちらから確認できますが、他のリソースからアクセスをさせたいです。

 

qiita.com

 

ローカルからアクセスする場合には、プロキシなるものを用意しないといけないっぽい。

でもGCP内でやり取りするなら、たぶんそんなのはいらない気がする。

 

 

github.com

 

これやったら動きそうな気がしてるけど、goのコードが長い。。

もう少しシンプルに接続だけを確認したい。

これは甘えか?

 

 

cloud.google.com

 

普通にこれでいいのでは?

接続方法は書いてありそうだけど、goのアプリケーションで、どうやって使うのかがわからないな。

これはgoの知識不足か。

普通にレコードselectができればいいんですが。

アプリケーションでの使用も書いてあるが、ちと情報量多いな。

github.com

 

シンプルに、コネクションを作った後の動きは別でgoのsqlパッケージの使い方として調べます。

www.wakuwakubank.com

 

これを実装してみるか。

ローカルのmysqlと接続してみることにする。

いや、これの例の通りmysqlもdockerで立ち上げることにする。

 

version: '3'

services:
  db-go-database-sql:
    image: mysql:5.7
    container_name: db-go-database-sql
    ports:
      - "13306:3306"
    volumes:
      - ./data/db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: test_db
      MYSQL_USER: test_user
      MYSQL_PASSWORD: test_password

とすると、エラーになりました。

 

no matching manifest for linux/arm64/v8 in the manifest list entries

 

stackoverflow.com

 

本質的な解決策じゃないようだが、とりあえず問題は解決するとのこと。

services:
  db:
    platform: linux/x86_64
    image: mysql:5.7
    ...

のようにplatform: linux/x86_64を追加してdocker compose upすると、うまく立ち上がった。

mysql -utest_user -h 127.0.0.1 --port 13306 -p

で接続後、以下実行

USE test_db;

CREATE TABLE users
(
    id         INTEGER NOT NULL AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name  VARCHAR(50),
    age        INTEGER,
    created    DATETIME DEFAULT CURRENT_TIMESTAMP,
    modified   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE posts
(
    id       INTEGER NOT NULL AUTO_INCREMENT,
    user_id  INTEGER NOT NULL,
    content  TEXT    NOT NULL,
    created  DATETIME DEFAULT CURRENT_TIMESTAMP,
    modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO users (first_name, last_name, age) 
VALUES 
    ("rina", "mikami", 43),
    ("jun", "kusano", 34),
    ("hideki", "yamada", 23);

 

最終的には以下のdocker-compose.yml

version: '3'

services:
  db-go-database-sql:
    platform: linux/x86_64
    image: mysql:5.7
    container_name: db-go-database-sql
    ports:
      - "13306:3306"
    volumes:
      - ./data/db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: test_db
      MYSQL_USER: test_user
      MYSQL_PASSWORD: test_password

これに対して、接続するようなgoのコードを作る。

main.go

package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

type User struct {
    ID        int
    FirstName string
    LastName  string
    Age       string
    Created   time.Time
    Updated   time.Time
}

func main() {
    db, err := sql.Open("mysql", "test_user:test_password@tcp(127.0.0.1:13306)/test_db?parseTime=true&loc=Asia%2FTokyo")
    if err != nil {
        log.Fatalf("main sql.Open error err:%v", err)
    }
    defer db.Close()

    fmt.Println("------------------")
    getRows(db)
    fmt.Println("------------------")
    getSingleRow(db, 1)
    fmt.Println("------------------")
    getSingleRow(db, 4) // 存在しないUserID
}

func getRows(db *sql.DB) {
    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        log.Fatalf("getRows db.Query error err:%v", err)
    }
    defer rows.Close()

    for rows.Next() {
        u := &User{}
        if err := rows.Scan(&u.ID, &u.FirstName, &u.LastName, &u.Age, &u.Created, &u.Updated); err != nil {
            log.Fatalf("getRows rows.Scan error err:%v", err)
        }
        fmt.Println(u)
    }

    err = rows.Err()
    if err != nil {
        log.Fatalf("getRows rows.Err error err:%v", err)
    }
}

func getSingleRow(db *sql.DB, userID int) {
    u := &User{}
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", userID).
        Scan(&u.ID, &u.FirstName, &u.LastName, &u.Age, &u.Created, &u.Updated)
    if errors.Is(err, sql.ErrNoRows) {
        fmt.Println("getSingleRow no records.")
        return
    }
    if err != nil {
        log.Fatalf("getSingleRow db.QueryRow error err:%v", err)
    }
    fmt.Println(u)
}

goの外部パッケージ追加方法は、まずmain.goと同じディレクトリ内で

go mod init

で始めてから、

main.goにコードを書いた状態で、

go mod tidy

をする。

すると、その中でimportしているがmodに追加されていないものを自動でimportしてくれる。

これで疎通確認できました!

これでとりあえず、mysqlからデータを取得するgoのコードが確認できました。

中身も見てみて、なるほどねって感じです。Goライクな書き方にだんだんと慣れてきている気がします。

 

これをそのままCloud Runにデプロイしようと思います。

Dockerfileは公式より、

docs.docker.com

これをベースにして、

# syntax=docker/dockerfile:1

FROM golang:1.16-alpine

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

RUN go build -o /docker-gs-ping

EXPOSE 8080

CMD [ "/docker-gs-ping" ]

 

 

まずは疎通確認のため、goのコードもこのサンプルのまま使います。

main.go

package main

import (
    "net/http"
    "os"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {

    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/", func(c echo.Context) error {
        return c.HTML(http.StatusOK, "Hello, Docker! <3")
    })

    e.GET("/ping", func(c echo.Context) error {
        return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
    })

    httpPort := os.Getenv("HTTP_PORT")
    if httpPort == "" {
        httpPort = "8080"
    }

    e.Logger.Fatal(e.Start(":" + httpPort))
}

 

タグを付けてイメージをbuildする

docker build . -t asia-northeast1-docker.pkg.dev/MY_PROJECT/cloud-run-source-deploy/go-test:latest

 

Artifact Registryにpushする

docker push asia-northeast1-docker.pkg.dev/MY_PROJECT/cloud-run-source-deploy/go-test:latest

 

Artifact Registryにimageがpushされているのを確認したら、コンソールからCloud Runサービスをデプロイ

 

おっと、ここでエラー。

The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable. Logs for this revision might contain more information. Logs URL:

これは以前解決したエラーでした。

my0shym.hatenablog.com

 

ここで解決していますが、buildコマンドを以下に変更です。

docker build . -t go-test --platform linux/amd64

再度pushして、Artifact RegistryからCloud Runにデプロイします。

公開されているURLからアクセスすると、

Hello, Docker! <3

の文字が表示されていました。

これでDockerfileが正しく動くことが確認できました。

これを次は、先程のDB接続用のmain.goに変更してpushします。

本当はDB接続情報は環境変数にセットして使用したいところですが、一旦決め打ちで作って動作を確認してから環境変数化していきたいと思います。

 

と思ったけど、このままだと、サーバーとしての機能は持たせていないからhttpリクエスト送ってもだめだ。

まずサーバーとしての機能は維持しつつ、handefuncでmysqlからデータを取得するように組み合わせる必要があるな。

Dockerfileのサンプルコードを使ってもいいんだけど、echoのモジュールを使ってリクエストの処理をしていたので、これは使わないで、シンプルにnet/httpの標準モジュールを使っていく。

以下より、まずはシンプルなwebサーバーとして動かす。

すごい回り道してきたきがするなぁ。

qiita.com

 

package main

import (
    "fmt"
    "log"
    "net/http"
)

func hello_world(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    http.HandleFunc("/", hello_world)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

 

これをpushしてArtifactからCloud Runのサービスデプロイ。

→ URLアクセスでHello, Worldが表示された!

 

これのHandleFuncにmysqlからデータを取ってくる関数を追加しよう。

 

package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log"
    "net/http"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

type User struct {
    ID        int
    FirstName string
    LastName  string
    Age       string
    Created   time.Time
    Updated   time.Time
}

func getSingleRow(db *sql.DB, userID int) string {
    u := &User{}
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", userID).
        Scan(&u.ID, &u.FirstName, &u.LastName, &u.Age, &u.Created, &u.Updated)
    if errors.Is(err, sql.ErrNoRows) {
        fmt.Println("getSingleRow no records.")
        return u.FirstName
    }
    if err != nil {
        log.Fatalf("getSingleRow db.QueryRow error err:%v", err)
    }
    // fmt.Println(u)
    return u.FirstName
}

func hello_world(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func sql_man(w http.ResponseWriter, r *http.Request) {
    db, err := sql.Open("mysql", "test_user:test_password@tcp(127.0.0.1:13306)/test_db?parseTime=true&loc=Asia%2FTokyo")
    if err != nil {
        log.Fatalf("main sql.Open error err:%v", err)
    }
    defer db.Close()

    u := getSingleRow(db, 1)
    fmt.Fprintf(w, u)
}

func main() {

    http.HandleFunc("/", hello_world)
    http.HandleFunc("/sql-man", sql_man)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

 

これでデプロイすると、

/ ではHello, worldが表示されるが、

/sql-man ではService Unavailable と表示される。

これはcloud sqlをまだ設定していないので正しい。

ここからcloud sqlの設定を入れていく。

と、公式のつなぎ込み方を再度確認すると、自分のコードではtcpで疎通している部分がunixソケットで疎通している。

cloud.google.com

unixソケットについて整理します。

 

qiita.com

 

UNIX ドメインソケットもソケットもいずれもプロセス間のデータのやり取りを行うための手法の一つ
UNIX ドメインソケットではプロセス間通信にファイルシステムを利用する(拡張子 .sock という場合が多い)その為、 同じホストでのプロセス間通信 として利用される
・ソケット通信は 異なるホスト で TCPUDP を使ってプロセス間のやり取りが可能

 

Unix socket connectionってので通信しているんだが、これはソケット通信なのか、unixドメインソケット通信なのかどっちなんだ?

そこまでは把握する必要はないか?

形式的には

/cloudsql/project:region:instance

みたいな感じだから、異なるホストっていう扱いなのかな?

このあたりは宿題として放置。まず動くことを目標とします。

 

元々Cloud SQLインスタンスは作成済みだったので、それを使用します。

 

以下コードで疎通の確認ができた!

package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log"
    "net/http"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

type User struct {
    ID        int
    FirstName string
    LastName  string
    Age       string
    Created   time.Time
    Updated   time.Time
}

func getSingleRow(db *sql.DB, userID int) string {
    u := &User{}
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", userID).
        Scan(&u.ID, &u.FirstName, &u.LastName, &u.Age, &u.Created, &u.Updated)
    if errors.Is(err, sql.ErrNoRows) {
        fmt.Println("getSingleRow no records.")
        return u.FirstName
    }
    if err != nil {
        log.Fatalf("getSingleRow db.QueryRow error err:%v", err)
    }
    // fmt.Println(u)
    return u.FirstName
}

func hello_world(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}
func connectUnixSocket() (*sql.DB, error) {
    // mustGetenv := func(k string) string {
    //     v := os.Getenv(k)
    //     if v == "" {
    //         log.Fatalf("Warning: %s environment variable not set.", k)
    //     }
    //     return v
    // }
    // Note: Saving credentials in environment variables is convenient, but not
    // secure - consider a more secure solution such as
    // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
    // keep secrets safe.
    var (
        dbUser         = "root"
        dbPwd          = "hogehoge01"
        dbName         = "test_db"
        unixSocketPath = "/cloudsql/MY_PROJECT:us-central1:myinstance"
        // dbUser         = mustGetenv("DB_USER")              // e.g. 'my-db-user'
        // dbPwd          = mustGetenv("DB_PASS")              // e.g. 'my-db-password'
        // dbName         = mustGetenv("DB_NAME")              // e.g. 'my-database'
        // unixSocketPath = mustGetenv("INSTANCE_UNIX_SOCKET") // e.g. '/cloudsql/project:region:instance'
    )

    dbURI := fmt.Sprintf("%s:%s@unix(/%s)/%s?parseTime=true",
        dbUser, dbPwd, unixSocketPath, dbName)

    // dbPool is the pool of database connections.
    dbPool, err := sql.Open("mysql", dbURI)
    if err != nil {
        return nil, fmt.Errorf("sql.Open: %v", err)
    }

    // ...

    return dbPool, nil
}

func sql_man(w http.ResponseWriter, r *http.Request) {
    // db, err := sql.Open("mysql", "test_user:test_password@tcp(127.0.0.1:13306)/test_db?parseTime=true&loc=Asia%2FTokyo")
    db, err := connectUnixSocket()
    if err != nil {
        log.Fatalf("main sql.Open error err:%v", err)
    }
    defer db.Close()

    u := getSingleRow(db, 1)
    fmt.Fprintf(w, u)
}

func main() {

    http.HandleFunc("/", hello_world)
    http.HandleFunc("/sql-man", sql_man)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

 

これを一応環境変数を読み取るように変更する。

ここらへんは問題なく進めるはず。

コメントアウト部分を復活させたものをArtifactにpushしてからCloud Runにデプロイ、その際に環境変数

DB_USER
DB_PASS
DB_NAME
INSTANCE_UNIX_SOCKET

を設定したら、いけました!

 

くぅ〜これにて完結です。

本当は上の4つの値はシークレットマネージャーで管理したほうが良さそうなんだけども、それはまたのお話ということで。

どっかできれいにしてまとめたいです。

 

GoをCloud Runで動かしてみる。コードとDockerfileそれぞれでデプロイ

Goをデプロイしてみます。

 

qiita.com

 

前提条件として、GCPはちょっと知っていないとだめそうですね。

 

GCPは大昔触ったことがありましたが、プロジェクトとかの分け方がよくわかってないため、そのあたりからおさらいでやっていきます。

 

qiita.com

 

これを参考にしてやってみます。

まず、クイックスタートをやってみたいと思います。

その前に、公式のドキュメントもチェックしておきます。

チュートリアル

cloud.google.com

 

チュートリアルは、なんかトピックが多すぎて、だめだ。

 

cloud.google.com

クイックスタートはなんか軽そうだし、上のQiitaの記事でも参考にしていたので、これを確認します。

cloud.google.com

ちょうどGoのやつがありました。これでいいじゃん。

「Cloud Shell」でできるみたいなので、これで進める。

1. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

プロジェクトを作成します。

作成できました。

 

2. Cloud プロジェクトに対して課金が有効になっていることを確認します。詳しくは、プロジェクトで課金が有効になっているかどうかを確認する方法をご覧ください。

大丈夫そうでした。というより、新規アカウントで作成したので、そのときに登録しました。

300$クレジットげっと!

 

3. Google Cloud CLI をインストールして初期化します。

 

3-1. gcloudをmacにinstallしてみます。

Quickstart: Install the Google Cloud CLI  |  Google Cloud CLI Documentation

 

と、これをやろうとしたのですが、brewでありそうなので、そっちでやってみます。

formulae.brew.sh

zenn.dev

brewのバージョンを確認

% brew -v
Homebrew 3.6.3
Homebrew/homebrew-core (git revision 3de6ff81a00; last commit 2022-10-01)
Homebrew/homebrew-cask (git revision 81521f14c8; last commit 2022-10-01)

 

installします。

$ brew install --cask google-cloud-sdk

完了後、以下を.zshrcに追記

source "$(brew --prefix)/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.zsh.inc"

以下実行

source ~/.zshrc

これでパスが通りましたわ。

% gcloud -v
Google Cloud SDK 405.0.0
bq 2.0.78
core 2022.09.30
gcloud-crc32c 1.0.0
gsutil 5.14

初期化も必要みたいなので、やっておきます。

$ gcloud init

ブラウザで認証画面になるので、使いたいアカウントを設定します。

使用するプロジェクトもここで選ぶように言われるので選びます。

 

なんかzennの記事だとできなかったので、普通にbrewの指示通りにやったらできました。やはり公式が正義か。。

 

Cloud Run サービスのデフォルト プロジェクトを設定するには

$ gcloud config set project PROJECT_ID
Updated property [core/project].

(gcloud initで選択済みなのでこれは必要なさそうだったが、まあいい)

 

4. サンプル アプリケーションを作成する

helloworldディレクトリを作成して、

go.modファイルを以下で作成

module github.com/GoogleCloudPlatform/golang-samples/run/helloworld

go 1.13

 

以下でも良いようだが、goのバージョンが上がっていそうなので、素直に上記ファイルを作成した。

 

$ go mod init

 

main.goを以下で作成

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    log.Print("starting server...")
    http.HandleFunc("/", handler)

    // Determin port for HTTP service.
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("defaulting to port %s", port)
    }

    // Start HTTP server.
    log.Printf("listening on port %s", port)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatal(err)
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    name := os.Getenv("NAME")
    if name == "" {
        name = "World"
    }
    fmt.Fprintf(w, "Hello %s!\n", name)
}

 

5. ソースから Cloud Run にデプロイする

main.goのあるディレクトリで以下コマンド

$ gcloud run deploy

諸々聞かれるオプションを適当に設定していく。

 

This command is equivalent to running `gcloud builds submit --pack image=[IMAGE] y` and `gcloud run deploy y --image [IMAGE]`

 

と、エラーになった。

ERROR: (gcloud.run.deploy) INVALID_ARGUMENT: could not resolve source: googleapi: Error 403: 385636020183@cloudbuild.gserviceaccount.com does not have storage.objects.get access to the Google Cloud Storage object., forbiddenさ

アカウント作ってから何もいじってないからadminユーザーとして実行しているような気がするんだがー

 

もう一回実行してみたら、行けました。

たぶん途中で待ち時間の間にオプションに変な文字が入ってしまっていて正しく入力できていなかったのかもしれません。

 

Service URL: https://helloworld-xxxxxxxxxxxx-x.a.run.app

との表示があったのでアクセスすると、Hello, Worldの文字が!

成功!?

デプロイ簡単すぎわろたぁ〜

クイックスタートはここで終わりです。

すぐに終わって物足りない感あったので、もう少し見てみます。

 

<サービスを開発する>

cloud.google.com

軽く読みました。

 

 

<全般的な開発のヒント>

cloud.google.com

 

以下自分が気になった部分の引用と、まとめです。

・バックグラウンド アクティビティ

・Cloud Run はリクエストを 10 秒以上維持しないため、コンテナの起動に 10 秒以上かかる場合は、最小インスタンス数を 1 以上に設定する必要があります。

・Cloud Run では、リクエスト間でサービスの状態が維持されるとは限りません。ただし、Cloud Run はコンテナ インスタンスを再利用してトラフィックの処理を継続するため、グローバル スコープで変数を宣言することで、その値を以降の呼び出しで再利用できます。個々のリクエストで値が再利用されるかどうかを事前に確認することはできません。

グローバル変数の扱いに注意する

・alpineかscratch等、小さいサイズのベースイメージを使用する。

 

<コンテナをビルドする>

cloud.google.com

 

Dockerfileからデプロイ

大枠はこれで良さそうですが、Dockerfileとか含め、実際に絶対に動くやつでまずは確認したいので、そういった情報を探してみます。

 

qiita.com

 

これを参考にしてやってみる。

新しいプロジェクトを作ってトライします。

`main.go`

package main

import (
    "fmt"
    "net/http"
    "os"
    "strconv"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello world\n")
}

func main() {
    port, _ := strconv.Atoi(os.Args[1])
    fmt.Printf("Starting server at Port %d", port)
    http.HandleFunc("/", handler)
    http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}

以下実行

go run main.go 3000

ブラウザからhttp://localhost:3000 にアクセス。

「Hello, world」が表示されました。良さそうです。

 

`Dockerfile`

FROM golang:latest as builder

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /myprojectsample
COPY . .
RUN go build main.go

# runtime image
FROM alpine
COPY --from=builder /myprojectsample /app

CMD /app/main $PORT

 

docker build -t myprojectsample .

→成功。

 

% docker run -e "PORT=3000" -p 3000:3000 -t farsidesample

http://localhost:3000 にアクセスして成功。hello, worldが表示される。

 

Container RegistryへのPush

Container RegistryよりもArtifact Registryの方が推奨されているみたいなので、使ってみるか?

一旦GCRの方でやってみて、すぐ変更できそうな気もするのであとでやる。

 

cloud.google.com

 

 

Container Registryにまずはpushしてみる。

gcloud auth configure-docker

 

push するイメージを取得する

docker pull gcr.io/google-samples/hello-app:1.0

 

イメージを Container Registry に追加する

docker tag gcr.io/google-samples/hello-app:1.0 gcr.io/PROJECT_ID/quickstart-image:tag1

 

イメージを Container Registry に push する

docker push gcr.io/PROJECT_ID/quickstart-image:tag1

 

GCPコンソールにイメージが確認できました。

Artifact Registryのクイックスタートも見てみます。

 

Artifact Registryのクイックスタート

cloud.google.com

Dockerリポジトリを作成する

gcloud artifacts repositories create quickstart-docker-repo --repository-format=docker \
--location=asia-northeast1 --description="Docker repository"

 

リポジトリが作成されたことを確認

gcloud artifacts repositories list

→OK

 

認証を構成

gcloud auth configure-docker asia-northeast1-docker.pkg.dev

 

pushするイメージを取得

docker pull us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0

 

イメージにレジストリ名をタグ付けする

docker tag us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0 \
asia-northeast1-docker.pkg.dev/PROJECT/quickstart-docker-repo/quickstart-image:tag1

 

イメージをArtifact Registryにpushする

docker push asia-northeast1-docker.pkg.dev/PROJECT/quickstart-docker-repo/quickstart-image:tag1

 

GCPコンソールでイメージがpushされていることが確認できました!

 

 

qiitaの記事に戻る

imageのpushをします。

qiitaの記事ではContainer Registryを使っていましたが、Artifact Registryを使っていきます。

先にArtifact RegistryのGCPコンソールからリポジトリを作っておきます。

 

イメージにタグをつける

docker tag myprojectsample asia-northeast1-docker.pkg.dev/PROJECT_ID/myprojectsample/sample:firstbuild

 

pushする

docker push asia-northeast1-docker.pkg.dev/PROJECT_ID/myprojectsample/sample:firstbuild

→ GCPコンソールから確認できました。

 

サービスの作成

うまくいかない。

イメージからCloud Runにデプロイをやってみると、以下のエラーメッセージが出てデプロイ失敗してしまいました。

The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable. 

 

localでは動いていたのに、、

cloud.google.com

これもやってみました。

gcloud beta code dev

これではちゃんと表示されていたので、なおさらおかしいですね。

 

 

以下のDockerfileとmain.goでやってみてもだめでした。

github.com

 

これを見て解決できるか?

cloud.google.com

 

ポートは問題ないと思うけどなぁ。

注: ARM ベースのマシンでコンテナ イメージを作成した場合、Cloud Run で意図したとおりに動作しないことがあります。この問題を解決するには、Cloud Build を使用してイメージをビルドします。

 

ARMベースじゃなくてApple M1 Proなんだけど、もしかしてこのあたりが問題なのか?

結論、以下で解決しました。

stackoverflow.com

 

以下オプションを付けてbuildしてみる。

--platform linux/amd64

 

これでbuildしたらいけました!

 

Artifact RegistryのイメージからCloud Runにデプロイを選択して、リージョンはasia-northeast1にして、認証は「未認証の呼び出しを許可」にして「作成」

すると、サービスが作成されて、表示されたURLにアクセスすると、Hello World!の文字が!

 

くぅ〜これにて完結です!

 

このあたりは一回どっかできれいにまとめたいところ。

個人開発でのサーバー料金等の運用コストについて調査・検討

さて、そろそろ本格的にwebサービスの開発に着手していきたいと思いますが。

 

まず、全体の構成として

Go(gin) + Next.js

というのは決定しています。

そしてNext.jsをデプロイする先はvercelでほぼ決まりでしょう。

企業で本番環境で使うことはあまりないようなのですが、個人開発にとってはもってこいです。無料かつデプロイが超簡単なので。

herokuのノリですかね。

 

APIサーバー

AWSが親しみ慣れているが、せっかく個人開発なので、使ったことないサービスとか馴染みないサービスを使ってみたいところ。

なので、GCPを使おうと。

CloudRunが良さそう。

cloud-ace.jp

 

コンテナを簡単にいい感じにデプロイしたり、マネージしてくれたりするやつ?

料金も使わなければ安そう。

 

dev.classmethod.jp

 

AWSのApp Runnerみたいなやつとのこと。

App Runnerも初見だった。

見ると2021年5月リリースなので、自分がエンジニア休憩していた時期だった。

友人にはCloud RunはAWSでいうECSみたいな感じって聞いていたが、それよりも簡単そう。VPCとかネットワークとかセキュリティグループとかの設定も皆無らしい。強い。

 

zenn.dev

 

Cloud RunとCloud SQLについて書いてあった。

さらっとCloud SQLのミニマム料金書いてあった。12$くらいと。ちょっと調べてみても出てこなかったので助かる。

 

やはりCloud Runは良さそう。

ベストではなくてもベターではありそう。一回使ってみたいってのはあるので、これを採用する。

 

料金について

リクエストの処理中のみCPUを割り当てるサービスの料金

CPU: 毎月最初の 180,000 vCPU 秒は無料

メモリ: 毎月最初の 360,000 GiB 秒は無料

リクエスト: 毎月 200 万リクエストは無料

 

CPUが常に割り当てられるサービス

CPU: 46.656$/月(無料枠を加味していない)

メモリ: 5.184$/月(無料枠を加味していない)

 

これは後者は高すぎるので、前者でやる。

前者だと無料でできそう。

 

データベース

今回は普通にRDBMSでテーブルの設計をなんとなく考えていたので、となるとCloud Runを使う以上GCPでやるのがやりやすそうで、つまりはCloud SQLが有力です。

ネックになるのは料金。

個人開発者にとってこれは死活問題。

thr3a.hatenablog.com

 

公式を見てみても、料金体系がよくわかりづらい。

これによると、(4年前の情報だけども)ミニマムで月額1500円弱くらい。

 

AWSのRDSでも東京リージョンでミニマムでも18$/月くらいかかるので、1500円だったら飲み込めるか。。

 

あるいは、無料で使える?データベースとしてFirebaseを選択するか。

しかし、今回GoとNext.jsが所見なので、ここにFirebaseも入ってくると流石に勉強期間が長過ぎてだれる恐れがあるので、やめときたい。

Firebaseそのものもそうだし、そもそものNoSQLでのデータのリレーションとかどうするのかとかもよくわかっていないため、かなり学習コストが高い。

それにGoを今後使っていきたいとして、殆どの企業では基本的にはRDBMSを使っている?と思うので、そのあたりにスムーズに入れるように、一旦今回はNo NoSQLでいきます。

 

料金について

vCPU: $39.19 per vCPU・月

メモリ: $6.64 per GB・月

 

ストレージ: HDD ストレージ容量: 1 GB あたり $0.117/月

ネットワーク: 内向き・外向きともにGoogleプロダクトなら大陸内は無料

 

インスタンス: 一番安いやつで$9.96/月

 

CPUとメモリ料金に加えて、インスタンス料金もかかるってこと?

インスタンスにはCPUとメモリは含まれていないの?

sharedの場合は、インスタンス料金だけでいけるような雰囲気がある。

 

 

これによると、1,646円。まあ許容できる。

 

あとはドメイン代くらいか。

全体で月2000円くらいで運用できそうかな。

 

がんばります。

Go言語のデータベース連携ライブラリの選定

ORMはsql-boiler
migrationはgoose

 

をGoをよく使っている友人に進められたので、調べてみます。

 

SQLBoilerについて

zenn.dev

 

sql-boilerの他にORMライブラリは

・ GORM

・sqlx

などがある。

sql-boilerはマイグレーションの機能は入っていないが、それは他のマイグレーションライブラリを組み合わせれば良いので問題にはならないと。

コンパイル時にエラーが出るので、誤りにすぐ気がつくと。

 

公式ドキュメントを見てみます。

 

github.com

SQLBoiler は、データベーススキーマに合わせた Go ORM を生成するツールです。

(gorm/gorp のような) "コードファースト" とは対照的に、"データベースファースト" な ORM です。つまり、最初にデータベーススキーマを作成する必要があります。データベースのライフサイクルのこの部分を管理するために、sql-migrateやその他の移行ツールのようなものを使ってください。

 

gooseについて

github.com

 

・設定ファイルなし

 

・デフォルトのgooseバイナリはSQLファイルのみマイグレート可能

 

・Goのマイグレーション
Go migrations: Go migrations 関数を goose バイナリからオンザフライでビルドすることはありません。
代わりに、独自のカスタム goose バイナリを作成し、Go マイグレーション関数を明示的に登録し、独自の *sql.DB 接続で複雑なマイグレーションを実行することができます。
Goマイグレーション関数では、*sql.Tx引数を使用すれば、SQLトランザクション内でコードを実行することができます。


・gooseのpkgはバイナリから切り離されています。
goose pkgはもうSQLドライバを登録しないので、コードベース内でのドライバのpanic()衝突がありません!
goose pkg は、もうベンダーへの依存を一切持ちません。

 

・デフォルトでタイムスタンプ付きのマイグレーションを使用しますが、開発工程ではタイムスタンプを使用し、実運用工程ではシーケンシャルバージョンを使用するハイブリッドアプローチを推奨します。

 

・allow-missing フラグを指定することで、missing (out-of-order) migrations をサポートします。ライブラリとして使用する場合は、関数オプション goose.WithAllowMissing() を Up, UpTo または UpByOne に指定してください。

 

スキーマテーブルで追跡することなく、アドホックマイグレーションを適用することをサポートします。移行が適用された後にデータベースをシードするのに便利です。no-versioning フラグまたは関数オプション goose.WithNoVersioning() を使用します。

 

DeepL翻訳より。

railsActiveRecordのノリでいけるってことでしょうか?

そう信じてやっていきます。

 

 

 

リフレクションとは?

ちょいちょいリフレクションという単語が出てきているけど、聞いたことなかった。

 

pokuwagata.hatenablog.com

 

実行時にわかるデータを使ってなにかの処理をさせたいときに使う、のかな?

なんとなくはわかったが、曖昧です。

概念はわかったような気がするけど、実際にこれをどう使うのか?なんで使いたいのかがよく理解できていないです。

これは一旦置いてお生きます。

 

 

 

データベース連携方針まとめ

色々比較検討した上で、決めていきたかったのですが、メリットデメリットを考えられるほどつらみとかを理解していないってのもあり、判断軸がないです。

なので、友人が勧めてくれたSQL-Boilerとgooseを脳死で使っていきたいと思います。