Open31

Golang × Postgres × Docker(Simple Bank)のチュートリアルをやってみたログ

ピン留めされたアイテム
CHIAKICHIAKI

以下のチュートリアルを進めていきます!
(このボリュームで無料ってすごい…)

https://github.com/techschool/simplebank

スター数は多めなのですがあまり日本語での記事が出てこなかったので、
分からなかったこと、調べたことをメモがわりに書いていきます。

学ぶこと

  • Golang × Postgres × Docker を使用したAPI開発
  • タイトルのSimple Bankの通り、銀行口座作成、送金処理等を実装していく
  • AWSを用いてデプロイまで行う
  • テストも書く

具体的な使用技術(ライブラリ・ツール)

  • PostgreSQL

  • Docker

  • Github(git-flow開発、GithubActiions)

  • AWS(ECR,EKS,RDS,)

  • Go

  • ライブラリ:
    sqlc,Gin,gomock,testify,JWT,PASETO

  • ツール

  • dbdiagram.io

  • TablePlus

チュートリアルの構成

  • #1-10 DB構築
  • #11-22 API実装
  • #23-37 Docker環境構築・AWSへのデプロイ

所感

TBD

理解が浅い点

  • DB操作(トランザクション処理、デッドロック)
  • UnitTest(モック化、httptest)
ピン留めされたアイテム
CHIAKICHIAKI

[Backend #1] Design DB schema and generate SQL code with dbdiagram.io

学ぶこと

  • DB設計(テーブル、主キー、外部キー、インデックス、複合インデックス、)
  • dbdiagram.io を用いたDBスキーマの出力
  • SQLのコードの生成

メモ

  • dbdiagram.ioはSQLを使用することなく手軽にDBスキーマを書けるので便利(typoのwarningが出なかったので、typo のままデータを出力してしまった為その点気をつける必要がありそう)
  • インデックスを付けると検索時に早くなる、ただむやみに作ってはいけない、必要最小限。

インデックスは、データベースの性能を向上させるための一般的な方法です。 データベースサーバでインデ
ックスを使用すると、インデックスを使用しない場合に比べてかなり速く、特定の行を検出し抽出すること>ができます。 しかし、インデックスを使用すると、データベースシステム全体にオーバーヘッドを追加する>ことにもなるため、注意して使用する必要があります。

参考: インデックス

CHIAKICHIAKI

始める前の私のレベル感

  • Golang: 業務で書いたことはあるが、初心者レベル
  • PostgresSQL: 独学時に軽く触ったくらい。業務もFirestoreなのでRDBの基本的なところから学ぶ必要あり
  • Docker: 独学時に軽く触ったくらい。コンテナ内で何が起こっているのかわからなくて苦手だった…
CHIAKICHIAKI

[Backend #5] Write Golang unit tests for database CRUD with random data

学ぶこと

  • sqlc 配下に自動生成されたCRUD処理のテスト
  • testifyを用いたテスト方法
  • ランダムなテストデータの作成方法

メモ

  • ここにきて、構造体名がAccounts と複数形になっていることに気づく
type Account struct {
	ID        int64     `json:"id"`
	Owner     string    `json:"owner"`
	Balance   int64     `json:"balance"`
	CteatedAt time.Time `json:"cteated_at"`
}

type Entry struct {
	ID        int64 `json:"id"`
	AccountID int64 `json:"account_id"`
	// can be negative
	Amount    int64     `json:"amount"`
	CteatedAt time.Time `json:"cteated_at"`
}

type Transfer struct {
	ID            int64     `json:"id"`
	FromAccountID int64     `json:"from_account_id"`
	ToAccountID   int64     `json:"to_account_id"`
	Amount        int64     `json:"amount"`
	CteatedAt     time.Time `json:"cteated_at"`
}

理由: sqlc.yml の emit_exact_table_namesがtrueになっていたので、テーブル名がそのまま構造体名に自動で使用されてしまっていた。
進めていく上で大きな問題ではないが、単数系にしたかったのでemit_exact_table_names` にfalse を指定
することで修正。

    emit_exact_table_names: false
  • データベースのDriverパッケージとしてlib/pqパッケージを使用する
CHIAKICHIAKI

[Backend #7] DB transaction lock & How to handle deadlock in Golang

学ぶこと

  • DB のロックについて
  • DBのデッドロックについて
  • ログを用いたデバッグ方法
  • TDD

メモ

  • PostgresではBEGIN でトランザクションを開始し、ROLLBACKでロールバックを行うことができる
  • select文にFOR UPDATEをつけると行ロックをかけることができる
  • context.Withvalue() をデバッグに使用できる
  • 外部キーの制約によってデッドロックを引き起こすことがある(一度では理解しきれなかったので要復習…
  • defer をつけることでその処理を関数内の最後に呼び出すことができる
CHIAKICHIAKI

[Backend #2] Install & use Docker + Postgres + TablePlus to create DB schema

学ぶこと

  • Docker Desktopのインストール
  • PostgreSQLのコンテナ生成、起動
  • TablePlusのインストール
  • #1 でdbdiagram.ioで出力したDBスキーマを使用したSQLコードの生成

メモ

  • docker run でコンテナを起動できる。docke run の裏側ではdocker pull + docker create + docker start をまとめて行ってくれる
  • docker runt -it コンテナ名 bin/sh でdocker のコンテナを作って中に入ることができる
  • docker exec 実行中のコンテナ内で1つのコマンドを実行できる。起動中のコンテナ内で指定したコマンドを実行するコマンドなので、コンテナは起動していないといけない。
  • docker logs コンテナのログを確認できる
  • docker start で停止していたコンテナを起動できる
  • ポートマッピング(ポートフォワーディング): HTTPリクエストを受けるコンテナの場合、コンテナ外から来たリクエストをコンテナ内のアプリケーションにまで到達させる必要がある。
    ホストマシーンのポートをコンテナポートに紐づけることで、コンテナポートをコンテナ外からも使用できるようにする。(同じポート番号にすることも多い)
CHIAKICHIAKI

[Backend #9] Understand isolation levels & read phenomena in MySQL & PostgreSQL via examples

学ぶこと

  • トランザクション分離レベルとは
  • トランザクション分離レベルの扱い方について、MySQLとPostgresSQLで比較する

メモ

CHIAKICHIAKI

[Backend #10] Setup Github Actions for Golang + Postgres to run automated tests

学ぶこと

  • Github Actions のworkflowの書き方を学ぶ

メモ

  • GithubActiond はGItHubが提供しているCI/CDサービス(2019年にリリース)
    GitHubの新機能「GitHub Actions」でワークフローを自動化しよう
  • workflow は .github/workflows に置かれているyamlファイル1つ1つのこと
    イベント・スケジューリング・手動のいずれかをトリガーにして実行できる
  • Runs on で使用するRunnerを指定する
  • job は同じRunner を用いて実行されるステップ、並列で実行される
  • step はjob 内で実行される
  • action はstep 内で実行される
CHIAKICHIAKI

[Backend #11] Implement RESTful HTTP API in Go using Gin

学ぶこと

  • GoのフレームワークのGin(高速でライブラリが豊富)をインストールし、GInを利用してRESTfulなAPIを作成する
  • 今回のチャプターではアカウントのCRUD処理に関するAPIを実装する

メモ

  • APIサーバーとしてServer構造体を定義
// simple bank の全てのHTTPリクエストを処理するHTTP APIサーバーを実装する
type Server struct {
	config util.Config
	// クライアントからのリクエストに応じてDB とやりとりするために必要(dbを持つために構造体にした)
	store      *db.Store
	tokenMaker token.Maker
	// 各APIリクエストを正しいハンドラに送信して処理するのに役立つ
	router     *gin.Engine
}

  • APIサーバーを立ち上げた状態でPOSTMAN上でPOSTメソッドを使用してリクエストを送ってみる
  • Handlerは第一引数の場合に第二引数の関数を実行する
  • createAccountRequest CreateAccountParams等、リクエストデータの種類ごとに構造体を作り分けている
CHIAKICHIAKI

[Backend #12] Load config from file & environment variables in Golang with Viper

学ぶこと

  • Viper を使用して設定ファイルから環境変数を読み込む(Viper を使用することで環境変数のデータを構造体に変換、といった処理もできるようになる)
CHIAKICHIAKI

[Backend #13] Mock DB for testing HTTP API in Go and achieve 100% coverage

学ぶこと

  • #11で実装したAPIのテスト
  • gomock を用いたDBのテスト

メモ

  • モックを用いたテストの方が実際のDBに接続して書くテストよりも独立したテストがしやすい
  • 実際のDBに接続しないため、クエリを待つ時間等を短縮でき、テスト実行速度が速い
  • カバレッジが100パーセント
var _ Querier = (*Queries)(nil)
CHIAKICHIAKI

[Backend #14] Implement transfer money API with a custom params validator

学ぶこと

  • #11 で実装した内容の続きである、送金APIの実装
  • validator を使用した、任意の項目のバリデーションチェックを行う関数の実装

メモ

-reflectionとは、プログラム実行中に型の情報などを判別するgoのモジュール(あまり使わない)

CHIAKICHIAKI

[Backend #15] Add users table with unique & foreign key constraints in PostgreSQL

学ぶこと

  • DBを後から部分的に変更した場合の移行方法

  • ユーザーの認証機能を新たに追加するために、usersテーブルを新設する

  • #1 でも使用したdbdiagramを使用する(typoなどwarning表示してくれるので便利!!)

  • passwordを変更するタイミングもDBに保存するのか〜

  • usersテーブルが作成されたことで既存のカラムにも制約を追加する

    • 1人の所有者に対して複数通貨の口座を持つことはできるが同じ通貨の口座は1つまでしかもてない
    • 所有者と通貨に対して一意の制約をもたせるクエリの書き方は以下2通り
      --- CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
      ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency");
  • 以下の方法でDBの移行はできるが、実践的ではない

    1. dbdiagramからexport postgressSQL で sqlファイルをエクスポートする
    2. プロジェクトのdb/migrate/000001_init_schema.up.sql のファイル内容を全て削除し、
      先ほどのsqlファイルの内容をコピペする
  • 新しい移行ファイルを作る方がよい

    • migrate create -ext sql -dir db/migrate -seq add_users コマンドを叩き、移行ファイルを生成する(この時点ではファイルの中身は空)
    • 生成ファイルに現在のDBとの差分だけ記載する

エラー内容

  • error: Dirty database version 2. Fix and force version.
    make: *** [migrateup] Error 1 のエラーがよく出る…
     - Table Plus上で dirty をFALSEに手動で修正すれば解決できる
    • 外部キー制約を後から追加したりするとDBのマイグレーションが失敗することがある。その場合make migratedownで全てのDBを削除し、再度make migrateup を行うことで解決できる

メモ

  • DBを後から部分的に変更した場合の移行方法
CHIAKICHIAKI

[Backend #16] How to handle DB errors in Golang correctly

学ぶこと

  • #15 で作成したusers テーブルに対応するgoのコードを書く
  • Postgresから返されるエラーの対処法について学ぶ

メモ

  • ShouldBindJSON で構造体からJSONにマッピングしている

エラー内容

  • make test でcan not connect to dbsql: unknown driver "postgres" (forgotten import?) というエラーが出てテストが実施できなかった
    • db/sqlc/main_test.go に以下をimport することで解決。元々importされていたがテスト修正のタイミングで消してしまっていたよう。
	_ "github.com/lib/pq"
  • テスト実施で以下のエラー。
    これまでaccounts テーブルのowner フィールドには外部キー制約がついていなかったが、
    #15 で新たに追加したのでエラーになっている。
Error Trace:    account_test.go:20
                                                        account_test.go:79
                Error:          Received unexpected error:
                                pq: insert or update on table "accounts" violates foreign key constraint "accounts_owner_fkey"
                Test:           TestListAccount
  • usersテーブルのusername にないowner を作成しようとするとエラーになる(外部キー制約があるため)

{
    "error": "pq: insert or update on table \"accounts\" violates foreign key constraint \"accounts_owner_fkey\""
}
CHIAKICHIAKI

[Backend #17] How to securely store passwords? Hash password in Go with Bcrypt!

学ぶこと

  • #15 で作成したusers テーブルのカラムであるhassed_password にハッシュ化したパスワードを格納する方法を学ぶ
  • bcryptに用意されているGenerateFromPassword 関数を使用すればハッシュ化したパスワードが一瞬で生成できる!
CHIAKICHIAKI

[Backend #18] How to write stronger unit tests with a custom gomock matcher

学ぶこと

  • #13 で学んだモックを用いたテストを作成する
  • gomock.Any() マッチャーを使用している箇所を、カスタムしたマッチャーに置き換える

メモ

  • 匿名の構造体の書き方
 []struct {
		name          string
		body          gin.H
		buildStubs    func(store *mockdb.MockStore)
		checkResponse func(recoder *httptest.ResponseRecorder)
	}{
// 具体的な値を定義する

  • gomock.Any() はどんな引数でも取れてしまうので、便利だが危険でもある。
    カスタムマッチャーを作成することで厳格なテストを作成するようにする
CHIAKICHIAKI

[Backend #19] Why PASETO is better than JWT for token-based authentication?

学ぶこと

  • この回は講義中心で手を動かすパートは少なめ
  • トークンベースの認証の実装方法を学ぶ
  • 秘密鍵と公開鍵それぞれのメリットデメリット

メモ

  • トークンにはさまざまな種類があるが、広く普及されているトークンの1つにJSON Webトークン(JWT)がある。JWTとは認証やアクセス制御についての情報をJSON形式で記述し、一定の手順で符号化した「トークン」(token)を生成することができる仕組みのこと。
    https://e-words.jp/w/JWT.html
  • しかし直近ではJWTの脆弱性なども明らかになっているので、よりセキュリティが向上したPASETOを例にあげ、トークンベースの認証への理解を深める。
CHIAKICHIAKI

[Backend #20] How to create and verify JWT & PASETO token in Golang

学ぶこと

  • JWTでの認証の実装方法、テストを学ぶ
  • PASETOでの認証方法を学ぶ

メモ

CHIAKICHIAKI

[Backend #21] Implement login user API that returns PASETO or JWT access token in Go

学ぶこと

  • #20でJET,PASETOを用いてそれぞれ作成したtoken をサーバーに登録する
  • ユーザー認証のAPIを実装する。

メモ

  • ユーザー認証のAPIの具体的な動作について:
    1.クライアントからユーザー名とパスワードを送る
    2.サーバーはデータを検証し、情報が正しければアクセストークンを返す
CHIAKICHIAKI

[Backend #22] Implement authentication middleware and authorization rules in Golang using Gin

学ぶこと

  • 前回実装したAPIにBearerトークンをベースに認証を組み込むように改良する
    前回作成したAPIではアカウントの所有者ではなくても、ログインできてしまう仕様だった。
    今回のチャプターでBearerトークンベースの認証レイヤーを追加することで、送金API、アカウント取得APIなどの認証機能を実装する(例:認証されているアカウントのアカウント情報しか取得できない等)
  • ミドルウェアの役割について学ぶ
  • ミドルウェアを実装する

メモ

  • 認証ミドルウェアではクライアントから送られてきたアクセストークンが有効かどうかを検証する
  • 認証/Authentication=通信の相手が誰であるかを検証すること
  • 認可(承認)/Authorization=特定の動作を行うことを許可すること
  • Bearer認証について
  • payload = 一般的にはメタデータを除いた、伝達したいデータそのもののこと
CHIAKICHIAKI

[Backend #23] Build a minimal Golang Docker image with a multistage Dockerfile

このチャプターから、これまで開発したAPIをデプロイする方法について学ぶ

学ぶこと

  • Dockerfileの記載方法
  • Dockerfileをビルドし、Dockerイメージを作成する方法
  • github-flowを用いた開発方法
CHIAKICHIAKI

[Backend #24] How to use docker network to connect 2 stand-alone containers

学ぶこと

  • DBの役割を果たすsome-postgress コンテナとAPIサーバーとしてのsimple-bankコンテナは
    同じネットワークIPアドレスを共有していないので、ローカルホストを利用してsome-postgressに接続することができない。
  • viperを使用して、DB_SOURCEを読み取ることができるようにする
  • bridgeを使用して2つの独立したコンテナを接続する

メモ

  • Dockerのコンテナはデフォルトではコンテナ外部のネットワークと通信することができない。
    以下のURLが非常にわかりやすかった
    参考 Dockerのネットワークモデル
CHIAKICHIAKI

[Backend #25] How to write docker-compose file and control service start-up orders with wait-for.sh

学ぶこと

  • docker compose を使用して同じネットワーク内のコンテナを一度にセットアップする方法
  • 同じネットワーク内のコンテナを一度に立ち上げる方法

メモ

  • docker-compose up
    Dockerイメージがない場合:イメージ生成・コンテナ生成を行なってくれる
    Dockerイメージがある場合:コンテナを生成・起動してくれる

  • docker-compose up でコンテナを起動後、ちゃんとサービスが起動しているかをたしかめるためには
    リクエストを送ることで確認できる(チュートリアル内ではPostmanを使用)

  • 作成した start.sh はbin/shによって実行され、起動したい内容を記載する

CHIAKICHIAKI

[Backend #26] How to create a free tier AWS account

学ぶこと

  • アプリケーションをデプロイするための無料のAWSアカウントの作成方法
CHIAKICHIAKI

[Backend #27] Auto build & push docker image to AWS ECR with Github Actions

学ぶこと

  • GithubAction を使用してDockerイメージを自動的にビルドし、AWS ECRにデプロイする
  • Github Secretに環境変数を保存(GithubActionsを走らせる際にロードして使用する)

メモ

  • ECR はDockerのコンテナイメージを保管しておくためのレジストリ
CHIAKICHIAKI

[Backend #28] How to create a production DB on AWS RDS

学ぶこと

  • AWS RDS を使用して本番環境のDBを作成する
CHIAKICHIAKI

[Backend #30] Kubernetes architecture & How to create an EKS cluster on AWS

学ぶこと

  • AWS EKSを用いてKubernetes Cluster をセットアップする

メモ

  • Kubernetes については以下の記事がわかりやすかったです。

Kubernetesをデプロイすると、クラスタが構築されます。
クラスタは、マスターノードとワーカーノードの2種類で構成されています。
ワーカーノードはコンテナの実行環境を提供します。
マスターノードはワーカーノードとコンテナを管理します。
1つのマスターノードと1つのワーカーノードがクラスタの最小構成です。
クラウドで話題のコンテナ技術「Kubernetes」を使ってみた!

CHIAKICHIAKI

[Backend #31] How to use kubectl & k9s to connect to a kubernetes cluster on AWS EKS

学ぶこと

  • Kubernetesに接続する方法を学ぶ
  • Kubernetesクラスターに対してコマンドを実行できるkubectlを学ぶ

メモ

> % aws eks update-kubeconfig --name simple-bank --region ap-northeast-1
Unable to locate credentials. You can configure credentials by running "aws configure".

講義内容と違うエラーだがエラーの原因は同じ

CHIAKICHIAKI

[Backend #3] How to write & run database migration in Golang

学ぶこと

  • golang-migrate を利用したDBマイグレーションを行う方法
メモ
  • golang-migrate はDBマイグレーションツールの1つ
  • DBマイグレーションとはDBの構造(スキーマ)を簡潔、便利に変更するための仕組み
  • up migrate が実行されると、数字の昇順にup.sqlファイルが実行される
  • down migrate が実行されると、数字の昇順にdown.sqlファイルが実行される
  • DBを削除する際には、外部キー制約のあるテーブル(外部キーをもっていて、他テーブルを参照しているテーブル。今回のテーブルだとtransfers とentriesテーブル)を先に削除する
  • docker exec -it コンテナID /bin/sh は コンテナへログインしてシェル操作するコマンド
-> % docker exec -it some-postgres /bin/sh
# pwd
// この中はdocker のコンテナのシェルの中

# psql simple_bank
psql (14.2 (Debian 14.2-1.pgdg110+1))
Type "help" for help.

simple_bank=# dropdb simple_bank
// この中はDBのsimple_bankの中
CHIAKICHIAKI

[Backend #4] Generate CRUD Golang code from SQL | Compare db/sql, gorm, sqlx & sqlc

学ぶこと

  • goでDBを操作するためのCRUD処理の書き方を学ぶ
  • SQLCの使用方法

メモ

  • goでDBを操作する方法はいくつかあるが(SQL,GORM,SQLX等)今回はSQLCを使用する
  • SQLCによって、DBを操作するCRUD処理が自動で生成されるのでとても便利!
    DB操作したい時は自動生成されたメソッドを呼び出せば良い。
    (動作を保証するために次のチャプターでテストを書く)
  • go mod tidy で必要なパッケージのインストールや不要なパッケージの削除を行なってくれる
  • SQLCで自動生成されるQueries構造体はdb とトランザクション処理を内部にもっているので、
    基本的なDB操作はQueries構造体をレシーバとしてメソッドとして実装する
CHIAKICHIAKI

[Backend #6] A clean way to implement database transaction in Golang

学ぶこと

  • goでSQLトランザクションを実装する方法
  • go routineで並列処理を行う

メモ

// Store provides all functions to execute db queries and transactions
type Store struct {
	// コンポジションを実装している
	// Queriesを持つことで、Queriesがもつ個別のメソッドを使用することができるようになる
	*Queries
  // トランザクションをサポートするために必要
	db *sql.DB
}

  • トランザクション処理などこれまで使用していない処理が多かったので再度確認したい