- 投稿日:2020-04-05T21:13:15+09:00
goで作ったツールインストールするの面倒くさい
goでツール作ろう
「まくる」と言うほどには作ってないけど。
ところで、go製のツールいっぱいあるけど、管理だるくない?install は楽ちん。
$ go get github.com/foo/barで、一月ほど経った頃に、
bar
がアップデートしたらしいことを知る。
さて、なんだっけなと思いながら$ go get github.com/bar/barあれ、通らねえやとなる。しょうがないので、調べてああそうだ、fooさんだったこれ、となる。
…ならまだしも、変なツールインストールしちゃったりしたら目も当てられない。そしてさて、新しい環境作るかな、となるとこれをインストールしたツール分繰り返す。
だるい。homebrewっぽくしたい
作った。
https://github.com/kyoh86/gordon$ gordon install foo/bar $ which bar /home/kyoh86/.local/bin/bar $ gordon update $ gordon uninstall foo/bar $ gordon dump GordonDumpFile $ gordon restore GordonDumpFileあら便利。
制限
- GitHub Releasesにアップロードされたアセットしか見てない。
- GitHub Enterpriseへの対応が適当(環境がない)のでバグがありそう。
$ gordon config set github.host hogehoge.com
とか$ gordon config set github.user foo
とかして使えるはず。- アセットにはGOOS相当(windowsとか)とGOARCH相当(386とか)を含んでないと駄目。
- バイナリとmanファイルだけインストールする。
適当に作ったので、まだ全然ブラッシュアップできてない。
賛同者の方いらっしゃいましたらContributeお待ちしています。もらえたら嬉しいツッコミその他
- GitHub Releasesつかうのだるい
- → goreleaserを使え。良いぞ。
- Linuxbrew使えば?
- → はい。
- 投稿日:2020-04-05T20:47:02+09:00
PostgreSQLのNotifyを使ってGraphQL Subscriptionを実装する(バックエンド構築編)
前置き
前提
- prisma2インストール済み
- docker環境インストール済み
- 記事内のgqlgenはエイリアスに登録して使っています。
- gqlgenとprisma2のliftをある程度使っている人向け
サンプル
https://github.com/graphql-lab/subscription-with-postgres-notifyこんな感じの作っていきます
最終的にpythonのスクリプトを叩くと
異なる端末の画面がリアルタイムに更新されるというもの
これを応用すればスクレイピングした結果を定期的にwebサイトにつぶやかせる、というbotが
作れるかと
下準備
gqlgen init
からビルドし、必要なディレクトリを作って...とやっても良いのですが
今回はgqlgenの使い方の記事という事ではないので割愛します。
という事で今回は
gqlkit
というdockerベースのgraphqlサービスフレームワークを使っていきます。
gqlkit
の内容としては
マイグレーションツールにprisma2のliftを、
サーバー構築にgqlgenを使っていて
あとは、handlerやmiddlewareなど、
よく使うであろう機能のディレクトリを詰め合わせただけの他愛もないシンプルなフレームワークです。
任意のディレクトリで下記のリポジトリをクローンします。git clone git@github.com:gqlkit-lab/gqlkit.git subscription-with-postgres-notifyクローンできたらPostgreSQLとDB確認用のpgwebをdocker-composeで立ち上げましょう。
cd subscription-with-postgres-notify docker-compose up -dschema.prisma
PostgreSQLサーバーが立ち上がったら
早速、DBへのマイグレーション作業をやっていきます。schema.prismadatasource db { provider = "postgresql" url = "postgresql://postgres:postgres@localhost:5432/postgres?schema=public" } model messages { id String @default(cuid()) @id text String created_at DateTime @default(now()) updated_at DateTime @updatedAt }cd gqlkit-server/lift prisma2 lift save prisma2 lift up
これでpgwebで確認すると
Tables
のところにmessages
というテーブルが追加されているはずです。
ちなみに今回、ORMはgormを使うのでテーブル名の命名規則は複数形にしなければならない点に注意です。schema.graphql
次に、
schema.graphql
を書きgqlgen
でresolver等をビルドしていきます。
ここまで来れば、もうサーバー側の実装は半分は終わった感じです。schema.graphqltype Query { readMessages: [Message!]! } type Subscription { messageCreated: Message! } type Message { id: ID! text: String! created_at: String updated_at: String! }cd ../ # gqlkit-serverのルート gqlgenPostgreSQLのテーブルを監視するためのSQL関数を用意する
※ここからが今回の本題となります。
PostgreSQLの監視、通知の仕組みを有効にするためには以下のようなSQLを実行する必要があるようです。
下記のSQLは簡単に言えば
INSERTやUPDATE文が実行された際の
イベントを検知するトリガーと、イベントをキャッチして何らかの処理を行うハンドラを
PostgreSQLデータベースに組み込む為のSQLといったところです。
参考にさせて頂いたのはこちらです。begin; create or replace function 「イベント名」_handler () returns trigger language plpgsql as $$ declare channel text := TG_ARGV[0]; payload_json json; begin payload_json = json_build_object(「ペイロード」); PERFORM pg_notify(channel, payload_json::text); RETURN NULL; end; $$; CREATE TRIGGER 「イベント名」_trigger AFTER 「SQLのメソッド名(INSERT、UPDATE、DELETEなど)」 ON 「テーブル名」 FOR EACH ROW EXECUTE PROCEDURE 「イベント名」_handler('「テーブル名」'); commit;これをpgwebのqueryのところで叩いてしまっても良いとは思うのですが
それでは、少々不格好ですのでgormで叩いてやるというふうにしてみます。
servantというディレクトリを作りPGNotifyBuilder
というパッケージを作ります
dropTriggerという関数はこれによって
graphqlサーバーを起動した際に一旦、古いトリガーを全て削除し
throwNotificationSQLに書いてあるSQLで再度トリガーを作り直します。
dropTriggerが無いとトリガーの重複でエラーが発生してしまうのでここがポイントです。servant/PGNotifyBuilder/PGNotifyBuilder.gopackage PGNotifyBuilder import ( "fmt" "log" "github.com/jinzhu/gorm" _ "github.com/lib/pq" ) type Receive struct { DBConnect string EventName string Table string SqlMethod string Payload string } type PgTrigger struct { Tgname string } func dropTrigger(db *gorm.DB, tableName string) { var triggers []*PgTrigger db.Table("pg_trigger").Select("tgname").Scan(&triggers) for _, trigger := range triggers { sql := fmt.Sprintf(`DROP TRIGGER IF EXISTS %s ON %s CASCADE;`, trigger.Tgname, tableName) db.Exec(sql) } } func throwNotificationSQL(eventName string, table string, sqlMethod string, payload string) string { re := fmt.Sprintf( ` begin; create or replace function %s_handler () returns trigger language plpgsql as $$ declare channel text := TG_ARGV[0]; payload_json json; begin payload_json = json_build_object(%s); PERFORM pg_notify(channel, payload_json::text); RETURN NULL; end; $$; CREATE TRIGGER %s_trigger AFTER %s ON %s FOR EACH ROW EXECUTE PROCEDURE %s_handler('%s'); commit; `, eventName, payload, eventName, sqlMethod, table, eventName, table) return re } func Serve(r *Receive) { db, err := gorm.Open("postgres", r.DBConnect) defer db.Close() if err != nil { log.Fatal(err) } dropTrigger(db, r.Table) db.Exec(throwNotificationSQL(r.EventName, r.Table, r.SqlMethod, r.Payload)) }続いて、
servant/PGNotifyBuilder/PGNotifyBuilder.go
を
server.go
に読み込みます。package main import ( "gqlkit/env" "gqlkit/handler" pgnb "gqlkit/servant/PGNotifyBuilder" "log" "net/http" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/rs/cors" ) func main() { // Payloadはschema.prismaのmessagesモデルを参考に pgnb.Serve(&pgnb.Receive{ DBConnect: env.DB_CONNECT, EventName: "message_created", Table: "messages", SqlMethod: "INSERT", Payload: ` 'id', NEW.id, 'text', NEW.text, 'created_at', NEW.created_at, 'updated_at', NEW.updated_at `, }) r := chi.NewRouter() cors := cors.New(cors.Options{ AllowedOrigins: []string{env.GQL_SERVER_ALLOW_ORIGIN}, AllowedMethods: []string{"GET", "POST", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, }) r.Use(middleware.SetHeader("Content-Type", "application/json")) r.Use(cors.Handler) r.Handle("/", handler.Playground()) r.Handle("/query", handler.Graphql()) log.Printf("connect to http://localhost:%s/ for GraphQL playground", env.GQL_SERVER_PORT) log.Fatal(http.ListenAndServe(":"+env.GQL_SERVER_PORT, r)) }GraphQLサーバーのresolverを書く
まずは、resolver.go
resolver.gopackage graph import "gqlkit/graph/model" // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { messages []*model.Message message *model.Message }続いて
schema.resolvers.go
ReadMessages
schema.resolvers.gofunc (r *queryResolver) ReadMessages(ctx context.Context) ([]*model.Message, error) { db, err := gorm.Open("postgres", env.DB_CONNECT) defer db.Close() if err != nil { return nil, fmt.Errorf(err.Error()) } db.Order("created_at desc").Find(&r.messages) return r.messages, nil }MessageCreated
schema.resolvers.gofunc (r *subscriptionResolver) MessageCreated(ctx context.Context) (<-chan *model.Message, error) { event := make(chan *model.Message) reportProblem := func(ev pq.ListenerEventType, err error) { if err != nil { fmt.Println(err.Error()) } } listener := pq.NewListener(env.DB_CONNECT, 10*time.Second, time.Minute, reportProblem) err := listener.Listen("messages") if err != nil { panic(err) } go func() { for { select { case n := <-listener.Notify: err = json.Unmarshal([]byte(n.Extra), &r.message) if err != nil { fmt.Println(err) } event <- r.message } } }() return event, nil }テスト用のPython scriptを用意する
以上でGraphQLサーバーの構築はできましたので
GraphQLサーバーがちゃんと、Subscriptionの通知結果を返すのかテストする為のスクリプトを書きます。
別にpythonでなくてもいいですし
GraphQLサーバーにCreateMessage
というmutationを作ってテストしてもいいのですが
今回は、pythonでスクレイピングした内容をPostgreSQLにINSERTした際に
通知をするという今後の想定もあるという事で敢えてpythonを使います。docker-compose.ymlファイルがある階層にcreate-messageというディレクトリを作ります。
mkdir create-message cd create-message touch main.pymain.pyimport psycopg2 import uuid from datetime import datetime, timedelta, timezone def db_connect(): return psycopg2.connect("host=localhost port=5432 user=postgres dbname=postgres password=postgres sslmode=disable") def main(): with db_connect() as conn: with conn.cursor() as db: _id = uuid.uuid4() jst = timezone(timedelta(hours=+9), 'JST') now = datetime.now(jst) now = now.isoformat(timespec='seconds') db.execute(""" INSERT INTO messages ( id, text, created_at, updated_at ) VALUES (%s,%s,%s,%s); """,( str(_id), "test from python script", now, now )) if __name__ == '__main__': main()GraphQLサーバーを起動する
GraphQLサーバーを起動する前にenv.goファイルの
godotenvのコメントアウトを外します。
ここまでのソースコード内で登場していたenv.〇〇がこれで読み込めるようになります。
gqlkit-serverのルートで下記を実行します。go run server.gohttp://localhost:8080 でGraphQL Playgroundを開きます。
Playgroundで下記Queryを実行します。
ローダーが回り出し
通知待ち状態になります。subscription{ messageCreated{ id text created_at updated_at } }実際にpythonのscriptを実行してみます。
textにはtest from python script
とでも入れておきましょう。python main.pyまとめ
以上が、PostgreSQLのNotifyを使ってGraphQL Subscriptionを実装する方法の
バックエンド実装部分でした。
次回はnuxt.jsでのフロントエンド実装例をご紹介します。
- 投稿日:2020-04-05T17:05:21+09:00
Go言語によるWebアプリケーション開発 チャットアプリのその後
本記事について
Go言語によるWebアプリケーション開発のチャットアプリでちょっと遊んでみます
チャットアプリを外部公開する方法と、なりすましについての説明があります。
後者は手元のサーバ以外に実施すると、法に触れる可能性があります。悪用厳禁です外部公開
ngrok
を使用し、指定したポートを外部に公開することができます。
(画像にある通り、セッションは8時間で途切れてしまうみたいなので。実用的ではないですね)$ ngrok http 8080
実行すると、URLなどの情報がでてきます。後でhttpの方を使用します。本の手順でgoogle developer consoleを設定したら、
認証情報のOAuth 2.0 クライアント IDというところを以下の画像のように設定します。
ここで、2つのURIはngrokで表示されていたものを使用しています。再現する場合は適宜置き換えてくださいOAuth 2.0 クライアント IDの画面の右上の方にクライアント ID、クライアント シークレットの情報があるので
その情報とngrokのURLを使用し、main.go
の53行目、google.New()
の中身を書き換えます。main.gogoogle.New("[クライアントID]", "[クライアントシークレット]", "http://94150776.ngrok.io/auth/callbackgoogle"),これで準備完了です。ビルドし、実行します。
$ go build $ ./chat 2020/04/05 16:10:17 Webサーバーを開始します。ポート :8080
http://94150776.ngrok.io/login
をブラウザで開くと、ログイン画面が表示されます。
googleアカウントを使用してログインすると、チャットができるようになっています。
複数タブでアカウントを変えたり、友達を招待したりして遊べます。
遊ぶ
このチャットアプリ、名前や画像が自己申告制なので簡単になりすましができてしまいます。
※ 手元のサーバでも、他のユーザがいる状態で実行すると混乱を招く可能性があります。悪用厳禁ですやってみましょう。
デベロッパーツールのコンソールでcookieを確認します。
> console.log(document.cookie) auth=[cookieの値]cookieの値はbase64された文字列なので、複合します。
{"avatar_url":"[画像のURL]","name":"[名前]","userid":"[ユーザID]"}
アバターのURLや名前などのデータが入っています。値を書き換えてみます。
{"avatar_url":"https://help.qiita.com/images/qiitan-button.png","name":"きーたん","userid":""}
base64エンコードして、コンソールでcookieを書き換えます。> document.cookie="auth=eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9oZWxwLnFpaXRhLmNvbS9pbWFnZXMvcWlpdGFuLWJ1dHRvbi5wbmciLCJuYW1lIjoi44GN44O844Gf44KTIiwidXNlcmlkIjoiIn0="任意のユーザになりますことができた。
最後に
大事な事なので3回書きます。悪用厳禁です。
最後まで読んでくださり、ありがとうございました。
- 投稿日:2020-04-05T16:39:15+09:00
S3バケット別概算コスト算出ツールを作ってみた
はじめに
S3って安価なストレージなので、容量気にせずとりあえずなんでも格納して放置なんてことが多いですよね?
将来的に分析するかもってことでデータレイクに蓄積して結果使わないなんてことも多い。(my観測範囲)で、新型コロナウィルスの影響で先行き不安な状況なので、収束まではどうにか支出を抑えて凌ぎたいという
人も多いと思い、要否判断(&不要であれば削除)しやすいように、S3バケットごとに使用量/概算コストを
一覧表示するツールを作ったので、紹介します。ツール概要
一言で言えば、
全リージョン/全バケット別にオブジェクト数/使用バイト数/概算月額料金を出力するツール
です。(誰かが作ってそうなんですが探してもなかったので作りました)CloudWatchからバケットサイズなど取得できるようになってるので、それを実行すればawscliでも取得できる
のですが、リージョン指定しないといけないとか、メトリクス/ディメンションを指定するのがダルイので
そこらへんうまくやってくれるツールが欲しいなぁと思ったのがモチベーションです。
あと、請求コンソールだとバケット別の使用量はわからないので。補足(&免責事項)
- コスト算出について
- 実行時点の利用量を1ヶ月間継続した場合の概算請求額であり正確ではありません(ご利用は自己責任で)
- ストレージ保存量に対して課金される料金が対象であり、それ以外(リクエスト/転送量)のコストは含みません
- 概算請求額は東京リージョン料金(2020/04時点)で算出します
- 料金通貨は米ドル
- バージョニングについて
- 以前のバージョンのオブジェクトやそのサイズもカウントされます
- ツール結果と
aws s3 ls --recursive
等で得られるオブジェクト数は異なります要は
ざっくりでいいのでバケットごとにかかる料金を知りたい
って人がターゲットです実行方法
前提
- AWSクレデンシャル情報(~/.aws/credentials)が設置されていること
Go言語がインストールされている環境の場合
GO111MODULE=off go get -v github.com/miyaz/s3usage cd $(go env GOPATH)/src/github.com/miyaz/s3usage go run main.goそれ以外の環境
こちらから環境に応じたzipファイルをダウンロードし
展開後s3usage
というバイナリを実行しますオプション
- -p {プロファイル名}
- ~/.aws/credentials に記載されているプロファイル名
- 指定なし時(デフォルト)は
default
- -v
- ストレージタイプ毎の使用量/概算コストも表示したい場合に指定
実行例
おわりに
コロナに負けるな!
- 投稿日:2020-04-05T11:31:07+09:00
[Go] Realizeで実行ファイルを指定する
Realizeとは
realizeが分からない人はぜひこちらのページをみてください
https://qiita.com/enta0701/items/9f60ad18600acab8c93dRalizeでのディレクトリ指定
go言語にはディレクトリ構成のスタンダードがあり、実行ファイルはcmd配下、他のファイルはinternalなどの配下に保存するようです。
参考:https://qiita.com/sueken/items/87093e5941bfbc09bea8しかし、プロジェクトルートでrealizeを起動したところ、以下のエラーが出てしまいました
$ realize start [01:09:22][GO_BASE_REST] : Watching 7 file/s 15 folder/s [01:09:22][GO_BASE_REST] : Install started [01:09:23][GO_BASE_REST] : Install build .: cannot find module for path .なにも設定しなかった場合、
go build .
が実行されてモジュールがないと怒られているのかと思い
.realize.yamlの場所を変えようかと思ったのですが、go.modなどもプロジェクトルートに置いてあり、それもrealizeのために移動させるのは少し嫌だったので以下のように.realize.yamlに設定を追加したところ問題なく動くようになりました。
.realize.yamlsettings: legacy: force: false interval: 0s schema: - name: go_rest path: . commands: build: status: true method: go build -o cmd/go_rest/main cmd/go_rest/main.go run: status: true method: ./cmd/go_rest/main watcher: extensions: - go paths: - / ignore: paths: - .git - .realize - vendormethodに記入すると、本来のコマンドと置き換えられて実行されるようになり、
上記ではbuildで実行ファイルを作成、runで実行ファイルを起動しています。なんとなく気持ち悪いので他にもやり方があるのかもしれませんが、私は分からなかったです!
もっといい方法を知っている人がいたら是非教えて欲しいです!
- 投稿日:2020-04-05T00:01:00+09:00
Protocol Buffersのメッセージ定義からHTMLのフォームを生成してみる実験
はじめに
HTMLでフォームを作るのは技術的には簡単なのですが、何度もやることになると作業の重複が多くなって疲れるかと思います。ここではProtocol Buffersを用いてフォームの表示と処理の自動化を試みてみます。実運用するには足りない点も多々ありますので、一つの実験として読んでみて頂ければと思います。
コードは https://github.com/yt76/pbforms にあるので、cloneして(もしくはcloneしたつもりになって)お読みください。
Protocol Buffersによるフォームの定義
例えば、名前(string)と年齢(int32)と同意の有無(bool)を取得したいとします。このメッセージを my_form.proto というファイルにあるように次の内容で定義します。
message MyForm { optional string name = 1; optional int32 age = 2; optional bool agree = 3; }これをコンパイルし、
$ protoc myform.proto --go_out=forms/それを使うサーバーをビルドし
$ go build server/server.go実行し
$ ./server Serving at 8080ブラウザでアクセスすると
適当に埋めて
送信を押すと
内容がProtocol Buffersのテキスト形式で表示されます。実際のプログラム上ではこの各フィールドの値をデータベースに詰めることになりそうですが、このテキスト形式もしくはProtocol Buffersのバイナリ形式でそのまま保存したりRPCに投げることでProtocol Buffersの機能を使ったデータ操作をするなんてのも可能かと思います。
実装
実装といっても大したことはしてなくて、Goのリフレクションで各フィールドの名前と型を取り出してそれに応じたHTMLを生成し、POSTのハンドラ内ではフィールドの値を戻すといった感じです。
// コンストラクタ内 form := forms.MyForm{} sv.writer = pbforms.NewFormWriter(form) sv.reader = pbforms.NewFormWrite() // GETのハンドラ fmt.Fprintf(w, "<html><body>\n") // <form method="post" action="/">…</form>が生成される sv.writer.Write("/", w) fmt.Fprintf(w, "</body></html>\n") // POSTのハンドラ f := forms.MyForm{} // r *http.Requestでformの値からfを埋める sv.reader.Parse(r, &f)最後に
- すぐに思いつく改善点としては見栄えの改善ですが、適当なCSSのクラスを出力してあとからJavaScriptで外側のHTMLのスタイルに書き換えるという手でなんとかなりそうです。
- ここではstringとintとboolをHTML formのtextとcheckboxに対応させましたが、その他の要素もProtocol Buffersの様々な機能を使えばそんなに無理せずに対応できる?
- フォーム上にNameとかAgeとかフィールド名をそのまま出してしまってるので、別途メッセージカタログ(国際化もできればなお良い)的なものが必要ですね。
ネット上には様々なフォームがあって、その中には技術の粋をつくした一品物から何かのコピペで済ましたい安易なものまで色々とありますが、適当なレベルのところまではこの記事で紹介したようなものを多少改善すれば間に合うのではないかなと考えてます。