20211201のGoに関する記事は6件です。

【AWS SAM Go】Go*SAM で環境変数を導入するには

前提 AWS SAM を用いてAWSのリソースをコードで管理できるようにしています。(Infrastructure as Code) 今回はSAMでAWS lambdaを作成し、その上でGoで書かれた関数を実行できるようにしております。 また、今回利用するSAMはAWS SAM テンプレートを使用してサーバーレスアプリケーションを作って作成しました。 環境はdefault環境のみで動作させてます。 結論 ローカル起動時(sam local start-api / sam local invoke)とデプロイ時(sam deploy)とで、環境変数の読み込み方法には違いがあります. ローカル起動時に環境変数を読み込むには 1. env.jsonを利用する方法 ルートディレクトリにenv.jsonの配置 template.yamlにて、Globalsセクション、あるいは、ResourcesセクションのEnvironment > Variables 配下に環境変数のKeyを設置 local起動時に、env.jsonを読み込んでもらうオプションを指定する が必要になります。 ルートディレクトリにenv.jsonの配置 { "Parameters": { "DB_USERNAME": "ユーザー名", "DB_PASSWORD": "パスワード", "HOSTNAME": "ホスト名", "PORT": "ポート名", "DB_NAME": "DB名" } } template.yamlにて、Globalsセクション、あるいは、ResourcesセクションのEnvironment > Variables 配下に環境変数のKeyを設置 Globals: Function: Timeout: 5 Environment: Variables: DB_USERNAME: DB_PASSWORD: HOSTNAME: PORT: DB_NAME: local起動時に、env.jsonを読み込んでもらうオプションを指定する > sam local start-api --env-vars env.json これであとはコード内で環境変数を読み込めばOKです! main.go func Connect() *sql.DB{ db_source_name := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USERNAME"), os.Getenv("DB_PASSWORD"), os.Getenv("HOSTNAME"), os.Getenv("PORT"), os.Getenv("DB_NAME")) log.Print(db_source_name) db, err := sql.Open("mysql", db_source_name) または、次のような方法もあります。 2. samconfig.tomlを利用する方法 sam deploy --guidedを行うことで作成されるsamconfig.tomlにstart-apiあるいはlocal-invokeをする際に送られるパラメーターを上書きする方法になります。 version = 0.1 [default] # start-apiする場合 [default.local_start_api.parameters] parameter_overrides = "DbUsername=DB名 DbPassword=パスワード Hostname=ホスト名 Port=ポート名 DbName=DB名" # local invokeする場合 [default.local_invoke.parameters] parameter_overrides = "DbUsername=DB名 DbPassword=パスワード Hostname=ホスト名 Port=ポート名 DbName=DB名" [default.deploy] [default.deploy.parameters] parameter_overridesに、上書きしたいパラメーターを記述します。 注意として、この上書きしたいパラメーターを記述する際のKey名はキャメルケースでないと正常に環境変数を読み込んでくれません。 次に、template.yamlに、パラメータを渡します。 # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 5 Environment: Variables: DB_USERNAME: !Ref DbUsername DB_PASSWORD: !Ref DbPassword HOSTNAME: !Ref Hostname PORT: !Ref Port DB_NAME: !Ref DbName 任意のFunction内でのみ環境変数を渡したいときは以下のようにResourcesの中に書いてあげてください。 Globalsに書くと全てのFunctionsに環境変数が適用されます。 Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: Runtime: go1.x Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: DB_USERNAME: !Ref DbUsername DB_PASSWORD: !Ref DbPassword HOSTNAME: !Ref Hostname PORT: !Ref Port DB_NAME: !Ref DbName これであとはコード内で環境変数を読み込めばOKです! main.go func Connect() *sql.DB{ db_source_name := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USERNAME"), os.Getenv("DB_PASSWORD"), os.Getenv("HOSTNAME"), os.Getenv("PORT"), os.Getenv("DB_NAME")) log.Print(db_source_name) db, err := sql.Open("mysql", db_source_name) デプロイ時に環境変数を読み込むには samconfig.yamlにパラメーターを上書きするための記述をする version = 0.1 [default] [default.deploy] [default.deploy.parameters] parameter_overrides = "DbUsername=DB名 DbPassword=パスワード Hostname=ホスト名 Port=ポート名 DbName=DB名" 次に、template.yamlに、パラメータを渡します。 # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 5 Environment: Variables: DB_USERNAME: !Ref DbUsername DB_PASSWORD: !Ref DbPassword HOSTNAME: !Ref Hostname PORT: !Ref Port DB_NAME: !Ref DbName 次に渡すパラメーターの型をParametersセクションに追加します template.yaml Parameters: DbUsername: Type: String DbPassword: Type: String Hostname: Type: String Port: Type: String DbName: Type: String Globals: Function: Timeout: 5 Environment: Variables: DB_USERNAME: !Ref DbUsername DB_PASSWORD: !Ref DbPassword HOSTNAME: !Ref Hostname PORT: !Ref Port DB_NAME: !Ref DbName これであとはコード内で環境変数を読み込めばOKです! main.go func Connect() *sql.DB{ db_source_name := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USERNAME"), os.Getenv("DB_PASSWORD"), os.Getenv("HOSTNAME"), os.Getenv("PORT"), os.Getenv("DB_NAME")) log.Print(db_source_name) db, err := sql.Open("mysql", db_source_name) 参照 パラメーターの上書きの仕方 AWS SAM CLI の設定ファイル localで環境変数の読み込ませ方 sam local start-api GLobalな環境変数の指定の仕方と、Resourcesごとの環境変数の指定の仕方 AWS SAM テンプレートの Globals セクション
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

これから始めるなら気にしたいGoの作法

この記事は Goのカレンダー | Advent Calendar 2021 - Qiita の3つ目のカレンダーのの3日目です。 これはなに? あくまで筆者の主観で、Golangを使ったアプリケーションはこういった書き方が多い/こういう風にすると良さそう、を書き連ねる記事 「主観で」とはいえど、なるべく有名どころのライブラリなどを参考に持ち出して執筆します Golangの基本文法が分かる人向け インデントはタブが主流 Golangのデフォルトがタブ 標準ライブラリやPlayground、有名なライブラリなどを見るとタブ採用が多い 参考: timeパッケージのSleepメソッド 参考: Go Playground 参考: webフレームワークのecho タブだとGitHub上でコード読みにくい!な問題への対処法 .editorconfigファイルを使うことで、見た目上のインデント幅を任意のサイズに指定できる 参考: pre { tab-size: 4 } · Issue #170 · isaacs/github 参考: echoにも指定されている または、Chrome拡張機能もある GitHub Custom Tab Size - Chrome ウェブストア // Before func main() { fmt.Println("スペース4つ") } // After func main() { fmt.Println("タブ") } (エディタ上だとスペースとタブの違いわかるけど、Qiitaの記事表示だと違い伝わらない… ) testは同階層でOK ここでいう「階層」とは「package」を指す 階層を分けてしまうとunexported(private)な構造体や変数を単体テストで使うためにはexport(public)が必要になる 実処理としてはexport不要だが、テストのためだけにexportする、のような状況が生じてしまうので良くない これを避けるために同階層に置くべし 下記参考を見るとそうなっている 参考: webフレームワークのecho 参考: csvライブラリのgocsv ただし、基本的には同階層に置いているがtestspackageを切ってその中にもテストファイル置くパターンもある(理由はしっかり中身読めばわかる…はず…) ORMのgorm // Before ┣ repository ┣ campaign_repository.go ┣ repository_test ┣ campaign_repository_test.go // After ┣ repository ┣ campaign_repository.go ┣ campaign_repository_test.go エラーメッセージは基本的に「小文字始まり」「句読点終わりは避ける」 エラーメッセージ(標準だとfmt.Errorf, errors.New)に関しては、「小文字始まり」「句読点終わりにしない」、が公式の推奨 エラーメッセージは別のエラーメッセージやログメッセージなどの中で使われることもあるので、エラーメッセージを文章のようにしてしまうと読みにくくなるため もちろん、固有名詞や頭字語はこの限りではない 参考: CodeReviewComments · golang/go Wiki ログメッセージに関してはこの限りではないが、揃えたほうがわかりやすいため、基本的に「小文字始まり」「句読点終わりにしない」で良さそう こちらは諸説ありそう // Before err := errors.New("Required field cannot be null.") log.Errorf("Create test file failed. (err=%+v)", err) // After err := errors.New("required field cannot be null") log.Errorf("create test file failed(err=%+v)", err) エラーまわりの書き方 こちらもCodeReviewCommentsを参考にした話 参考: CodeReviewComments · golang/go Wiki エラーは即時リターン // Before err := db.Err if err != nil { // エラーハンドリング } else { // 通常処理 } // After err := db.Err if err != nil { // エラーハンドリング } // 通常処理 err変数のスコープは狭くできる スコープ狭くなってヨシ // Before err := db.Err if err != nil { return err } // After if err := db.Err; err != nil { return err } ただ、スコープを無理に狭くしすぎると逆に読みにくくなるので注意 // Before if x, err := f(); err != nil { return err } else { // use x } // After x, err := f() if err != nil { return err } // use x 変数名 Golangでは略語は全部小文字 or 大文字に ID, id→Identifierの略語 CV, cv→Conversionの略語 参考: CodeReviewComments · golang/go Wiki // Before var campaignId int64 var CvData struct{} // After var campaignID int64 var CVData struct{} import, type, const, var句あたりの順番 標準パッケージ参考にするとimport→type→const→var→funcの順になっている 参考: zoneinfo.go - Go package time import ( "toto" ... ) type Titi struct { name string ... } const ( tata = "" ... ) var ( tutu = "" ... ) import句内部の記述順 標準パッケージの塊を先頭に、あとは他の塊ごとに空行を挟んで記述し、それぞれの塊の中ではアルファベット順で記述 参考: CodeReviewComments · golang/go Wiki // Before import ( "os" "fmt" "github.com/toto/titi" "github.com/tata/tutu" "log" "myapp/foo" "myapp/user" "net/http" ) // After import ( "fmt" "log" "net/http" "os" "myapp/foo" "myapp/user" "github.com/tata/tutu" "github.com/toto/titi" ) 定数はまとめる constは複数ある場合はまとめる 同カテゴリである場合はまとめて、同カテゴリではない場合は別でまとめる 変数varについても同様 // Before const expireDate = "2006-01-02T15:04:05" const maxLength = 100 const MethodGet = "GET" const MethodPost = "POST" // After const ( expireDate = "2006-01-02T15:04:05" maxLength = 100 ) const ( MethodGet = "GET" MethodPost = "POST" ) swtich-case文 これは基本文法だけど、別の条件で同じ処理の書き方忘れがちなので載せる ちなみに、Golangではcaseごとでbreakするのでfallthroughしたい場合はcase文の最後にfallthroughを明記する // Before switch type { case enum.ONE: fmt.Println("hoge") case enum.TWO: fmt.Println("hoge") case enum.THREE: fmt.Println("fuga") default: fmt.Println("piyo") } // After switch type { case enum.ONE, enum.TWO: fmt.Println("hoge") case enum.THREE: fmt.Println("fuga") default: fmt.Println("piyo") } 合わせて読みたい CodeReviewComments · golang/go Wiki 本記事でもかなり参考にしているので原文を是非 Go言語の記述の迷いどころについて この記事書いた後に見つけた 「Go言語らしさ」とは何か? Simplicityの哲学を理解し、Go Wayに沿った開発を進めることの良さ - エンジニアHub|Webエンジニアのキャリアを考える! Golang書くならこの辺も読みたいかも
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fake Time

Overview Fake Time for test. not best way. Detail var now = Time.Now // if you want to use specify time, you call it. then replace time to argument valuen. // then, you receive function is for revert original time module. for finish test, recommend call by defer func switchFakeTime(t time.Time) (revertFakeTime func()) { now = func() time.Time { return t } return func() { now = time.Now } } fmt.Println(now()) revertFunc := switchFakeTime.(Time.Now()) fmt.Println(now()) time.Sleep(10 * time.Second) fmt.Println(now()) revertFunc() fmt.Println(now())
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

generic な golang gRPC サーバをつくる

まえふり gRPC サーバを実装するには protobuf ファイルから各言語のコードを自動生成し、各 RPC ごとの処理を実装することが一般的と思われる。一方で、どんな RPC に対しても同様な処理を行いたい generic な gRPC サーバを実現したい場合、もう少し簡単に RPC の追加を行いたい。 C++ の場合、このような generic な gRPC サーバを構築する方法はよく知られているように思う。ここのフォーラムで言及されているし、 一例を挙げると envoy の json_grpc_transcoder で似たようなことは実装されている。 また、 Java でも同様なことができるとサンプルコードとともに提示されている。 このような低レイヤのIFの提供は全ての言語で行われているわけではないが、golang でも2020年に新しくなったライブラリを使えば generic な golang gRPC サーバを構築できそうだったのでつくってみたい。 descriptor と dynamic message 具体的な実装に触れる前に、 generic な gRPC サーバを作るための要素となる descriptor と dynamic message について軽く触れておく。 descriptor descriptor は protobuf ファイルに対応する情報を持った protobuf message のことである。 protobuf ファイルを作成すると何らかの名称・型・オプション等を持つ field を含んだ message を作ることになる。descriptor はこれらのような protobuf ファイルで定義される情報を保持できるような構造を持った protobuf message となっている。descriptor の protobuf ファイル にあるように、protobuf の file・service・rpc・message・field・enum のそれぞれに対応する file descriptor・service descriptor・method descriptor・field descriptor・enum descriptor が存在する。 なので、file descriptor がわかっていれば protobuf ファイルをほぼ復元することができる。 dynamic message dynamic message は message descriptor から生成された protobuf message のことである。 protobuf message は各言語のコード自動生成によって各言語に応じた型やクラスにマッピングされるが、これをこれを使うとそれらの型やクラスを使わずに、 runtime で渡された descriptor をもとに protobuf message を生成できる。 C++ は dynamic_message, Java は DynamicMessage, Golang は dynamicpb で実装されている。 golang での実装 golang protobuf の標準ライブラリを使って実装する。 descriptor_set ファイルの生成 protobuf ファイルは公式の helloworld.protoサンプルを利用する。 unary RPC を一つ持つシンプルな protobuf を例として取り上げるが、 複数 unary や stream RPC を含む場合でも似たようなことができると思う。 protobuf ファイルから下記コマンドで descriptor_set ファイルが生成される。これはhelloworld.proto の内容を google.protobuf.descriptor.FileDescriptorSet message の形に serialize されたファイルとなっている。 protoc --descriptor_set_out=helloworld_descriptor.pb helloworld.proto descriptor set file を取り込む gRPC サーバ起動時に RegisterService メソッドを呼び出して service を登録する際に、 descriptor_set ファイル から descriptorpb.FileDescriptorSet に deserialize して全ての service を取得して下記のようにそれらを登録する。 func newGrpcServer() (*grpc.Server, error) { s := grpc.NewServer() bytes, err := ioutil.ReadFile("<descriptor file path>") if err != nil { return nil, err } var fileSet descriptorpb.FileDescriptorSet if err := proto.Unmarshal(bytes, &fileSet); err != nil { return nil, err } files := protoregistry.GlobalFiles for _, fd := range fileSet.File { d, err := protodesc.NewFile(fd, files) if err != nil { return nil, err } for i := 0; i < d.Services().Len(); i++ { // protobuf service descriptor から gRPC service descriptor に変換して service を登録する s.RegisterService(...) } } return s, nil } 上記サンプルでは省略したが、もし gRPC reflection の機能をサーバに取り込みたい場合protoregistry.GlobalFiles.RegisterFile(...) を使って proto file を registry に登録する必要がある。 RPC 呼び出し時の処理をおこなう 全ての RPC で同様な処理を行いたい場合、 gRPC service descriptor を作成するときに grpc.MethodDesc.Handler に同一の handler を仕込むことになる。 その handler は例えば下記のような実装になる。request, response message ともに service descriptor から取得できる input message descriptor, output message descriptor から dynamic message を生成している。 func unaryHandler(svcd protoreflect.ServiceDescriptor, md protoreflect.MethodDescriptor) func(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { return func(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := dynamicpb.NewMessage(md.Input()) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return callRPC(ctx, in, md.FullName(), md.Output()) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: fmt.Sprintf("/%s/%s", svcd.FullName(), md.Name()), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return callRPC(ctx, req.(proto.Message), md.FullName(), md.Output()) } return interceptor(ctx, in, info, handler) } } // RPC が呼ばれた際の処理例 func callRPC(base context.Context, request proto.Message, fullName protoreflect.FullName, mdesc protoreflect.MessageDescriptor) (interface{}, error) { reqbyte, err := protojson.Marshal(request) if err != nil { return nil, err } var m map[string]interface{} err = json.Unmarshal(reqbyte, &m) if err != nil { return nil, err } resp := dynamicpb.NewMessage(mdesc) respbyte := []byte(fmt.Sprintf("{\"message\":\"%s\"}", fmt.Sprintf("hello %s", m["name"]))) err = protojson.Unmarshal(respbyte, resp) return resp, err } さいごに 実際に実行可能なコードはこちらのリポジトリにまとめた。 今回は標準ライブラリを使って実装したが、github.com/jhump/protoreflect も使いやすい API になっておりオススメ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

golangはじめました

概要 素人としてこの1ヶ月間Golangでアプリケーション作ることになりましたので、所感や実際調べたことについて共有し、少しでも参考になれれば幸いです。 とりあえず現時点でのディレクトリ構成を貼ります 左がrootディレクトリで右がsrcディレクトリ配下です。 srcというディレクトリを切るかどうかで結構悩んでいましたが、とりあえず切ることにしました。 IDE、開発環境、実行環境 IDEは下記の理由でVSCodeにしました。 - VSCodeにはGolang開発チームが提供している(元々はMicrosoftがメンテしてたが、Golang開発チームに移管された)pluginがあるので、実質VSCodeがstandardらしい - JetBrainsのGoLandというのがあるが、有料なので、今回はスキップ(30日間無料トライアルはある) - Intellij Ultimate使う場合はaws toolkitもあり Docker化 Golangアプリケーションは最終的にはLinuxのELF形式のバイナリ1個にまとまりますので、気軽にdocker化できます。 Golangアプリケーションを動かすためのdocker imageを探した結果、googleがメンテしているDistrolessイメージ群を発見し、良さそうでしたので、今回使うことにしました。参考サイト そして、今回はDockerのmulti stage buildを使って一つのDockfileにてbuildとアプリケーションの実行まで書いてシンプルな構成にしました。 multi-stage-build例 FROM golang:1.17.3-bullseye as builder ## -- buildするイメージ COPY src/ /go/src/ WORKDIR /go/src RUN GOOS=linux GOARCH=amd64 go build -o hoge main.go FROM gcr.io/distroless/base:latest ## -- アプリケーション実行するイメージ & 最終的に出来上がるイメージ COPY --from=builder /go/src/hoge / ENTRYPOINT [ "/hoge" ] パッケージ構成、アーキテクチャについて 私自身、長らくJavaでAPI開発とかやってきた者としては、SpringフレームワークをベースにしたMVCパターンに馴染みがありますが、GolangにはSpringみたいな強力なフレームワークが存在しませんので、プログラミングの原点に戻ってクリーンアーキテクチャを検討することにしました。 ディレクトリ構成画像からわかるように完全にクリーンアーキテクチャに出てくるレイヤーを元にディレクトリを切っています。クリーンアーキテクチャも今回初めてですし、うまく説明する自信がないですので、下記の記事を参考いただければと思います。 https://qiita.com/nrslib/items/a5f902c4defc83bd46b8 ディレクトリ構成や、アーキテクチャについて色々先駆者達が試行錯誤しているような印象があって、これが正解だというのがないですので、チーム内で話し合って今の構成に決めました。(クリーンアーキテクチャのレイヤーで切ってたけど、後で直したよっていう経験談もあったが将来的に壁にぶつかったらまた考えることに) WebフレームワークはGin アーキテクチャの次にフレームワークの選定です。 githubのスター数だけではすべての指標ではないですが、Ginが一番スター数があるのは事実、そして活発的に開発されるし、ググったらたくさん参考できる日本語サイトも出てきますので、良いのではないかというところでGinにしました。 他にGinの特徴はこんなところかなと思います。 速い 軽量 JetBrainsさんがアンケートベースで出したレポートでも1位取ってますね こちら 各フレームワーク比較もご参考にどうぞ コンパイル GolangがサポートしているOS/CPUアーキテクチャや下記のコマンド確認できるので、環境ごとにコンパイルすることもできます。 サポートOS/CPU go tool dist list ...(一部省略) linux/386 linux/amd64 linux/arm linux/arm64 ...(一部省略) 今回本番環境はAWSのECS Fargateを想定していて、つい先日arm64もサポートするリリースがありましたので、linux/amd64とlinux/arm64のコンテナを用意することにしました。(実際どちらを使うかはまだ決めてないですが、arm64の方が省エネで料金節約になるらしいです。) 本番用と開発用のコンパイルオプションについて -race : thread-safeをチェックするみたいなもの(実際つけるとアプリケーションが遅くなるみたいな記事もあったので、チェックは開発環境だけにして、本番用にコンパイルする時はつけないようにしています) -w -s : このオプションをつけないとDebug用の情報も入るので、本番はつけてないです。 コンパイル例 // 開発用 go build -race -o hoge main.go // 本番用 go build -o hoge main.go -ldflags "-w -s" クロスコンパイルときの注意点 クロスコンパイルでは-raceは使えないです。詳細はこちら 例:amd64のサーバーでarm64用のコンパイルをする時 Makefileにについて Golangはコンパイラ言語としてMakefileの利用が主流になっています。 buildコマンドや環境構築コマンドをMakefileにまとめておくと結構便利です。 例えば本番用と開発用のコンパイルオプションについてで紹介した開発環境と本番環境のコンパイル差別化もMakefileに下記のようにまとめておくとmakeコマンド一つで実行できるようになります。 Makefile例 # disable symbol table and dwarf GO_LDFLAGS_SYMBOL:= ifeq ($(HOGE_ENV),prod) GO_LDFLAGS_SYMBOL:=-w -s endif GO_LDFLAGS:=$(GO_LDFLAGS_SYMBOL) # race detector GO_BUILD_RACE:=-race ifeq ($(HOGE_ENV),prod) GO_BUILD_RACE:= endif # go build GO_BUILD:=$(GO_BUILD_RACE) -ldflags "$(GO_LDFLAGS)" .PHONY: build build: GOOS=linux GOARCH=amd64 go build -o hoge $(GO_BUILD) main.go makeコマンド例 # 開発環境用build HOGE_ENV=dev make build # 本番環境用build HOGE_ENV=prod make build 使用ライブラリ viper JSON, YAML, TOMLなどをサポートする設定ファイル用ライブラリ 参考できる資料はたくさんありますので、こちらでは割愛しますが、どんなふうに使っているかだけを簡単に紹介します。go:embedと組み合わせて使ってます。 go:embedは静的ファイルをコンパイル済みELF形式バイナリファイルに含める機能を提供しています configディレクトリ ├── config.go └── yaml ├── config.prod.yml └── config.dev.yml config.xxx.yml例 database: postgres: name: hoge host: localhost port: 5432 user: user pass: password 上記のようなディレクトリ構造で環境ごとの設定(DB設定など)を保持しているとするとconfig.goの中身はこうなります。 config.go type Config struct { Database struct { Postgres struct { Name string Host string Port string User string Pass string } } } var ( //go:embed yaml/* staticYamlDir embed.FS ) func LoadConfig() *Config { setDefaultEnv() hogeEnv := viper.GetString("env") fileName := "yaml/config." + hogeEnv + ".yml" viper.SetConfigName("config." + hogeEnv) viper.SetConfigType("yaml") viper.AddConfigPath("config/yaml") b, err := staticYamlDir.ReadFile(fileName) if err != nil { panic("Failed to read config.") } if err := viper.ReadConfig(bytes.NewReader(b)); err != nil { panic("Failed to load config.") } var c Config if err := viper.Unmarshal(&c); err != nil { panic("Failed to unmarshal config.") } return &c } func setDefaultEnv() { viper.SetEnvPrefix("hoge") viper.BindEnv("env") viper.AutomaticEnv() viper.SetDefault("env", "dev") } これでアプリケーション起動時にHOGE_ENVという環境変数を設定することで読み込むconfigファイルがそれぞれ違うことになるので、環境別の設定をファイル分けて管理することができます。 sqlboiler ORMライブラリです。使い方は割愛します。 有名なGORMとかありますが、今回sqlboilerを採用したのは下記の観点からです。 schemaからコード生成がやりやすい 静的に型付けされており、実行時にリフレクションを使う必要がないため高速 migration機能はないがflywayとかでカバーできる 生queryがかける relationもサポート air Golangはコンパイラ言語ですので、開発時に修正、ビルド、実行のサイクルを頻繁に行わないと行けないです。 airはそのサイクルを自動化してくれるいわゆるホットリロードライブラリとしてローカルでの開発効率を上げてくれます。 所感 Javaみたいにアノテーションつけておけばよしなにやってくれるみたな書き方ができないですが、とにかく素直にコードを書くところがGolangの特徴かなと思います。(JavaはそもそもSpringという強力な武器があるので、言語だけの比較にはならないですが) 良いところ: 新卒でもコード見ればある程度理解できるようなわかりやすさ 軽量、高速 クラウドやコンテナ技術、マイクロサービスなどと親和性が高い 面倒なところ: Golangには例外という概念がなく、常にif err != nilしないと行けないこと Golangは何でもシンプルで行こうという設計思想から他の言語と差別化をし、たくさんの支持を得ていますので、使いにくいところもあるかもしれませんが、今後も仲良くなって行けそうな雰囲気でしたー。 最後に 新しい言語を勉強する時にネットにたくさんの情報が散らがっていて、実際そのとおりにやって見たらなんか古いやり方だったり、アンチパターンで作ったりするかもしれませので、一番先にオフィシャルドキュメントを一読することをおすすめします。特にGolangの場合はオフィシャルドキュメントが結構しっかりしていて、読みやすいので、ぜひご確認ください。 オフィシャルドキュメント Effective go ※これとかは必ず読んで見てください。 Golangの目玉機能: goroutine, channelについては興味ありますが、まだ触れてないので、今後このあたりも攻略したいと思います。 おわり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

引越し業界初の新サービスを支えるバックエンドの技術

この記事はエイチーム引越し侍/エイチームコネクトの社員による、Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021一日目の記事です。 2021年も終わりが近づいて来ましたね〜! 初日は、エイチーム引越し侍でバックエンドを主に担当している新卒2年目・完全感覚programmerの@sho-hataが担当します!! はじめに 「引越し侍 ネット見積もりサービス」 というサービスを11月1日にリリースしました(β版)? 今までの引越し見積もりサービスは、複数の引越し業者と電話・メール・訪問対応の必要があったのですが、本サービスは引越し業者の比較・相談・申込をネットで完結できる業界初のサービスになっています。 β版のため、現在は関東エリア発着の引越しのみ対応ですが、今後機能・エリア拡充し2022年6月頃に正式版をリリース予定です。 本記事では、「引越し侍 ネット見積もりサービス(β版)」(以下、新規サービス)を作るにあたり、どういった技術・アーキテクチャを採用したのか、その概要と背景を共有します。 技術選定に関して 新サービスの技術選定では、以下のような状況・要望がありました。 ??‍?「引越し見積もり」というドメインはビジネスフローがかなり複雑なため、できるだけシンプルかつスケールしやすい構成にする必要 ?‍♂️サービス立ち上げ期のためできるだけ早く世に出したいというビジネス要件 このような背景のもと、具体的な技術選定の方針として以下の2つの基準で検討が進められました。 ・開発にかかる工数を限りなく少なくする ・ビジネスロジックを分離してスケールしやすくする ここからは、採用したアーキテクチャ・技術について述べていきます。 アーキテクチャの全体像 簡単な全体像は以下の図のようになります。 フロントエンドはWebになっており、Next.jsで動いています。認証/チャット機能はFirebaseが担っており、基本的なデータ取得はHasura Cloud(GraphQLサーバー)が担っています。ビジネスロジックが絡む処理は、Hasuraの後ろのGoで稼働するREST(ful)APIサーバーが担います。データベースはPostgreSQLです。 以下では、それぞれの技術の採用理由をまとめています。 サービスを支える技術 1.GraphQL GraphQL APIを採用した理由は以下になります。 ・フロントエンドのユースケースに合わせて、柔軟にデータを提供したい ・データ取得のパフォーマンスを良くしたい 既存サービスは長らくPHPで稼働するRESTful APIを用いていました。その歴史はとても古く、度重なる改修によって ・必要ないデータフェッチをしてしまっているAPI ・1つのページで4~5回APIを叩いている などのパフォーマンス面で課題がありました。また、フロントエンドのユースケースが1つ増えるごとに専用のAPIを開発する必要があるなど、工数/設計面においても課題がありました。 このような反省から、よりフロントエンドから扱いやすいGraphQLを選択しています。採用実績がなかったため、最初の教育・学習コストは低くありませんでしたが、Fragment colocationなどの機能によりフロントエンドの設計の見通しが良くなるなど、開発者体験が大きく改善されました。 2.Hasura GraphQLサーバーには、Hasuraを使用しています。フロントエンドからのリクエストを一手に引き受けており、いわゆる"BFF"(Backend For Frontend)的な使い方をしています。また、セルフホスティングではなくマネージドのHasura Cloudを使用しています。 Goのライブラリgql-genやAWSのAppSyncと比較し、以下の理由でHasuraを採用しました。 ・工数ゼロで、CRUDリゾルバが作成できる ・認証周りの機能が組み込みで充実しており、Firebaseとの相性も良い ・ビジネスロジックを既存・外部のGraphQL・REST(ful)APIで拡張できる Hasuraは、DB設計さえしてしまえば工数ゼロで基本的なリゾルバが作成できるため、かなり開発工数を短縮することができました。 ただ、裏を返せばロックインしている状態でもあります。また、幸いサービスの段階的にそれほどではありませんが、ビジネスロジックがサービスの成長に伴いどんどん増えていくことが予想されます。Remote Schema・Actionで拡張ができるのは強力ですが、設計方針を決めておかないと管理できずカオスになっていくため、方針を確定してしっかりと運用に乗せていかなければなりません。 ここではざっくりとまとめましたが、Hasuraを半年間使用してみてのメリット/デメリットについて、より踏み込んだ内容を以下の記事でまとめています。合わせて読んでいただけると幸いです。 3.Go Hasuraではできないようなドメイン固有のビジネスロジック(ユーザーの入力情報から引越し業者を選択する・概算料金を算出するなど)はGoで書かれています。具体的な採用理由は以下になります。 ・静的型付けによる型安全 ・言語仕様がシンプル。学習コストが他の静的型付け言語より比較的低い ・後方互換性が保たれている(1.x系) こちらのGoプロジェクトでは、Clean Architectureに寄せた設計技法を採用しています。他の言語だと、モノリシックなデザインのフレームワーク・ライブラリが多いですが、Goのライブラリは言語仕様と同じく小さくシンプルなものが多いため、プログラムがフレームワークに依存しにくく非常に相性が良かったです。 また、強力な周辺ツール・ライブラリが揃っており、開発者体験向上の恩恵を受けています。 例えば、gomockを使用することで、インタフェース定義からモックを生成して手軽にテストができています。また、gotestsはコマンド一発でテストコードの雛形を自動生成してくれるので、開発者はテストケースを書くだけで良い状態にしてくれます。使用法もシンプルで扱いやすいため、大きく役立っています。 4.PostgreSQL RDBには、PostgreSQLを採用しています。既存システムはほぼMySQLだったのでMySQLを検討していたのですが、プロジェクト発足時点ではHasuraがPostgreSQLしかサポートしていませんでした。今後Hasuraのバージョンアップにより全面的にMySQLがサポートされるため、そのときにメリット/デメリットを再吟味した上でMySQLへの移転を検討しています。 5.Firebase 新サービスでは引越しに必要な情報を入力した後、マイページにて引越し業者とのチャットでのやりとりや、引越し予約の確定が可能です。この認証の仕組みにはFirebase Authenticationを使用しています。 以下が、採用の理由になります。 ・チャット機能でFirebase Cloud Firestoreを採用しているため、アクセス制御の連携が楽 ・Firebase Authで認証・発行したJWT TokenをHasuraで使用できる また、チャット機能はNoSQLを採用しており、Firebase Cloud Firestoreを使用しています。 その他開発を支える技術 以下では、直接サービスを構成しているわけではありませんが、開発者体験向上などを支えている技術を紹介します。 6.Renovate 新サービスでは、Renovateによってアプリケーションの依存パッケージ・DockerFileなどのアップデート作業を半自動化しています。 一週間に一度、GitlLab Cl/CD PipelineでスケジューリングされたRenovateが実行され、アップデートがあったライブラリ・Dockerイメージのマージリクエストが作成されます。レビュアーとしてランダムアサインされたメンバーは該当のリリースノート・自動テスト結果をチェックし、問題なければ手動でマージするという運用です。 Renovateの導入によって、人力の手動アップデートの面倒さから開放されるとともに、開発メンバーのアップデート内容への感度も、プロジェクト発足時と比較すると高くなるなどのメリットがありました。 よりポピュラーなDependabotがありますが、GitLabへの導入の手軽さという点からRenovateを選択しています。 Go × GitLab CIによるGo Packagesの自動更新の詳細については下記にまとめています。 7.Husky, Commitlint huskyを使用して、各Gitアクション(commit・merge)時にlinterやスクリプトを走らせています。 コミット時には、コミットメッセージがチームで決めたコミット規約に従っているかをチェックするCommitlintを走らせており、コミットメッセージのスタイルを合わせています。 また、プロジェクトの依存パッケージ(npm・gomod)・DockerイメージはRenovateによってどんどん半自動的にアップデートされていきます。そこで、リモートからpullした際にpackage.jsonやDockerfileに差分があった場合、yarn installを実行するなどのスクリプトが走るような仕組みになっています。 8.カバレッジツール カバレッジ算出・表示には、coberturaを採用しており、Go用のヘルパー https://github.com/boumenot/gocover-cobertura を使用しています。 算出されたテストカバレッジ情報をXMLファイルに出力し、GitlabのTest Coverage Visualization機能を使用して、マージリクエスト(MR)の差分に表示しています。 MRの差分ビューでテストが通っていない部分が視覚的にすぐわかるため、コードレビュー時に非常に役に立っています。 まとめ ここまで新サービスのバックエンド全体の技術スタックを紹介しました。 最初にも述べましたが、 ・開発にかかる工数を限りなく少なくする ・ビジネスロジックを分離してスケールしやすくする という思想・ビジネス要件の下、上記のような技術を選択しました。これらの技術の採用により、引越し侍の新サービス立ち上げを少エネルギーかつスピーディーに進めることができました。 ただ、現在のアーキテクチャ構成はベストではなく、課題がいくつもあります。一例を挙げると、マルチクラウド構成になっている(AWS、GCP)ため複数のクラウドベンダーの障害を気にしないといけません。 こういった課題については、2週間に1回ペースでこれからのアーキテクチャ構成について話し合う"Architecture talks"という取り組みを行い、試行錯誤しながら改善サイクルを回しています。 おわりに ここまで読んでいただき、ありがとうございました!新サービスの立ち上げにおいて自分達が行なってきた技術選択の意思決定が、これから新規プロジェクトを立ち上げる方々にとって少しでも役に立てれば幸いです。 次回?✏️? Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021初日の記事はいかがでしたでしょうか!? 明日は、同じチームのめちゃつよ先輩フロントエンドエンジニア、@anneauによる「新サービスを支えるフロントエンド」についての記事です! 本記事で説明したバックエンドと表裏一体の関係、フロントエンドはどうなっているのか。こちらもトレンドの開発スタイルを取り入れたりなど、多くの技術的なチャレンジをしているのでお楽しみに〜〜!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む