- 投稿日:2020-12-27T23:21:01+09:00
slackにGo言語で、postする
はじめに
外部ライブラリを使用しないで、Slackに投稿する方法が気になったので、自分で組んでみた。
Slackの設定は、Bot Token Scopes
でchat:write
とfiles:write
を有効にする。
そして投稿したいチャンネルにアプリ許可を出しておくこと
test.pngという名の画像ファイルをあらかじめ用意しておくことソースについて
main.gopackage main import ( "bytes" "context" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "strings" "time" ) const ( SLACKPOSTMESSAGE = "https://slack.com/api/chat.postMessage" SLACKPOSTFILE = "https://slack.com/api/files.upload" TOKEN = "{表示されたトークンを記載}" CHANNEL = "general" ) } func postSlackfile() string { filename := "./test.png" file, err := os.Open(filename) if err != nil { return "" } defer file.Close() body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", filepath.Base(filename)) if err != nil { return "" } _, err = io.Copy(part, file) if err != nil { return "" } err = writer.Close() if err != nil { return "" } values := url.Values{ "token": {TOKEN}, } values.Add("channels", CHANNEL) values.Add("filename", filepath.Base(filename)) req, err := http.NewRequest("POST", SLACKPOSTFILE, body) if err != nil { return "" } req = req.WithContext(context.Background()) req.URL.RawQuery = (values).Encode() req.Header.Add("Content-Type", writer.FormDataContentType()) client := &http.Client{} // client.Timeout = time.Second * 15 resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() body2, err := ioutil.ReadAll(resp.Body) if err != nil { return "" } return string(body2) } func postSlackMessage(text string) string { urldata := SLACKPOSTMESSAGE values := url.Values{} values.Set("token", TOKEN) values.Add("channel", CHANNEL) values.Add("text", text) client := &http.Client{} client.Timeout = time.Second * 15 req, err := http.NewRequest("POST", urldata, strings.NewReader(values.Encode())) if err != nil { return "" } // req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "" } return string(body) } func main(){ fmt.Println(postSlackMessage("Hello World\nbbb")) fmt.Println(postSlackfile()) }結果
実行すると以下の通り投稿される。
余談
以下のリンクのライブラリを使用するともっと簡単にできるけど、機能を確認することや理解ならば、上のソースコードだけのほうが楽かな
- 投稿日:2020-12-27T22:09:43+09:00
Goでドコモメール送信
Goでドコモのキャリアメールを送信するサンプル実装
package main import ( "crypto/tls" "encoding/base64" "fmt" "io/ioutil" "log" "net" "net/http" "net/mail" "net/smtp" "strings" "golang.org/x/text/encoding/japanese" "golang.org/x/text/transform" ) func main() { send() } func send() { from := mail.Address{"", "from@docomo.ne.jp"} to := mail.Address{"", "to@gmail.com"} message := createMessage(from, to) servername := "smtp.spmode.ne.jp:465" host, _, _ := net.SplitHostPort(servername) auth := smtp.PlainAuth("", "account_id", "password", host) tlsconfig := &tls.Config{ InsecureSkipVerify: true, ServerName: host, } conn, err := tls.Dial("tcp", servername, tlsconfig) if err != nil { log.Panic(err) } c, err := smtp.NewClient(conn, host) if err != nil { log.Panic(err) } if err = c.Auth(auth); err != nil { log.Panic(err) } if err = c.Mail(from.Address); err != nil { log.Panic(err) } if err = c.Rcpt(to.Address); err != nil { log.Panic(err) } w, err := c.Data() if err != nil { log.Panic(err) } _, err = w.Write([]byte(message)) if err != nil { log.Panic(err) } err = w.Close() if err != nil { log.Panic(err) } err = c.Quit() if err != nil { log.Panic(err) } } func createMessage(from, to mail.Address) string { cc := []string{"hoge+cc1@gmail.com", "hoge+cc2@gmail.com"} delimeter := "**=myohmy689407924327" subj := "サンプルメール" htmlBody := "<html><body><h1>こんにちわ。</h1><br><h3>ワールド</h3></body></html>" textBody := "Hello World" sampleMsg := fmt.Sprintf("From: %s\r\n", from.String()) sampleMsg += fmt.Sprintf("To: %s\r\n", to.String()) if len(cc) > 0 { sampleMsg += fmt.Sprintf("Cc: %s\r\n", strings.Join(cc, ";")) } subject, err := toISO2022JP(subj) if err != nil { log.Panic(err) } sampleMsg += "Subject: " + string(subject) + "\r\n" sampleMsg += "MIME-Version: 1.0\r\n" sampleMsg += fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n", delimeter) if htmlBody != "" { sampleMsg += fmt.Sprintf("\r\n--%s\r\n", delimeter) sampleMsg += "Content-Type: text/html; charset=\"utf-8\"\r\n" sampleMsg += "Content-Transfer-Encoding: base64\r\n" sampleMsg += fmt.Sprintf("\r\n%s", base64.StdEncoding.EncodeToString([]byte(htmlBody))+"\r\n") } else if textBody != "" { sampleMsg += fmt.Sprintf("\r\n--%s\r\n", delimeter) sampleMsg += "Content-Type: text/plain; charset=\"utf-8\"\r\n" sampleMsg += "Content-Transfer-Encoding: base64\r\n" sampleMsg += fmt.Sprintf("\r\n%s", base64.StdEncoding.EncodeToString([]byte(textBody))+"\r\n") } attachmentFile, err := ioutil.ReadFile("./attachment.pdf") if err != nil { log.Panic(err) } sampleMsg += fmt.Sprintf("\r\n--%s\r\n", delimeter) sampleMsg += "Content-Type: " + http.DetectContentType(attachmentFile) + "; charset=\"utf-8\"\r\n" sampleMsg += "Content-Transfer-Encoding: base64\r\n" sampleMsg += "Content-Disposition: attachment;filename=\"" + base64.StdEncoding.EncodeToString([]byte("attachment.pdf")) + "\"\r\n" sampleMsg += "\r\n" + base64.StdEncoding.EncodeToString(attachmentFile) sampleMsg += fmt.Sprintf("\r\n--%s\r\n", delimeter) return sampleMsg } func toISO2022JP(str string) ([]byte, error) { reader := strings.NewReader(str) transformer := japanese.ISO2022JP.NewEncoder() return ioutil.ReadAll(transform.NewReader(reader, transformer)) }
- 投稿日:2020-12-27T22:08:54+09:00
GoでCRC-32Cハッシュでダウンロードファイルの整合性をチェックする
APIのレスポンスからファイルを保存して、CRC32C整合性をハッシュチェックするサンプル実装
func downloadFile(res *http.Response, path string) error { var buf bytes.Buffer tr := io.TeeReader(res.Body, &buf) writeFile(tr, path) if err := writeFile(tr, path); err != nil { return err } if err := checkCRC32c(res, path); err != nil { return err } } func writeFile(r io.Reader, path string) error { dir := filepath.Dir(path) err := os.MkdirAll(dir, os.ModePerm) if err != nil { return errors.Wrapf(err, "failed to create directory: %s", dir) } out, err := os.Create(path) if err != nil { return errors.Wrapf(err, "failed to create file: %s", path) } defer out.Close() if _, err := io.Copy(out, r); err != nil { return errors.Wrapf(err, "failed to write raw file: %s", path) } if err := out.Sync(); err != nil { return errors.Wrapf(err, "failed to sync raw file: %s", path) } return nil } func checkCRC32c(res *http.Response, path string) error { hash, checked := parseCRC32c(res) if !checked { return nil } bytes, err := ioutil.ReadFile(path) if err != nil { return errors.Wrapf(err, "failed to read file: %s", path) } if hash != crc32.Checksum(bytes, crc32cTable) { return errors.Errorf("CRC32C hash is incorrect and the file may be missing: %s", path) } return nil } func parseCRC32c(res *http.Response) (uint32, bool) { const prefix = "crc32c=" for _, spec := range res.Header["X-Goog-Hash"] { if strings.HasPrefix(spec, prefix) { c, err := decodeUint32(spec[len(prefix):]) if err != nil { log.L().Warn("failed to parse CRC32c hash", zap.Error(err)) break } return c, true } } return 0, false } func decodeUint32(b64 string) (uint32, error) { d, err := base64.StdEncoding.DecodeString(b64) if err != nil { return 0, errors.Wrapf(err, "failed to base64 decode string: %s", b64) } if len(d) != 4 { return 0, errors.Errorf("%q does not encode a 32-bit value", d) } return uint32(d[0])<<24 + uint32(d[1])<<16 + uint32(d[2])<<8 + uint32(d[3]), nil }ハッシュと ETag: ベスト プラクティス
- 投稿日:2020-12-27T18:04:16+09:00
Goのnetパッケージを理解する~2日目~
はじめに
この記事は、Goのnetパッケージを理解する~1日目~の二日目の記事です。
今回は、POSTやGETの処理・htmlの出力をしていきます。
ソースコード
main.gopackage main import ( "fmt" "log" "net/http" "text/template" "time" ) type Host struct { Title string Date time.Time Text string } func main() { http.HandleFunc("/", handler) http.HandleFunc("/temp", handlerTemp) http.ListenAndServe(":8080", nil) } func handler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": fmt.Fprint(w, "Hello GET") case "POST": fmt.Fprint(w, "Hello POST") default: fmt.Fprint(w, "Hello") } } func handlerTemp(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles("template/index.html") if err != nil { log.Fatalf("template ERROR") } host := Host{ Title: "テストタイトル", Date: time.Now(), Text: "これは、netパッケージの練習です。", } t.Execute(w, host) }index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> Test Page <form method="POST" action="/"> <button type="submit">POST</button> </form> <p>{{ .Title }}</p> <p>{{ .Date }}</p> <p>{{ .Text }}</p> </body> </html>前回のコードを引用して作成しました。
関数一つ一つ解説していきます。main関数
ここはほぼ前回と同じ。
8080番ポートでサーバの起動をしています。
/temp
ってのを追加したくらいhandler関数
前回とは大きく変更しました。
handler
に飛ばされてくるメソッドをswitch
文で捌いています。この書き方は、同じ関数を引数にとっても、メソッドが異なるため、正常な動作をしてくれます。
(なんとなくかっこいい感じもする)今回は、
/
に対するメソッドを確認したかったので、このように書きました。handlerTemp関数
今回の肝の関数。
ここで、htmlの出力系の処理をしています。まず、変数
t
を宣言して、template.ParseFiles("template/index.html")
を指定してます。これにより、
template
ディレクトリにあるindex.html
を出力しますよーって言っている。
index.html
のコードにもあるように、{{ .hoge }}
みたいにテンプレートを扱っているので、Host
を構造体として作成し、定義している。最後の
t.Execute
でhostを返している実行してみる
/にアクセスしてみる
GETメソッドのため、print文が
Hello GET
になっている/tempにアクセスしてみる
hostの内容が出力されていることがわかる。ここで、POSTボタンで
/
にアクセスしてみる
POSTでアクセスしたので、print文がHello POST
になっている事がわかる。まとめ
Goでなにか作るときは、ginとかechoを使ってアプリケーション開発してたんですが、netパッケージもアプリケーション開発以外にも有効に使えるので、これからも継続的にメモとして書いていきます。
netパッケージ。結構いい感じやん。
- 投稿日:2020-12-27T12:55:08+09:00
LeetCodeに毎日挑戦してみた 141. Linked List Cycle(Python、Go)
Leetcodeとは
leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)
32問目(問題141)
141. Linked List Cycle
問題内容
Given
head
, the head of a linked list, determine if the linked list has a cycle in it.There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the
next
pointer. Internally,pos
is used to denote the index of the node that tail'snext
pointer is connected to. Note thatpos
is not passed as a parameter.Return
true
if there is a cycle in the linked list. Otherwise, returnfalse
.日本語訳
head
リンクリストの先頭であるが与えられた場合、リンクリストにサイクルが含まれているかどうかを判断します。
next
ポインタを継続的にたどることによって再び到達できるノードがリストにある場合、リンクリストにサイクルがあり ます。内部的にpos
は、テールのnext
ポインタが接続されているノードのインデックスを示すために使用 されます。 これpos
はパラメータとして渡されないことに注意し てください。
true
リンクリストにサイクルがある場合に戻り ます。それ以外の場合は、を返しfalse
ます。Example 1:
Input: head = [3,2,0,-4], pos = 1 Output: true Explanation: There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed).Example 2:
Input: head = [1,2], pos = 0 Output: true Explanation: There is a cycle in the linked list, where the tail connects to the 0th node.Example 3:
Input: head = [1], pos = -1 Output: false Explanation: There is no cycle in the linked list.考え方
- 一度に2つノードを進めるfastと1つしか進めないslowを用意します。
- ループ処理をかけて、fastがslowに追いつくか、値がなくなるかまで処理を行います
- 追いついたらTrue,nilになったらFalseです
解答コード
def hasCycle(self, head): slow = fast = head while fast and fast.next: fast = fast.next.next slow = slow.next if slow == fast: return True return False
- Goでも書いてみます!
func hasCycle(head *ListNode) bool { if head == nil || head.Next == nil { return false } p1, p2 := head, head.Next for p1 != p2 { if p2 == nil || p2.Next == nil { return false } p1 = p1.Next p2 = p2.Next.Next } return true }
- 投稿日:2020-12-27T11:33:06+09:00
Go言語でAWS Lambdaの開発をサポートするパッケージを作った
仕事でGo言語 + AWS Lambdaを用いる機会が多く、特にセキュリティ監視関連基盤のバックエンド処理を開発しています(これとかこれとかこれ)。
開発をすすめる中で「こうすると便利だな」というちょっとしたtipsはいろいろあったのですが、あまりに細切れな処理すぎるのでプロジェクト間でコピーするなどして開発に利用していました。とはいえ管理しているプロジェクトが多くなってきたことで挙動がまちまちになってしまったり、ある程度tipsの数が溜まってきたのもあって、パッケージとして切り出してみました。
https://github.com/m-mizutani/golambda
AWSが公式で提供しているPowertools(Python版、Java版)を意識してはいますが、完全に再現する目的では作っていません。また、全てのGo + Lambdaの開発者が「この方法に従うべき!」とも思っていません。例えば、API gatewayによって呼び出されるLambdaは各種Web Application Frameworkで同じような機能がサポートされていることもあり、あまりこのパッケージの恩恵は受けられないと思います。なので「こういう処理をまとめておくと便利」ぐらいな話として見ていただければと思います。
基本的にはデータ処理のパイプラインやちょっとしたインテグレーションなどのためのLambdaを想定しており、以下の4つの機能を実装しています。
- イベントの取り出し
- 構造化ロギング
- エラー処理
- 秘匿値の取得
実装している機能
イベントの取り出し
AWS Lambdaはイベントソースを指定して、そこからの通知をトリガーに起動させることができます。この際、Lambda functionはSQSやSNSといったイベントソースのデータ構造が渡されて起動します。そのため、各種構造データから自分で使うデータを取り出す作業が必要です。
golambda.Start()
という関数にcallback(以下の例ではHandler
)を指定するとgolambda.Event
に必要な情報が格納され、そこから取り出すことができます。package main import ( "strings" "github.com/m-mizutani/golambda" ) type MyEvent struct { Message string `json:"message"` } // SQSのメッセージをconcatして返すHandler func Handler(event golambda.Event) (interface{}, error) { var response []string // SQSのbodyを取り出す bodies, err := event.DecapSQSBody() if err != nil { return nil, err } // SQSはメッセージがバッチでうけとる場合があるので複数件とみて処理する for _, body := range bodies { var msg MyEvent // bodyの文字列をmsgにbind(中身はjson.Unmarshal) if err := body.Bind(&msg); err != nil { return nil, err } // メッセージを格納 response = append(response, msg.Message) } // concat return strings.Join(response, ":"), nil } func main() { golambda.Start(Handler) }このサンプルコードは ./example/deployable ディレクトリにおいてあり、デプロイして実行を試すことができます。
データを取り出す処理を実装している
DecapXxx
という関数とは逆に、データを埋め込む処理をEncapXxx
として用意しています。これによって上記のLambda Functionに対して、以下のようにテストを書くことができます。package main_test import ( "testing" "github.com/m-mizutani/golambda" "github.com/stretchr/testify/require" main "github.com/m-mizutani/golambda/example/decapEvent" ) func TestHandler(t *testing.T) { var event golambda.Event messages := []main.MyEvent{ { Message: "blue", }, { Message: "orange", }, } // イベントデータの埋め込み require.NoError(t, event.EncapSQS(messages)) resp, err := main.Handler(event) require.NoError(t, err) require.Equal(t, "blue:orange", resp) }現状はSQS、SNS、SNS over SQS(SNSをsubscribeしているSQSキュー) の3つをサポートしていますが、後々DynamoDB stream、Kinesis streamも実装しようと考えています。
構造化ロギング
Lambdaの標準的なログ出力先はCloudWatch Logsになりますが、LogsあるいはLogsのビュワーであるInsightsはJSON形式のログをサポートしています。そのため、Go言語標準の
log
パッケージを使うのではなく、JSON形式で出力できるロギングツールを用意するのが便利です。ログの出力形式も含めて、Lambda上でのロギングは概ね要件が共通化されています。多くのロギングツールは出力方法や出力形式について様々なオプションがありますが、Lambda functionごとに細かく設定を変えるということはあまりしません。また出力する内容についてもほとんどの場合はメッセージ+文脈の説明に必要な変数だけで事足りるため、そのような単純化をしたwrapperを
golambda
では用意しました。実際の出力部分では zerolog を利用しています。本当はzerologで作成したロガーをそのまま露出させるというのでも良かったのですが、できることを絞っておいたほうが自分にとってもわかりやすいなと思い、あえてwrapする形にしました。
Logger
というグローバル変数をexportしており、Trace
,Debug
,Info
,Error
というログレベルごとのメッセージを出力できるようにしています。任意の変数を永続的に埋め込めるSet
と、メソッドチェインで値を継ぎ足していけるWith
を用意しています。// ------------ // 一時的な変数を埋め込む場合は With() を使う v1 := "say hello" golambda.Logger.With("var1", v1).Info("Hello, hello, hello") /* Output: { "level": "info", "lambda.requestID": "565389dc-c13f-4fc0-b113-xxxxxxxxxxxx", "time": "2020-12-13T02:44:30Z", "var1": "say hello", "message": "Hello, hello, hello" } */ // ------------ // request ID など、永続的に出力したい変数を埋め込む場合はSet()を使う golambda.Logger.Set("myRequestID", myRequestID) // ~~~~~~~ snip ~~~~~~ golambda.Logger.Error("oops") /* Output: { "level": "error", "lambda.requestID": "565389dc-c13f-4fc0-b113-xxxxxxxxxxxx", "time": "2020-11-12T02:44:30Z", "myRequestID": "xxxxxxxxxxxxxxxxx", "message": "oops" } */また、CloudWatch Logsはログの書き込みに対する料金が比較的高価であり、詳細なログを常に出力しているとコストに大きく影響します。そのため通常は最低限のログだけを出力し、トラブル対応やデバッグの時だけ詳細な出力ができるようにしておくと便利です。
golambda
ではLOG_LEVEL
環境変数を設定することで、ログ出力レベルを外部からいじることができるようにしています。(環境変数だけならAWSコンソールなどから容易に変更可能なため)エラー処理
AWS Lambdaはfunctionごとになるべく単機能になるよう実装し、複雑なワークフローを実現する場合にはSNS、SQS、Kinesis Stream、Step Functionsなどを使って複数のLambdaを組み合わせるようにしています。そのため処理の途中でエラーが起きた場合はLambdaのコード内で無理にリカバリしようとせず、なるべく素直にそのままエラーを返すことで外部からの監視で気づきやすくなったり、Lambda自身のリトライ機能の恩恵を受けやすくなったりします。
一方でLambda自身はエラーをあまり丁寧に処理してくれるわけではないので、自前でエラー処理を用意する必要があります。 先述したとおり、Lambda functionは何かあった場合はそのままエラーを返して落ちる、という構成にしておくと便利です。なので、殆どのケースにおいてエラーが発生した場合はメインの関数(後述する例だと
Handler()
)がエラーを返した場合に一通りまとめてエラーに関する情報を出力してくれると、あちこちのエラー発生箇所でログを出力したりどこかへエラーを飛ばすという処理を書く必要がなくなります。
golambda
では、golambda.Start()
で呼び出した主に以下の2つのエラー処理をしています。
golambda.NewError
あるいはgolambda.WrapError
で生成したエラーの詳細なログの出力- エラー監視サービス(Sentry)へエラーを送信
それぞれ詳しく説明します。
エラーの詳細なログ出力
経験上、エラーが起きたときにデバッグのため知りたいのは大きく分けて「どこで起きたのか」「どのような状況で起きたのか」の2つです。
どこでエラーが起きたのかを知る方法としては、
Wrap
関数を使いコンテキストを追記していく、あるいは github.com/pkg/errors パッケージのようにスタックトレースを持つ、などの戦略があります。Lambdaの場合、なるべく単純な処理になるよう実装する方針であれば、ほとんどの場合はスタックトレースでエラー発生箇所とどのように発生したかを知ることができます。また、エラーの原因となった変数の中身を知ることでエラーの再現条件を把握できます。これはエラーが発生したら関連しそうな変数を都度ログ出力することでも対応できますが、出力行が複数にわたってログの見通しが悪くなってしまいます(特に呼び出しが深い場合)。また、単純にログ出力のコードを繰り返し書かねばならず冗長になり、単純に書くのも大変だしログ出力に関する変更をしたいときに面倒です。
そこで、
golambda.NewError()
あるいはgolambda.WrapError()
1で生成したエラーは、With()
という関数でエラーに関連する変数を引き回せるようにしました。実体は中にmap[string]interface{}
の変数にkey/valueの形で格納しているだけです。golambda.NewError()
あるいはgolambda.WrapError()
によって生成されたエラーをメインロジック(以下の例のHandler()
)が返すと、With()
によって格納した変数と、エラーが生成された関数のスタックトレースをCloudWatch Logsに出力します。以下、コードの例です。package main import ( "github.com/m-mizutani/golambda" ) // Handler is exported for test func Handler(event golambda.Event) (interface{}, error) { trigger := "something wrong" return nil, golambda.NewError("oops").With("trigger", trigger) } func main() { golambda.Start(Handler) }これを実行すると、以下のように
error.values
の中にWith
で格納した変数が、error.stacktrace
にスタックトレースが含まれるログが出力されます。スタックトレースは github.com/pkg/errors の%+v
フォーマットでもテキストで出力されますが、構造化ログの出力に合わせてJSON形式に対応しているのもポイントです。{ "level": "error", "lambda.requestID": "565389dc-c13f-4fc0-b113-f903909dbd45", "error.values": { "trigger": "something wrong" }, "error.stacktrace": [ { "func": "main.Handler", "file": "xxx/your/project/src/main.go", "line": 10 }, { "func": "github.com/m-mizutani/golambda.Start.func1", "file": "xxx/github.com/m-mizutani/golambda/lambda.go", "line": 127 } ], "time": "2020-12-13T02:42:48Z", "message": "oops" }エラー監視サービス(Sentry)へエラーを送信
Sentryでないといけない理由は特にないのですが、APIに限らずLambda functionもWebアプリケーションなどと同様に何らかのエラー監視サービスを使うのが望ましいです。理由は以下のようなものがあります。
- CloudWatch Logsにデフォルトで出力されるログからは正常終了したか異常終了したかの判定ができないため、異常終了した実行のログだけ抽出するというのが難しい
- CloudWatch Logsではエラーをグルーピングするような機能はないため、エラー100件のうち1件だけ種類の違うエラーがある、みたいなやつを見つけ出すのが難しい
両方ともエラーログの出力方法を工夫することである程度解決できなくはないですが、色々気をつけて実装しないとならないため素直にエラー監視サービスを使うのがオススメです。
golambda
ではSentryのDSN (Data Source Name) を環境変数SENTRY_DSN
として指定することでメインロジックが返したエラーをSentryに送信します(Sentry + Goの詳細)。送るのはどのエラーでも問題ありませんが、golambda.NewError
やgolambda.WrapError
で生成したエラーは github.com/pkg/errors と互換性のあるStackTrace()
という関数を実装しているため、スタックトレースがSentry側にも表示されます。これはCloudWatch Logsに出力されるものと同じですが、Sentry側の画面でも確認できるため「通知を見る」→「Sentryの画面を見る」→「CloudWatch Logsでログを検索し詳細を確認する」の2番目のステップでエラーの見当をつけられる場合もあります。あとCloudWatch Logsの検索はまあまあもっさりしているので、検索しないですむならそのほうがよい、というのもあります…。
ちなみにSentryにエラーを送信すると
error.sentryEventID
としてSentryのevent IDをCloudWatch Logsのログに埋め込むので、Sentryのエラーから検索ができるようになっています。秘匿値の取得
Lambdaでは実行環境によって変更するようなパラメータは環境変数に格納して利用することが多いです。個人で使っているAWSアカウントであれば全て環境変数に格納するでよいのですが、複数人で共有して使うようなAWSアカウントでは秘匿値と環境変数を分離しておくことで、Lambdaの情報のみを参照できる人(あるいはRole)と秘匿値も参照できる人(あるいはRole)を分離することができます。これは個人で使っていても真にヤバい情報をあつかうのであれば何らかのアクセスキーが漏れても即死しないように権限を分離しておくケースもあるかもしれません。
自分の場合は権限を分離するため、AWS Secrets Manager を利用することが多いです2。Secrets Managerからの値の取り出しはAPIを呼び出せば比較的簡単ではあるのですが、それでも同じような処理を100回くらい書いて飽きたのでモジュール化しました。構造体のフィールドに
json
メタタグをつければそれで値が取得できます。type mySecret struct { Token string `json:"token"` } var secret mySecret if err := golambda.GetSecretValues(os.Getenv("SECRET_ARN"), &secret); err != nil { log.Fatal("Failed: ", err) }実装しなかった機能
便利そうかなと思いつつ、実装を見送ったものたちです。
- タイムアウト直前に任意の処理を実行:Lambdaは設定された最大実行時間をすぎると無言で死ぬためパフォーマンス分析の情報を出すためにタイムアウト直前に何らかの処理を呼び出すというテクニックがあります。ただ自分の場合は Lambda function がタイムアウトによって無言で死んで困った経験がほとんどないので、なんか便利そうと思いつつ特に手を付けませんでした。
- Tracing:Pythonの Powertools では、アノテーションなどを使ってAWS X-Rayでパフォーマンス計測するための機能が提供されています。Goでこれをやろうとすると現状だと普通に公式SDKを使う以上に楽ができる方法があまり浮かばなかったので、特に取り組みませんでした。
まとめ
ということで、Go言語でのLambda実装における自分なりのベストプラクティスのまとめと、それをコード化したものの紹介でした。冒頭に書いたとおりあくまで自分が必要だったものを作っただけなので、万人に使えるものではないかなと思いますが、なにかの参考になれば幸いです。
こういったエラー生成のメソッドは
errors.New()
やerrors.Wrap()
などとする習わしがあるかと思いますが、個人的にはどのパッケージを使っているのか直感的にわかりにくくなるので、あえて命名法則を変えました。 ↩他にも AWS Systems Manager Parameter Store に秘匿値を入れるというやり方もあります。RDSパスワードなどのローテーション機能がある Secrets Manager の方がサービスの思想としては適切かと思い、個人的にはそちらを使っています。ただコストやAPIレートリミットも違ったりするので、本来であれば要件によって使い分けたほうが良さそうです。 ↩
- 投稿日:2020-12-27T03:29:32+09:00
goのechoでファイルへログを出力できるようにする
fp, err := os.OpenFile("logs/debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Output: fp, }))