- 投稿日:2020-04-21T20:32:58+09:00
React×GoでTodoリスト作ってみた
基本の復習も兼ねてTodoリストを作りました。
Reactでは、非同期でHTTP通信を行うためにaxiosを使っています。
Goでは、GORMとechoを使っています。React
Todo.jsimport React from 'react'; import InputField from './InputField'; import List from './List'; import http from './http'; class Todo extends React.Component { constructor() { super(); this.state = { text: "", lists: [], } } componentDidMount() { this.getTodoList(); } getTodoList = () => { return http .get('/todo') .then((response) => { this.setState({ lists: response.data }) }) .catch(error => { console.log(error) }) } handleChange = e => { this.setState({ text: e.target.value }) } handleSubmit = () => { if (this.state.text === "") { return window.alert("入力してください") } return http .post('/todo', { text: this.state.text }) .then(() => { this.setState({ text: "" }); this.getTodoList(); } ) .catch(error => { console.log(error) }) } handleDelete = (list) => { return http .delete(`/todo/${list.id}`) .then(() => this.getTodoList() ) .catch(error => { console.log(error) }) } render() { return ( <div className="todo"> <div className="todo-title"> <h1>Todo</h1> </div> <InputField text={this.state.text} handleChange={this.handleChange} handleSubmit={this.handleSubmit} /> <List lists={this.state.lists} handleDelete={this.handleDelete} /> </div> ) } } export default Todo;InputField.jsimport React from 'react'; export default function InputField(props) { return ( <div className="todo-input-field"> <input placeholder="入力しよう" value={props.text} onChange={e => props.handleChange(e)}></input> <button onClick={props.handleSubmit}>保存</button> </div> ) }http.jsimport axios from 'axios'; const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:〇〇〇〇'; const http = axios.create({ baseURL: API_HOST, }); export default http;Go
main.gopackage main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "パスに合わせて他のパッケージをimport" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) const defaultPort = "〇〇〇〇" func port() string { p := os.Getenv("PORT") if p != "" { return ":" + p } return ":" + defaultPort } func main() { connStr := fmt.Sprintf( "%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True", "DBUSER", "DBPASSWORD", "DBPROTOCOL", "DBNAME", ) db, err := gorm.Open("mysql", connStr) if err != nil { log.Fatal(err) } h := handler.New(db) server := &http.Server{ Addr: port(), Handler: h, } go func() { stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) <-stop log.Println("Shutting down...") if err := server.Shutdown(context.Background()); err != nil { log.Println("Unable to shutdown:", err) } log.Println("Server stopped") }() log.Println("Listening on http://localhost" + port()) if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } }handler.gopackage handler import ( "net/http" "github.com/jinzhu/gorm" "github.com/labstack/echo" "github.com/labstack/echo/middleware" ) func New(db *gorm.DB) http.Handler { e := echo.New() e.Use(middleware.CORS()) h := &handler{ DB: db, } e.GET("/health", h.health) e.GET("/todo", h.getTodoLists) e.POST("/todo", h.createTodo) e.DELETE("/todo/:id", h.deleteTodo) return e } type handler struct { DB *gorm.DB } func (h *handler) health(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "OK"}) }list.gopackage handler import ( "net/http" "time" "github.com/labstack/echo" ) type List struct { ID uint `json:"id"` Text string `json:"text"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } func (h *handler) getTodoLists(c echo.Context) error { var lists []List err := h.DB.Find(&lists).Error if err != nil { return err } return c.JSON(http.StatusOK, lists) } func (h *handler) createTodo(c echo.Context) error { var list List err := c.Bind(&list) if err != nil { return err } err = h.DB.Create(&list).Error if err != nil { return err } return c.JSON(http.StatusOK, list) } func (h *handler) deleteTodo(c echo.Context) error { var list List paramID := c.Param("id") err := h.DB.Where("id=?", paramID).Delete(&list).Error if err != nil { return err } return c.JSON(http.StatusOK, list) }本来であれば、type ~ struct 構造体を他のパッケージからインポートするのがベストな気がするのですが、importがうまくできなかったので、list.goにまとめて書きました。
この問題は解決したい、、、DB
todoというデータベースの中にlistsテーブルがあり、中身は画像のようになってます。
Todoリスト
このような形で簡単なTodoリストが作成できると思います。
- 投稿日:2020-04-21T19:21:21+09:00
chromedp で Chrome を見える状態(not headless)で起動して、かつ終了しないようにする
銀行とかのサイトで 1password でもログインできないようなサイトに対して(セキュリティカードでの追加の認証が必要とか)、chromedp を使ってプログラム上から ID/PASS やそのほか認証情報を渡して自動でログインするやり方を思いついた。ブラウザ拡張作るやり方でもいいんだけど。
大昔は C++ で CComPtr 使いながらひたすら QueryInterface して COM で IE 操作してたけど、今となっては Puppeteer/Playwright とか Go なら chromedp とかがあるのでこちらを利用することにした。自動ログインを行うには、Chrome が headless ではない状態(否定語の連続で分かりにくい)で起動し、プログラムの終了後も Chrome が閉じないようにすればいいので、次のようにすることで要件を満たすことができた。
main.gopackage main import ( "context" "log" "github.com/chromedp/chromedp" ) func main() { opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", false), // headless=false に変更 chromedp.Flag("disable-gpu", false), chromedp.Flag("enable-automation", false), chromedp.Flag("disable-extensions", false), chromedp.Flag("hide-scrollbars", false), chromedp.Flag("mute-audio", false), ) // allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) // defer cancel() // ↓に書き換えた allocCtx, _ := chromedp.NewExecAllocator(context.Background(), opts...) // cancel() を呼ばないように変更 // ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf)) // defer cancel() // ↓に書き換えた ctx, _ := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf)) // cancel() を呼ばないように変更 // navigate to a page, wait for an element, click var example string err := chromedp.Run(ctx, chromedp.Navigate(`https://golang.org/pkg/time/`), // wait for footer element is visible (ie, page is loaded) chromedp.WaitVisible(`body > footer`), // find and click "Expand All" link chromedp.Click(`#pkg-examples > div`, chromedp.NodeVisible), // retrieve the value of the textarea chromedp.Value(`#example_After .play .input textarea`, &example), ) if err != nil { log.Fatal(err) } log.Printf("Go's time.After example:\n%s", example) }chromedp.Flag("headless", false) で head モードにしているのと、Context#cancel を呼び出さないようにしているのがポイント。
というのも、Context#cancel のタイミングで Chrome を閉じるようにしているらしい。これを元にいろいろ自動ログインするプログラムを作ろうと思う。
- 投稿日:2020-04-21T19:00:47+09:00
Go言語でハマったことメモ(インターフェース)
Golangを始めました。
GolangはC言語のように、シンプルな文法・データ構造でできているのですが、同時に、生産性を高めるための、高度な概念も取り入られています。
そしてそのために、Golangには若干トリッキーな構文がいくつかあります。
しかし、それらを知らずに、他の言語での先入観や勝手な思い込みで判断してしまって、ハマることがちょいちょいありました。
ここでは、Golangを始めてみて、個人的にハマったことや、勘違いしたことを、トピック別に備忘録としてメモしていこうと思います。
ここでは、「インターフェース(interface)」とは何ぞやについてのメモを記載しました。
インターフェースとは?
Golangの変数は、値だけを持っていて、型情報は持っていません。Golangには、変数の値とその変数の型情報をセットにして保持する、言語組み込みの特殊な構造体があり、その構造体をインターフェースと呼びます。
インターフェースには任意の型の変数を格納できるので、インターフェースはいわゆる汎用の変数容器とも言えます。
インターフェースは何に使うのか?
Golangがインターフェースを導入した目的の一つに、汎用的な関数を書けるようにしたかったというのがあると思います。
Golangは静的型付け言語のため、変数を使う時には、その変数の型が決まっていないといけません。
しかし、汎用的な関数を書こうとした時、それでは未知の型に対しての処理が書けません。
そこで、任意の型の変数を格納できる型、つまりインターフェースを用意して、そのインターフェースに対して処理を書くことにより、汎用的な関数が書けるようにしています。
インターフェースの使い方(空のインターフェース)
インターフェースは汎用容器なので、任意の型の変数を代入できます。このような何でも格納できるインターフェースは、空のインターフェースと呼ばれています。
例えば、下記のインターフェースには「
int
」と「bool
」の両方の型の値を代入できます。var i interface{} a := 123 i = a fmt.Println(i) // 123 b := true i = b fmt.Println(i) // trueただし、インターフェースは構造体で、その構造体の中に値が格納されているので、値を使うには、インターフェースから値を取り出す必要があります。
インターフェースから値を取り出すには
<インターフェース>.(型)
で指定し、これを型アサーションと呼びます。var i1, i2 interface{} i1 = 1 i2 = 2 fmt.Println(i1 + i2) // これはできない fmt.Println(i1.(int) + i2.(int)) // 3インターフェースの仕組み
インターフェースは下記のような、型情報へのポインターと値情報へのポインターからなる、16バイトの構造体です。
type emptyInterface struct { typ *rtype word unsafe.Pointer }インターフェースに値を代入すると、値を格納する領域が割り当てられ、その領域に値が代入され、インターフェース構造体の値領域へのポインターにアドレスがセットされます。
例えば、インターフェースに代入した値を、直接取得することも可能です。
package main import ( "fmt" "unsafe" ) type emptyInterface struct { typ unsafe.Pointer word unsafe.Pointer // ここに代入された値が格納されている } func main() { var i interface{} a := "TEST" i = a p := (*string)(((*emptyInterface)(unsafe.Pointer(&i))).word) fmt.Println(*p) // TEST }インターフェースの注意点
初めてインターフェースを見た時は、汎用ポインターみたいなものかと思ったのですが、違いました。
インターフェースに代入した時にインターフェースに格納されるのは、変数の代入「
=
」で行われるものと同じで、値のコピーです。例えば
int
変数をインターフェースに代入すると、int
変数の値が格納され、代入に使った変数が格納されるわけではありません。同様に、構造体を代入すると、構造体のコピーが格納されるといったように、通常の変数の代入で入るのと同じものが格納されます。代入に使った変数を格納したいのであれば、変数のポインターを代入する必要があります。
var i interface{} a := 123 i = a i.(int) = 0 // これはできない。エラーになる i = &a *i.(*int) = 0 // これはできる。aが書き換わる fmt.Println(a) // 0メソッド種類定義としてのインターフェース
以上が、汎用容器(空のインターフェース)としてのインターフェースの使い方です。
インターフェースにはもう一つ、型がどんなメソッドを持っているかを定義する型としての使い方があります。
インターフェースから直接、格納されている変数の型を特定せずに、変数のメソッドを呼び出すことができます。
それによって、汎用的な関数の中で、インターフェースのまま、変数のメソッドを呼び出すことができます。
ただし、そのためには、どういったメソッドを持ったインターフェースかを、インターフェースとして定義しておく必要があります。
例えば「A Tour of Go」にあるように、
String()
でstring
を返すメソッドを持つインターフェース「Stringer
」を定義して、「Stringer
」を使って文字列を出力する汎用的な関数を書いてみます。type Stringer interface { String() string } func printString(i Stringer){ fmt.Println(i.String()) }任意の構造体を「
Stringer
」にするには、String()
メソッドを定義します。すると、「
Stringer
」インターフェースを使って書いた汎用的な関数printString()
で呼び出されることができます。package main import ( "fmt" ) // 任意の型 type myStruct struct { str string } // 任意の型を Stringer インターフェースに対応する func (o myStruct) String() string { return o.str } // Stringerインターフェース type Stringer interface { String() string } // Stringerインターフェースを使った汎用関数 func printString(i Stringer) { fmt.Println(i.String()) } func main() { a := myStruct{str: "TEST"} printString(a) // TEST }ここで面白いのは、ある型にそのインターフェースを持たせるには、型の定義でインターフェースを取り込む必要はなく、インターフェースで定義されているメソッドと同じものを定義するだけで、そのインターフェースを持つとみなされる点です。
更に面白いのは、任意のインターフェースから、別のインターフェースを取り出すこともできます。
例えば、空のインターフェースから、前述の「
Stringer
」インターフェースを取り出すことができます。func printString(i interface{}) { switch v := i.(type) { case Stringer: fmt.Println(v.String()) default: fmt.Println("null") } }よくあるインターフェースの使われ方
ライブラリやパッケージなどで、インターフェースが公開されていて、そのインターフェースを満たす型を作れば、それに沿った処理をしてくれるといったような使い方です。
例えば前述の「
Stringer
」インターフェースだと、自分で作った構造体にtype Stringer interface { String() string }を満たすメソッドを定義すれば、
fmt.Println()
でその構造体を渡すと、String()
で返す文字列を出力してくれます。もう一つは、汎用容器としての使われ方です。
例えばJSONの解析で、どういった値が入っているか分からない場合は、事前に型を定義することができません。
そういった場合は、インターフェースを渡して、そこに値を格納してもらうといったことができます。
package main import ( "encoding/json" "fmt" ) func main() { jsonStr := "[{\"id\": 123, \"name\": \"TEST\"}]" var i interface{} json.Unmarshal([]byte(jsonStr), &i) fmt.Println(i) // [map[ id:123 name:TEST]] }reflect
インターフェースを使って汎用的な関数を書く場合、インターフェースの値を読み書きすることになるのですが、それらを扱いやすくするパッケージが「pkg/reflect」です。
感想など
インターフェースには、汎用的容器としての役割と、メソッド定義としての役割があって、最初混乱しました。
インターフェースに値を代入することの意味がよく分かってなかったのですが、下記記事でスッキリしました。要は、変数の入れ物(インターフェース)に値を代入してたんですね。
それが分かると、関数の引数にインターフェースを持ってくることは、要は値をインターフェースに代入しているのと同じということが理解できました。
func test(i interface{}){ fmt.Println(i) } test(123) // i = 123 しているまた、インターフェースに変数を代入しても、元の変数の値はインターフェースからは変更できない理由も理解できるようになりました。
元記事
- 投稿日:2020-04-21T13:43:19+09:00
コロナリモートに打ち勝つAWSログイン問題解決方法の提案
はじめに
AWSアカウントのセキュリティを守るため、ログイン時に多要素認証する検討を行いましたが、簡単に多要素認証を実装するには、メンバーの私用スマホにアプリをインストールする必要がありました。私用スマホには入れるのはあまり好ましくないので、アプリをインストールせずにセキュリティ面を強化できると良い!ということで、IPアドレス制限をかけることにしました!
しかし、IPアドレス制限をかけていることで、リモート勤務するにあたり弊害があったので、解決方法を提案します!
状況整理
IAMユーザのIP制限
全てのIAMユーザにIP制限のIAMポリシーがアタッチされているため、決められたIPアドレスからしかログインができません。
アタッチされているIAMポリシーは以下の通りです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "NotIpAddress": { "aws:SourceIp": [ "[オフィスのIPアドレス]/32" ] } } } ] }自宅のIPアドレスが毎日変わる
コロナの影響で自宅勤務になりましたが、ポケットWi-FiのIPアドレスが毎日変わり、IAMポリシーで事前に自宅のIPアドレスを登録することができません。
期間限定
現状はコロナの影響での在宅勤務なので、期限はコロナが落ち着くまで。あんまりコストもかけたくない!
解決方法
IPアドレスを追加する処理とIAMポリシーをお掃除する処理の2つの処理を実行することにしました。
IPアドレス追加処理
自宅から許可したいIPアドレスを持たせてAPI Gatewayを叩きます。そのAPI GatewayをトリガーとしてLambdaを実行し、最新のIAMポリシーに先ほど持たせたIPアドレスを追加します。API Gatewayはフルオープンになっていますが、AWSへログイン時に認証情報が必要なので、そこでセキュリティは担保しています。
ポリシーお掃除処理
毎日IPアドレスが追加されるので、IAMポリシーをお掃除(リセット)するためのLambdaを定期実行させます。S3に保存してあるIPアドレス追加処理前のIAMポリシーオブジェクトを取得し、IAMポリシーを更新します。こちらはCloudWatch Eventsをトリガーとします。
IAMポリシーを更新するときに必要なこと
IAMポリシーは最大5つまでバージョンを保存することができます。コンソール上でIAMポリシーを更新する際には、保存されているバージョンのうち1つ削除することで、5つ以上増えないようになっています。しかし、Lambda関数でIAMポリシーを更新する際には、削除処理を実装しなければいけません。また、デフォルト と表記があるバージョンが使用されるIAMポリシーなので、新しく追加するIAMポリシーをデフォルトに設定する必要があります。
IP制限IAMポリシーの作成
今回は例として
123.123.123.123
のIPアドレスのみを許可したIAMポリシーを作成しました。実際にはこのポリシーをユーザにアタッチしておきます。{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "NotIpAddress": { "aws:SourceIp": [ "123.123.123.123/32" ] } } } ] }IPアドレス追加処理
IP制限IAMポリシーに、許可したいIPアドレスを追加します。
Lambda関数の作成
Lambda関数を作成していきます。
IAMポリシー
AWSにIP制限をかけているIAMポリシーに対しての以下の権限を付与します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "iam:GetPolicyVersion", "iam:ListPolicyVersions", "iam:CreatePolicyVersion", "iam:DeletePolicyVersion" ], "Resource": "[IP制限IAMポリシーarn]" } ] }このポリシーをアタッチしたIAMロールを作成し、Lambdaに紐づけてください。
ソースコード
golangで書いてみました。
ソースコード : https://github.com/Maika995/aws-sourceip/blob/master/add/main.goAPI Gatewayの作成
GETメソッドを作成します。作成方法は下記の記事を参考にさせていただきました。
参考:AWS Lambdaにクエリ文字列(get値)を引数で渡すトリガを設定する前述のLambdaのコードに合わせて、クエリとしてIPアドレスを持たせるため、メソッドリクエストは以下のように設定しました。
そして、統合リクエストのマッピングテンプレートはテンプレートが定義されていない場合を選択し、以下のテンプレートを入力しました。
application/json{ "SourceIp": "$input.params('SourceIp')" }最後にリソースのデプロイを行い、発行されたURLをコピーしておきます。
実行結果
コピーしておいたURLにIPアドレスをクエリとして持たせてブラウザからたたきます。
ブラウザにはLambda関数の戻り値として設定していた文字列が返ってきたので成功したっぽいです。
IAMポリシーを確認すると…
無事先ほど指定したIPアドレスが許可されています!
これで、無事自宅からでもAWSにログインできるようになりました!ポリシーお掃除処理
定期実行でIAMポリシーを元々のIAMポリシーに戻す、お掃除バッチの実装です。
S3の作成
元バージョンのIAMポリシーを保管するために、S3バケットを作成します。パブリックアクセスもすべてブロックしたバケットを作成し、先ほどのIP制限IAMポリシーをJSONファイルにしてアップロードします。
Lambda関数の作成
Lambda関数を作成していきます。
IAMポリシー
IAMポリシーに関する権限に加えて、S3のオブジェクトを取得する権限を付与します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::[バケット名]/[オブジェクト名]" }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "iam:ListPolicyVersions", "iam:CreatePolicyVersion", "iam:DeletePolicyVersion" ], "Resource": "[IP制限IAMポリシーarn]" } ] }このポリシーをアタッチしたIAMロールを作成し、Lambdaに紐づけてください。
ソースコード
golangで書いています。
ソースコード : https://github.com/Maika995/aws-sourceip/blob/master/clean/main.goCloudWatch Events
夜中の0時に定期実行するように設定しました。
そしてお掃除Lambdaに紐付けます。Cron式で設定します。UTCなので-9時間で表記します。
実行結果
0時をすぎて、IAMポリシーがリセットされていることが確認できました!
おわりに
注意事項
1回目の実行時は削除できるIAMポリシーのバージョンがないので、(デフォルトのバージョンは削除できません。)バージョンの削除処理の部分で409エラーが返ってきていますが、その他の処理は普通に実行されます。2回目以降はバージョンが2つ保存されているので、409エラーも出ることなく、古いバージョンが削除されるようになります。
ボツになった施策
第一弾は、以下のようにポリシーのバージョンの中で1番古いバージョン(3番)をお掃除処理に使用するバージョンにしていました。
- 最新ポリシー(デフォルト)
- 2番目に新しいポシリー
- お掃除に使用する、元々のIP制限ポリシー
両方の処理とも2番目に新しいポリシーを削除して更新します。お掃除処理は3つ目のポリシーを取得してそのまま使うようにしていましたが…ある日、リセットされてない!ってことに気づきました。3番のバージョンがなくなっていました。おそらく複数人が利用しているAWSなので、手動でIAMポリシーを変更したなど、無意識に3番のバージョンが削除されてしまったのかなと思います。そういう経緯もあり、いろいろな人が使うものなので、お掃除用のバージョンはS3にファイルを置くことにしました!
より良くするには
上記の設定であれば、API Gatewayはフルオープンでどこからでもたたくことができます。あまり手間をかけずセキュリティ面でより固くするなら、クエリの条件にパスワードとかを追加してみるのもいいかなと思います!あとは、APIキーとかを使ってみるのも良さそう。
感想
現在、在宅勤務なので実際に使っていますが問題なく自宅からAWSにログインができています!
これでAWSログイン問題解決しました!めでたしめでたし!あと、golangを使う機会も少しずつ増えてきて、golangとSDKの苦手意識薄まってきたなあと感じました!