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

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使えば?
    • → はい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PostgreSQLのNotifyを使ってGraphQL Subscriptionを実装する(バックエンド構築編)

前置き

前提

  • prisma2インストール済み
  • docker環境インストール済み
  • 記事内のgqlgenはエイリアスに登録して使っています。
  • gqlgenとprisma2のliftをある程度使っている人向け

サンプル
https://github.com/graphql-lab/subscription-with-postgres-notify

こんな感じの作っていきます
最終的にpythonのスクリプトを叩くと
異なる端末の画面がリアルタイムに更新されるというもの
これを応用すればスクレイピングした結果を定期的にwebサイトにつぶやかせる、というbotが
作れるかと
subscription.gif

下準備

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 -d

schema.prisma

PostgreSQLサーバーが立ち上がったら
早速、DBへのマイグレーション作業をやっていきます。

schema.prisma
datasource 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.graphql
type Query {
    readMessages: [Message!]!
}

type Subscription {
    messageCreated: Message!
}

type Message {
    id: ID!
    text: String!
    created_at: String
    updated_at: String!
}

cd ../ # gqlkit-serverのルート
gqlgen

PostgreSQLのテーブルを監視するための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.go
package 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.go
package 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.go
func (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.go
func (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.py
main.py
import 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.go

http://localhost:8080 でGraphQL Playgroundを開きます。
Playgroundで下記Queryを実行します。
ローダーが回り出し
通知待ち状態になります。

subscription{
  messageCreated{
  id
  text
  created_at
  updated_at
}
}

実際にpythonのscriptを実行してみます。
textにはtest from python scriptとでも入れておきましょう。

python main.py

するとこんな感じに通知が取れるはずです。
subtest.output.gif

まとめ

以上が、PostgreSQLのNotifyを使ってGraphQL Subscriptionを実装する方法の
バックエンド実装部分でした。
次回はnuxt.jsでのフロントエンド実装例をご紹介します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語によるWebアプリケーション開発 チャットアプリのその後

本記事について

Go言語によるWebアプリケーション開発のチャットアプリでちょっと遊んでみます
チャットアプリを外部公開する方法と、なりすましについての説明があります。
後者は手元のサーバ以外に実施すると、法に触れる可能性があります。悪用厳禁です

外部公開

ngrokを使用し、指定したポートを外部に公開することができます。
(画像にある通り、セッションは8時間で途切れてしまうみたいなので。実用的ではないですね)

$ ngrok http 8080

image.png
実行すると、URLなどの情報がでてきます。後でhttpの方を使用します。

本の手順でgoogle developer consoleを設定したら、
認証情報のOAuth 2.0 クライアント IDというところを以下の画像のように設定します。
image.png
ここで、2つのURIはngrokで表示されていたものを使用しています。再現する場合は適宜置き換えてください

OAuth 2.0 クライアント IDの画面の右上の方にクライアント IDクライアント シークレットの情報があるので
その情報とngrokのURLを使用し、main.goの53行目、google.New()の中身を書き換えます。

main.go
google.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アカウントを使用してログインすると、チャットができるようになっています。
複数タブでアカウントを変えたり、友達を招待したりして遊べます。
image.png

遊ぶ

このチャットアプリ、名前や画像が自己申告制なので簡単になりすましができてしまいます。
※ 手元のサーバでも、他のユーザがいる状態で実行すると混乱を招く可能性があります。悪用厳禁です

やってみましょう。

デベロッパーツールのコンソールで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="

ブラウザを更新し、発言してみる
image.png

任意のユーザになりますことができた。

最後に

大事な事なので3回書きます。悪用厳禁です

最後まで読んでくださり、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
    • ストレージタイプ毎の使用量/概算コストも表示したい場合に指定

実行例

./s3usage -p prod
normal.png

./s3usage -v -p prod
verbose.png

おわりに

コロナに負けるな!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Go] Realizeで実行ファイルを指定する

Realizeとは

realizeが分からない人はぜひこちらのページをみてください
https://qiita.com/enta0701/items/9f60ad18600acab8c93d

Ralizeでのディレクトリ指定

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.yaml
settings:
  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
      - vendor

methodに記入すると、本来のコマンドと置き換えられて実行されるようになり、
上記ではbuildで実行ファイルを作成、runで実行ファイルを起動しています。

なんとなく気持ち悪いので他にもやり方があるのかもしれませんが、私は分からなかったです!
もっといい方法を知っている人がいたら是非教えて欲しいです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

ブラウザでアクセスすると

pbf1.PNG

適当に埋めて

pbf2.PNG

送信を押すと

pbf3.PNG

内容が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とかフィールド名をそのまま出してしまってるので、別途メッセージカタログ(国際化もできればなお良い)的なものが必要ですね。

ネット上には様々なフォームがあって、その中には技術の粋をつくした一品物から何かのコピペで済ましたい安易なものまで色々とありますが、適当なレベルのところまではこの記事で紹介したようなものを多少改善すれば間に合うのではないかなと考えてます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む