Golang × Postgres × Docker(Simple Bank)のチュートリアルをやってみたログ
以下のチュートリアルを進めていきます!
(このボリュームで無料ってすごい…)
スター数は多めなのですがあまり日本語での記事が出てこなかったので、
分からなかったこと、調べたことをメモがわりに書いていきます。
学ぶこと
- 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)
[Backend #1] Design DB schema and generate SQL code with dbdiagram.io
学ぶこと
- DB設計(テーブル、主キー、外部キー、インデックス、複合インデックス、)
- dbdiagram.io を用いたDBスキーマの出力
- SQLのコードの生成
メモ
- dbdiagram.ioはSQLを使用することなく手軽にDBスキーマを書けるので便利(typoのwarningが出なかったので、typo のままデータを出力してしまった為その点気をつける必要がありそう)
- インデックスを付けると検索時に早くなる、ただむやみに作ってはいけない、必要最小限。
インデックスは、データベースの性能を向上させるための一般的な方法です。 データベースサーバでインデ
ックスを使用すると、インデックスを使用しない場合に比べてかなり速く、特定の行を検出し抽出すること>ができます。 しかし、インデックスを使用すると、データベースシステム全体にオーバーヘッドを追加する>ことにもなるため、注意して使用する必要があります。
参考: インデックス
始める前の私のレベル感
- Golang: 業務で書いたことはあるが、初心者レベル
- PostgresSQL: 独学時に軽く触ったくらい。業務もFirestoreなのでRDBの基本的なところから学ぶ必要あり
- Docker: 独学時に軽く触ったくらい。コンテナ内で何が起こっているのかわからなくて苦手だった…
[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パッケージを使用する
[Backend #7] DB transaction lock & How to handle deadlock in Golang
学ぶこと
- DB のロックについて
- DBのデッドロックについて
- ログを用いたデバッグ方法
- TDD
メモ
- Postgresでは
BEGIN
でトランザクションを開始し、ROLLBACK
でロールバックを行うことができる - select文に
FOR UPDATE
をつけると行ロックをかけることができる -
context.Withvalue()
をデバッグに使用できる - 外部キーの制約によってデッドロックを引き起こすことがある(一度では理解しきれなかったので要復習…
) - defer をつけることでその処理を関数内の最後に呼び出すことができる
[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リクエストを受けるコンテナの場合、コンテナ外から来たリクエストをコンテナ内のアプリケーションにまで到達させる必要がある。
ホストマシーンのポートをコンテナポートに紐づけることで、コンテナポートをコンテナ外からも使用できるようにする。(同じポート番号にすることも多い)
[Backend #9] Understand isolation levels & read phenomena in MySQL & PostgreSQL via examples
学ぶこと
- トランザクション分離レベルとは
- トランザクション分離レベルの扱い方について、MySQLとPostgresSQLで比較する
メモ
- トランザクション分離レベルについてのまとめ
- #9まで続いたgoを用いたデータベース操作については一旦今回で一区切り
[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 内で実行される
[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
等、リクエストデータの種類ごとに構造体を作り分けている
[Backend #12] Load config from file & environment variables in Golang with Viper
学ぶこと
- Viper を使用して設定ファイルから環境変数を読み込む(Viper を使用することで環境変数のデータを構造体に変換、といった処理もできるようになる)
[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)
- 上記のようなvar _ Interface = (*Type)(nil) という書き方は Type がInterfaceを満たしている、ということを表すために記載する
Goのnilだけどnilじゃないちょっとだけnilな値
[Backend #14] Implement transfer money API with a custom params validator
学ぶこと
- #11 で実装した内容の続きである、送金APIの実装
- validator を使用した、任意の項目のバリデーションチェックを行う関数の実装
メモ
-reflectionとは、プログラム実行中に型の情報などを判別するgoのモジュール(あまり使わない)
[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の移行はできるが、実践的ではない
- dbdiagramからexport postgressSQL で sqlファイルをエクスポートする
- プロジェクトの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のマイグレーションが失敗することがある。その場合
メモ
- DBを後から部分的に変更した場合の移行方法
[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\""
}
[Backend #17] How to securely store passwords? Hash password in Go with Bcrypt!
学ぶこと
- #15 で作成したusers テーブルのカラムであるhassed_password にハッシュ化したパスワードを格納する方法を学ぶ
- bcryptに用意されているGenerateFromPassword 関数を使用すればハッシュ化したパスワードが一瞬で生成できる!
[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() はどんな引数でも取れてしまうので、便利だが危険でもある。
カスタムマッチャーを作成することで厳格なテストを作成するようにする
[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を例にあげ、トークンベースの認証への理解を深める。
[Backend #20] How to create and verify JWT & PASETO token in Golang
学ぶこと
- JWTでの認証の実装方法、テストを学ぶ
- PASETOでの認証方法を学ぶ
メモ
- JWTについては以下の記事が分かりやすかった
-
errors.Is()
でerror同士を比較してくれる - JWTよりもPASETOの方がシンプルに実装できる
[Backend #21] Implement login user API that returns PASETO or JWT access token in Go
学ぶこと
- #20でJET,PASETOを用いてそれぞれ作成したtoken をサーバーに登録する
- ユーザー認証のAPIを実装する。
メモ
- ユーザー認証のAPIの具体的な動作について:
1.クライアントからユーザー名とパスワードを送る
2.サーバーはデータを検証し、情報が正しければアクセストークンを返す
[Backend #22] Implement authentication middleware and authorization rules in Golang using Gin
学ぶこと
- 前回実装したAPIにBearerトークンをベースに認証を組み込むように改良する
前回作成したAPIではアカウントの所有者ではなくても、ログインできてしまう仕様だった。
今回のチャプターでBearerトークンベースの認証レイヤーを追加することで、送金API、アカウント取得APIなどの認証機能を実装する(例:認証されているアカウントのアカウント情報しか取得できない等) - ミドルウェアの役割について学ぶ
- ミドルウェアを実装する
メモ
- 認証ミドルウェアではクライアントから送られてきたアクセストークンが有効かどうかを検証する
- 認証/Authentication=通信の相手が誰であるかを検証すること
- 認可(承認)/Authorization=特定の動作を行うことを許可すること
- Bearer認証について
- payload = 一般的にはメタデータを除いた、伝達したいデータそのもののこと
[Backend #23] Build a minimal Golang Docker image with a multistage Dockerfile
このチャプターから、これまで開発したAPIをデプロイする方法について学ぶ
学ぶこと
- Dockerfileの記載方法
- Dockerfileをビルドし、Dockerイメージを作成する方法
- github-flowを用いた開発方法
[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のネットワークモデル
[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によって実行され、起動したい内容を記載する
[Backend #26] How to create a free tier AWS account
学ぶこと
- アプリケーションをデプロイするための無料のAWSアカウントの作成方法
[Backend #27] Auto build & push docker image to AWS ECR with Github Actions
学ぶこと
- GithubAction を使用してDockerイメージを自動的にビルドし、AWS ECRにデプロイする
- Github Secretに環境変数を保存(GithubActionsを走らせる際にロードして使用する)
メモ
- ECR はDockerのコンテナイメージを保管しておくためのレジストリ
[Backend #28] How to create a production DB on AWS RDS
学ぶこと
- AWS RDS を使用して本番環境のDBを作成する
[Backend #30] Kubernetes architecture & How to create an EKS cluster on AWS
学ぶこと
- AWS EKSを用いてKubernetes Cluster をセットアップする
メモ
- Kubernetes については以下の記事がわかりやすかったです。
Kubernetesをデプロイすると、クラスタが構築されます。
クラスタは、マスターノードとワーカーノードの2種類で構成されています。
ワーカーノードはコンテナの実行環境を提供します。
マスターノードはワーカーノードとコンテナを管理します。
1つのマスターノードと1つのワーカーノードがクラスタの最小構成です。
クラウドで話題のコンテナ技術「Kubernetes」を使ってみた!
[Backend #31] How to use kubectl & k9s to connect to a kubernetes cluster on AWS EKS
学ぶこと
- Kubernetesに接続する方法を学ぶ
- Kubernetesクラスターに対してコマンドを実行できるkubectlを学ぶ
メモ
- ローカル環境ではpythonのバージョンが2.7.18であった為エラー
AWS CLI をインストールする
https://qiita.com/mokuo/items/0484feefbaddb70a9b06 - pyenv 経由で3.9系をインストール
[MacのPythonを2系から3系にアップデートする]
(https://qiita.com/Ajyarimochi/items/ff40e57d082dd171e761) - 以下の記事を参考に、反映させる
pyenvでpythonのバージョンが反映されない時
> % aws eks update-kubeconfig --name simple-bank --region ap-northeast-1
Unable to locate credentials. You can configure credentials by running "aws configure".
講義内容と違うエラーだがエラーの原因は同じ
[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の中
[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構造体をレシーバとしてメソッドとして実装する
[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
}
- トランザクション処理などこれまで使用していない処理が多かったので再度確認したい