金とプラチナの価格をGoとMackerelで可視化する
はじめに
今回は、Goでスクレイピングして金とプラチナの価格をMackerelで可視化します。
yhidetoshi.hatenablog.com こちらと同様にMackerelを使った可視化です。
前から金の積立をしているので価格が気になるのもあり、Mackerelでグラフ化しておきたいなー。という気持ちで。
前回、goqueryを使ってスクレイピングしたときの記事はこちらです。
アーキテクチャ
- CloudWatch EventでLambdaを定期実行
- Lmabda ( Go ) でサイトにスクレイピングでデータを取得
- Lambda ( Go ) でMackerelにメトリクスを投げる
Lambda ( Go )について
LambdaにはサイトをスクレイピングしてMackerelにメトリクスをPostする処理を定義して CloudWatch Eventを連携させます。Lambda関数やCloudwatchEventなどはServerlessFrameworkを利用してデプロイしました。
スクレイピングは goquery
を利用しました。
*) Go: 1.12.4
- main.go
package main import ( "fmt" "os" "strconv" "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/aws/aws-lambda-go/lambda" "github.com/mackerelio/mackerel-client-go" ) var ( targetUrl = "https://gold.tanaka.co.jp/commodity/souba/index.php" mkrKey = os.Getenv("MKRKEY") client = mackerel.NewClient(mkrKey) ) const ( serviceName = "Metal" timezone = "Asia/Tokyo" offset = 9 * 60 * 60 ) func main() { lambda.Start(Handler) } // func main() { func Handler() { var goldRetailTax, goldPurchaseTax, platinumRetailTax, platinumPurchaseTax string doc, err := goquery.NewDocument(targetUrl) if err != nil { fmt.Println(err) } // Fetch gold and platinum price doc.Find("#metal_price_sp").Each(func(_ int, s *goquery.Selection) { // Gold goldRetailTax = s.Children().Find("td.retail_tax").First().Text() goldPurchaseTax = s.Children().Find("td.purchase_tax").First().Text() // Platinum platinumRetailTax = s.Children().Find("td.retail_tax").Eq(1).Text() platinumPurchaseTax = s.Children().Find("td.purchase_tax").Eq(1).Text() }) // Format strGoldRetailTax := strings.Replace(goldRetailTax[0:5], ",", "", -1) strGoldPurchaseTax := strings.Replace(goldPurchaseTax[0:5], ",", "", -1) strPlatinumRetailTax := strings.Replace(platinumRetailTax[0:5], ",", "", -1) strPlatinumPurchaseTax := strings.Replace(platinumPurchaseTax[0:5], ",", "", -1) // Convert string to int intGoldRetailTax, _ := strconv.Atoi(strGoldRetailTax) intGoldPurchaseTax, _ := strconv.Atoi(strGoldPurchaseTax) intPlatinumRetailTax, _ := strconv.Atoi(strPlatinumRetailTax) intPlatinumPurchaseTax, _ := strconv.Atoi(strPlatinumPurchaseTax) jst := time.FixedZone(timezone, offset) nowTime := time.Now().In(jst) mkrErr := PostValuesToMackerel(intGoldRetailTax, intGoldPurchaseTax, intPlatinumRetailTax, intPlatinumPurchaseTax, nowTime) if mkrErr != nil { fmt.Println(mkrErr) } } // PostValuesToMackerel Post Metrics to Mackerel func PostValuesToMackerel(goldRetailTax int, goldPurchaseTax int, platinumRetailTax int, platinumPurchaseTax int, nowTime time.Time) error { // Post Gold metrics errGold := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{ &mackerel.MetricValue{ Name: "Gold.retail_tax", Time: nowTime.Unix(), Value: goldRetailTax, }, { Name: "Gold.purchase_tax", Time: nowTime.Unix(), Value: goldPurchaseTax, }, }) if errGold != nil { fmt.Println(errGold) } // Post Platinum metrics errPlatinum := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{ &mackerel.MetricValue{ Name: "Platinum.retail_tax", Time: nowTime.Unix(), Value: platinumRetailTax, }, { Name: "Platinum.purchase_tax", Time: nowTime.Unix(), Value: platinumPurchaseTax, }, }) if errPlatinum != nil { fmt.Println(errPlatinum) } return nil }
デプロイ
- Mackerelにサービスを登録する
export MKRKEY=XXX curl -X POST https://api.mackerelio.com/api/v0/services \ -H "X-Api-Key: ${MKRKEY}" \ -H "Content-Type: application/json" \ -d '{"name": "Metal", "memo": "metal"}'
- main.goをコンパイルする
make build
- ServerlessFrameworkでデプロイする
sls deploy --aws-profile <PROFILE> --mkrkey ${MKRKEY}
ソースコード等はこちらにおいています。 ( Makefileや ServelessFrameworkのyamlファイルなども置いています )
結果 ( Mackerelの画面 )
さいごに
今回はGoで貴金属の価格が掲載されているサイトをgoqueryを利用してスクレイピングし、MackerelにメトリクスをPostすることで可視化しました。 次はこの貴金属の価格をjsonで返すAPIをGoで作りたいと思います。
GoogleAnalyticsのPV数をGoとMackerelで可視化する
はじめに
今回はGoとMackerelを使ってGoogleAnalyticsのデータ(PV数)をMackerelにカスタムメトリクスとして投稿・可視化について書いていきます。
アーキテクチャ
- CloudWatch EventでLambdaを定期実行
- Lmabda ( Go ) でGoogleAnalyticsにAPIでデータを取得
- Lambda ( Go ) でMackerelにメトリクスを投げる
Google Cloud Platformについて
GoogleAnalyticsのAPIを有効にして、認証キーを作成します。
この鍵ファイルを使ってAPIを実行します。
GoogleAnalyticsについて
今回はGoogleAnalyticsの初期設定は完了している状態で進めます。
GCPで作成したサービスアカウントをGoogleAnalyticsに登録します。
ビューIDを確認する。
Lambda ( Go )について
LambdaにはGoogleAnalyticsにAPIでデータを取得してMackerelにメトリクスをPostする処理を定義して CloudWatch Eventを連携させます。Lambda関数やCloudwatchEventなどはServerlessFrameworkを利用してデプロイしました。
GoでGoogleAnalyticsのデータ取得にはこちらのパッケージを利用しました。
そして、Mackerelに取得したデータをメトリクスとして投稿にするところはこちらを利用!
*) Go: 1.12.4
- main.go
package main import ( "context" "fmt" "os" "strconv" "time" "github.com/aws/aws-lambda-go/lambda" "github.com/mackerelio/mackerel-client-go" "golang.org/x/oauth2/google" "google.golang.org/api/analytics/v3" "google.golang.org/api/option" ) var ( mkrKey = os.Getenv("MKRKEY") client = mackerel.NewClient(mkrKey) json = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON") viewID = os.Getenv("VIEW_ID") ) const ( // GA startDate = "today" endDate = "today" metricsUsers = "users" metricsPVs = "pagePath!=/preview" metricsPageViews = "pageviews" // Mackerel serviceName = "GoogleAnalytics" // Lambda timezone = "Asia/Tokyo" offset = 9 * 60 * 60 ) func main() { lambda.Start(Handler) } // Handler Lambda func Handler() { jst := time.FixedZone(timezone, offset) nowTime := time.Now().In(jst) ctx := context.Background() jwtConfig, err := google.JWTConfigFromJSON([]byte(json), analytics.AnalyticsReadonlyScope) if err != nil { fmt.Println(err) } ts := jwtConfig.TokenSource(ctx) client, err := analytics.NewService(ctx, option.WithTokenSource(ts)) // Get Users resUsers, err := client.Data.Ga.Get("ga:"+viewID, startDate, endDate, "ga:"+metricsUsers).Do() if err != nil { fmt.Println(err) } // Get PVs resPVs, err := client.Data.Ga.Get( "ga:"+viewID, startDate, endDate, "ga:"+metricsPageViews).Filters("ga:" + metricsPVs).Do() if err != nil { fmt.Println(err) } intResultUsers, _ := strconv.Atoi(resUsers.TotalsForAllResults["ga:"+metricsUsers]) intResultPVs, _ := strconv.Atoi(resPVs.TotalsForAllResults["ga:"+metricsPageViews]) mkrErr := PostValuesToMackerel(intResultUsers, intResultPVs, nowTime) if err != nil { fmt.Println(mkrErr) } } // PostValuesToMackerel Post Metrics to Mackerel func PostValuesToMackerel(intResultUsers int, intResultPVs int, nowTime time.Time) error { // Post users errUser := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{ &mackerel.MetricValue{ Name: "Users.users", Time: nowTime.Unix(), Value: intResultUsers, }, }) if errUser != nil { fmt.Println(errUser) } // Post PV errPV := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{ &mackerel.MetricValue{ Name: "PV.PVs", Time: nowTime.Unix(), Value: intResultPVs, }, }) if errPV != nil { fmt.Println(errPV) } return nil }
認証ファイル(json)のセット方法
序盤でGoogleAnalyticsの認証に使うjsonファイルを生成しました。環境変数でFilePathを指定して実行してもいいみたいです。( 下記リンクに記載があります )
今回は認証ファイル(json)を環境変数に中身をセットして読み込む方法にしました。 ( Lambdaの環境変数にkey/valueでセットしたかったので )
以下、コードの一部抜粋です。
- main.go
jwtConfig, err := google.JWTConfigFromJSON([]byte(json), analytics.AnalyticsReadonlyScope) if err != nil { fmt.Println(err) } ts := jwtConfig.TokenSource(ctx) client, err := analytics.NewService(ctx, option.WithTokenSource(ts))
WTConfigFromJSON
の定義 ( google.go )
func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { var f credentialsFile if err := json.Unmarshal(jsonKey, &f); err != nil { return nil, err } if f.Type != serviceAccountKey { return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey) } scope = append([]string(nil), scope...) // copy return f.jwtConfig(scope), nil }
credentialsFile
の定義 ( google.go )
// credentialsFile is the unmarshalled representation of a credentials file. type credentialsFile struct { Type string `json:"type"` // serviceAccountKey or userCredentialsKey // Service Account fields ClientEmail string `json:"client_email"` PrivateKeyID string `json:"private_key_id"` PrivateKey string `json:"private_key"` TokenURL string `json:"token_uri"` ProjectID string `json:"project_id"` // User Credential fields // (These typically come from gcloud auth.) ClientSecret string `json:"client_secret"` ClientID string `json:"client_id"` RefreshToken string `json:"refresh_token"` }
→ 認証ファイル(json)をパースして利用しています。
- 参考
認証ファイルを環境変数にセットできるように加工
そのままだとjsonの中身をbashで exportで環境変数にセットできないので以下のように修正しました。
変更パターン
"
-->\"
\n
-->"'\n'"
変更前 ( 一部抜粋 )
"type": "service_account", "private_key": "-----BEGIN PRIVATE KEY-----\n"
- 変更後 ( 一部抜粋 )
\"type\": \"service_account\", \"private_key\": \"-----BEGIN PRIVATE KEY-----"'\n'"\"
- こちらを参考にさせていただきました 🙏
GoogleAnalyticsから欲しい情報を取得する
今回取得するのは、今日のPV数です。クエリパラメータは以下のようにセットしました。 取得したデータに合わせて、クエリパラメータのページを参考に書けばいいかと。
( metricsPVs = "pagePath!=/preview"
ではてブロで執筆中に確認するプレビューはカウントしないようにしています )
- main.go ( 一部抜粋 )
const ( // GA startDate = "today" endDate = "today" metricsUsers = "users" metricsPVs = "pagePath!=/preview" metricsPageViews = "pageviews" dimensionsTitle = "pageTitle" dimensionsPath = "pagePath" // Mackerel serviceName = "GoogleAnalytics" // Lambda timezone = "Asia/Tokyo" offset = 9 * 60 * 60 ) // Get PVs resPVs, err := client.Data.Ga.Get( "ga:"+viewID, startDate, endDate, "ga:"+metricsPageViews).Filters("ga:" + metricsPVs).Do()
以下のページにクエリパラメータについて記載されています。
- 参考 github.com
Lambdaの定期実行
CloudWatch Eventを利用して定期実行させます。今回は15分ごとに実行するようにしました。
Mackerelにメトリクスを投稿する
GoでMackerelにメトリクスを投稿する方法は以前のブログに書いているのでよければ参照ください!
今回書いたMackerelにメトリクスをPostするコードは以下の通りです。
- main.go ( 一部抜粋 )
// PostValuesToMackerel Post Metrics to Mackerel func PostValuesToMackerel(intResultUsers int, intResultPVs int, nowTime time.Time) error { // Post users errUser := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{ &mackerel.MetricValue{ Name: "Users.users", Time: nowTime.Unix(), Value: intResultUsers, }, }) if errUser != nil { fmt.Println(errUser) } // Post PV errPV := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{ &mackerel.MetricValue{ Name: "PV.PVs", Time: nowTime.Unix(), Value: intResultPVs, }, }) if errPV != nil { fmt.Println(errPV) } return nil }
- Mackerelの画面 ( 実行結果 )
デプロイ
- 環境変数をセットする
export MKRKEY=XXX export GOOGLE_APPLICATION_CREDENTIALS_JSON="{...}"
- Mackerelにサービスを作成する
- 名前は
GoogleAnalytics
で作成する
- 名前は
curl -X POST https://api.mackerelio.com/api/v0/services \ -H "X-Api-Key: ${MKRKEY}" \ -H "Content-Type: application/json" \ -d '{"name": "GoogleAnalytics", "memo": "google analytics"}'
- goをコンパイルする
make build
- ServerlessFrameworkでデプロイする
sls deploy --aws-profile <PROFILE> --viewid <VIEW-ID> --mkrkey ${MKRKEY} --google-apikey ${GOOGLE_APPLICATION_CREDENTIALS_JSON}
- ソースコード等はこちらにおいています。 ( Makefileや ServelessFrameworkのyamlファイルなども置いています ) github.com
さいごに
Goで利用してGoogleAnalyticsのデータをMackerelのメトリクスとして投稿・可視化をしました。 今回はじめて Google Cloud Servicesに接続するためのライブラリを利用したので、これから活用していくいいきっかけになりました。 他にも個人的に可視化したいメトリクスがあり、最近ありがたいことにアンバサダプランになったとこもあるので、どんどん活用していこうと思います!
Mackerelアンバサダーを拝命しました!
この度、嬉しいことにMackerelアンバサダーを拝命しました!!
Mackerelアンバサダーとは?
Mackerelとの出会いとツール等作成のきっかけ
Mackerelとの出会いは、前職の先輩社員に "よさそうな監視サービスあるけど話聞いてみる??" というのがきっかけで、 はてな CREのa-knowさんに会社に来ていただいてMackerelを丁寧にアツくご紹介していただいたのがはじまりでした(ともて印象に残っています)!
当時、私はGoの勉強をはじめていて、MackerelはGo製で管理画面は直感的に使いやすかったことや、エージェントインストールの容易さ、 mkrコマンドで監視設定を変更できるなど、すごく興味をもったことを覚えています。 幸運なことに、前職のシステム監視に導入することができました。 当時のMackerel利用は標準的に監視して、設定をCLIで変更したりする程度でした。
ありがたいことに現職でもMackerelを利用しています。そのなかで、継続的にGoの勉強を進めているとGoとMackerelをテーマに何か作りたいという気持ちに。 運用をより楽にしたり、メトリクスをMackerelで可視化することができればGoの勉強になるし運用改善になると思いはじめてツールやプラグインを作成したりするようになりました。
つくったもの
- Billing情報を可視化するプラグイン
- アラート時の高負荷プロセス通知
- 退役せずに不要に残ったインスタンスを通知
- 監視設定の自動化(効率化)
- 独自CLI
- Mackerelでセンサー情報の可視化
- カスタムメトリクスを可視化
OSSへのコミット
Mackerelツールを開発する中で mackerel-client-go を活用する機会が多くなり、欲しい機能が未実装だったので このOSSにPull Requestを出してコミットすることができました。
- GitHub Pull Request ( mackerel-client-go )
最後に
Mackerelを通じてGoを勉強しつつMackerelのプラグインやツール等を作成、最近は mackerel-client-go にコミットする事ができました。 そして、嬉しいことに先日にMackerelのアンバサダーを拝命することができました!!(ノベルティをいただけるとの事なのでとても楽しみしてます!!) 個人的には楽しくやっていた事がこのような結果となって返ってきたことがとても嬉しく良い経験になりました!(多謝)
GuardDutyの結果をLambda ( Go ) でSlackに通知する
はじめに
AWSのセキュリティ対策のひとつにGuardDutyというサービスがあります。
現時点では30日間の無料トライアルがあるので、一度有効にしてみて、1ヶ月でどれくらいコストがかかるのかを確認することができます。
今回はこの脅威検出サービスで検知した結果を少しカスタマイズしてSlackに通知させます。
Slack通知を設定
アーキテクチャ
通知結果
今回は、GuardDutyにサンプル生成した結果をSlack通知させました。
- 高 ( GetFindingsの結果 severity パラメータの値は 7.0〜8.9 の範囲 )
- 中 ( GetFindingsの結果 severity パラメータの値は 4.0〜6.9 の範囲 )
- 低 ( GetFindingsの結果 severity パラメータの値は 0.1〜3.9 )
- ※ ) 値 0 と 9.0 から 10.0 が将来使用するために現在予約されています。
このseverity
の値 ( 高・中・低 ) によってSlack通知の色分けをしました。
今回、以下のように脅威度によって配色しました。
color
- 高:
danger
- 中:
warning
- 低:
#0000ff
- 高:
slack通知結果
CloudWatchの設定
- イベントバターン
- サービス名:
GuardDuty
- イベントタイプ:
GuardDuty Finding
- ターゲット: Lambda関数を選択して関数名を指定
serverlessFrameworkでデプロイしていますが、コンソール画面での設定ものせておきます。
Lambda関数
serverlessFrameworkでデプロイします。ソースコードはのちほど記載します。 通知のjsonをパースしてSlackに通知するGoのコードは以下の通りです。
cloudwatchに送信されたGuardDutyのjsonをパースするために調べて必要なところだけ加工したjsonが以下です。
- json全体をログ出力。
err := json.Unmarshal([]byte(event.Detail), gd) fmt.Println(string([]byte(event.Detail))) if err != nil { fmt.Println(err) }
- CloudWatchLogsでログを確認 (必要なところだけ抽出)
{ "schemaVersion": "2.0", "accountId": "XXX", "region": "ap-northeast-1", "type": "UnauthorizedAccess:EC2/TorRelay", "resource": { "resourceType": "Instance", "instanceDetails": { "instanceId": "i-99999999", "instanceType": "m3.xlarge" } }, "title": "EC2 instance i-99999999 is communicating with Tor Exit node.", "description": "EC2 instance i-99999999 is communicating with IP address 198.51.100.0 on the Tor Anonymizing Proxy network." }
{ "schemaVersion": "2.0", "accountId": "XXX", "region": "ap-northeast-1", "type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration", "resource": { "resourceType": "AccessKey", "accessKeyDetails": { "accessKeyId": "GeneratedFindingAccessKeyId", "principalId": "GeneratedFindingPrincipalId", "userType": "IAMUser", "userName": "GeneratedFindingUserName" } }, "severity": 8, "title": "Credentials for instance role GeneratedFindingUserName used from external IP address.", "description": "Credentials created exclusively for an EC2 instance using instance role GeneratedFindingUserName have been used from external IP address 198.51.100.0." }
これをもとにgoのコードを作成。
- main.go
package main import ( "encoding/json" "fmt" "os" "strconv" "strings" "github.com/ashwanthkumar/slack-go-webhook" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" ) const ( version = "0.0.1" region = "ap-northeast-1" ) var ( // USERNAME username of slack USERNAME = "GuardDutyAlert" // SLACKURL webhookurl of slack SLACKURL = os.Getenv("SLACKURL") notPostThreshold = os.Getenv("THRESHOLD") // add excluded account name: []string{"dev"} excludeAccountList = []string{"dev"} config = aws.Config{Region: aws.String(region)} svcIAM = iam.New(session.New(&config)) ) // GuardDutyFindings set guardduty GuardDutyFindingsValue type GuardDutyFindings struct { AccountID string `json:"accountId"` Region string `json:"region"` Type string `json:"type"` Severity json.Number `json:"severity"` Title string `json:"title"` Description string `json:"description"` Resource Resource `json:"resource"` } // Resource set guardduty ResourceValue type Resource struct { ResourceType string `json:"resourceType,omitempty"` UserName string `json:"userName,omitempty"` InstanceDetails InstanceDetails `json:"instanceDetails,omitempty"` AccessKeyDetails AccessKeyDetails `json:"accessKeyDetails,omitempty"` } // InstanceDetails set guardduty value type InstanceDetails struct { InstanceID string `json:"instanceId,omitempty"` InstanceType string `json:"instanceType,"` } // AccessKeyDetails set guardduty AccessKeyDetailsValue type AccessKeyDetails struct { UserName string `json:"userName,omitempty"` } func main() { lambda.Start(Handler) } // Handler get value from cloudwatch event func Handler(event events.CloudWatchEvent) (events.CloudWatchEvent, error) { var resource string postFlag := true gd := &GuardDutyFindings{} err := json.Unmarshal([]byte(event.Detail), gd) if err != nil { fmt.Println(err) } // cast to float64 float64Severity, err := gd.Severity.Float64() slackColor := CheckSeverityLevel(float64Severity) float64NotPostThreshold, err := strconv.ParseFloat(notPostThreshold, 64) // get aws account name accountAliasName := FetchAccountAlias() // Check excluded List for i := range excludeAccountList { if strings.Contains(accountAliasName, excludeAccountList[i]) { postFlag = false } } // Set affected resource if gd.Resource.InstanceDetails.InstanceID != "" { resource = gd.Resource.InstanceDetails.InstanceID } else if gd.Resource.AccessKeyDetails.UserName != "" { resource = gd.Resource.AccessKeyDetails.UserName } else { resource = "unknown" } // Post slack if postFlag == false && float64Severity < float64NotPostThreshold { fmt.Println("Do not post slack") } else { PostSlack(slackColor, gd.Title, accountAliasName, string(gd.Severity), resource, gd.Type, gd.Description) } return event, err } // CheckSeverityLevel fix the color func CheckSeverityLevel(severity float64) string { var color string if severity == 0.0 { color = "good" } else if (0.1 <= severity) && (severity <= 3.9) { color = "#0000ff" } else if (4.0 <= severity) && (severity <= 6.9) { color = "warning" } else { color = "danger" } return color } // FetchAccountAlias get account alias name func FetchAccountAlias() string { var accountAlias string params := &iam.ListAccountAliasesInput{} res, err := svcIAM.ListAccountAliases(params) if err != nil { fmt.Println(err) } if res.AccountAliases == nil { accountAlias = "None" } else { accountAlias = *res.AccountAliases[0] } return accountAlias } // PostSlack post slack result func PostSlack(slackColor string, title string, accountAliasName string, severity string, resource string, reason string, description string) { field0 := slack.Field{Title: "Title", Value: "_" + title + "_"} field1 := slack.Field{Title: "Account", Value: accountAliasName} field2 := slack.Field{Title: "Severity", Value: severity} field3 := slack.Field{Title: "Affected Resource", Value: resource} field4 := slack.Field{Title: "Type", Value: reason} field5 := slack.Field{Title: "Description", Value: "```" + description + "```"} attachment := slack.Attachment{} attachment.AddField(field0).AddField(field1).AddField(field2).AddField(field3).AddField(field4).AddField(field5) color := slackColor attachment.Color = &color payload := slack.Payload{ Username: USERNAME, Attachments: []slack.Attachment{attachment}, } err := slack.Send(SLACKURL, "", payload) if err != nil { os.Exit(1) } }
ServerlessFrameworkを利用したデプロイ
Lambda (Go) + IAMロール + CloudwatchEventをseverlessFrameworkを利用してデプロイしました。
■ デプロイコマンド
- Goコンパイル - $ make build - ServerlessFrameworkでデプロイ - $ sls deploy --aws-profile <PROFILE> --slackurl <SLACKURL>
export GO111MODULE=on ## Install dependencies .PHONY: deps deps: go get -v -d ## Setup development .PHONY: deps devel-deps: deps GO111MODULE=off go get -u golang.org/x/lint/golint go get -u github.com/motemen/gobump/cmd/gobump go get -u github.com/Songmu/make2help/cmd/make2help ## Setup build .PHONY: pre-build build-deps: go get -u github.com/mitchellh/gox ## Build binaries .PHONY: build build: build-deps rm -rf ./main gox -os=linux -arch=amd64 -output=./main -ldflags "-s -w" gobump show ## Lint .PHONY: lint lint: devel-deps go vet ./... golint -set_exit_status ./... ## Show help .PHONY: help help: @make2help $(MAKEFILE_LIST)
$ make help
build: Build binaries build-deps: Setup build deps: Install dependencies devel-deps: Setup development help: Show help lint: Lint
- serverless.yml
service: guardduby frameworkVersion: ">=1.48.0" provider: name: aws stage: dev runtime: go1.x region: ap-northeast-1 functions: slack: handler: main role: GuardDutyLambda timeout: 30 description: GuardDutyLambda slack notice memorySize: 128 environment: SLACKURL: ${opt:slackurl} THRESHOLD: 2.0 events: - cloudwatchEvent: event: source: - 'aws.guardduty' detail-type: - 'GuardDuty Finding' resources: Resources: GuardDutyLambda: Type: AWS::IAM::Role Properties: RoleName: GuardDutyLambda AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/IAMReadOnlyAccess Policies: - PolicyName: GuardDutyLambda PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*"
まとめ
AWS GuardDutyのアラート通知を CloudwatchEvent + Lambda (Go)を使ってSlackに通知する方法を記載しました。 デプロイについては、ServerlessFrameworkを利用しています。
GuardDutyの通知は、最近サービスリリースされた ChatBotが対応しているので利用してみてもいいかもしれません。 jp.techcrunch.com
AWS アベイラビリティゾーン名とAZ-IDのマッピングを確認する
AWSの大障害がありアベイラビリティーゾーン名とAZ-IDのマッピングを確認した事があったので備忘録的に書きます。
■ aws-cli
aws-cliを利用します。バージョンが1.15系だと ZoneId
が取得できなかったので、アップデートしました。
$ aws --version
aws-cli/1.16.106
以下、実行コマンドと結果です。 <PROFILE_NAME>
の部分は各自の実行環境に合わせてください。
■ 整形せずに出力する
$ aws ec2 describe-availability-zones --region ap-northeast-1 --profile <PROFILE_NAME>
{ "AvailabilityZones": [ { "State": "available", "Messages": [], "RegionName": "ap-northeast-1", "ZoneName": "ap-northeast-1a", "ZoneId": "apne1-az4" }, { "State": "available", "Messages": [], "RegionName": "ap-northeast-1", "ZoneName": "ap-northeast-1c", "ZoneId": "apne1-az1" }, { "State": "available", "Messages": [], "RegionName": "ap-northeast-1", "ZoneName": "ap-northeast-1d", "ZoneId": "apne1-az2" } ] }
■ jq
で整形する
- コマンド
aws ec2 describe-availability-zones \ --region ap-northeast-1 \ --profile <PROFILE_NAME> \ | jq -c '.AvailabilityZones[] | {RegionName: .RegionName, ZoneName: .ZoneName, ZoneId: .ZoneId}'
- 出力
{"RegionName":"ap-northeast-1","ZoneName":"ap-northeast-1a","ZoneId":"apne1-az4"} {"RegionName":"ap-northeast-1","ZoneName":"ap-northeast-1c","ZoneId":"apne1-az1"} {"RegionName":"ap-northeast-1","ZoneName":"ap-northeast-1d","ZoneId":"apne1-az2"}
■ テーブル表示にする
- コマンド
aws ec2 describe-availability-zones \ --region ap-northeast-1 \ --query 'AvailabilityZones[].{RegionName:RegionName, ZoneName:ZoneName, ZoneId:ZoneId}' \ --output table \ --profile <PROFILE_NAME>
- 出力
---------------------------------------------------- | DescribeAvailabilityZones | +----------------+-------------+-------------------+ | RegionName | ZoneId | ZoneName | +----------------+-------------+-------------------+ | ap-northeast-1| apne1-az4 | ap-northeast-1a | | ap-northeast-1| apne1-az1 | ap-northeast-1c | | ap-northeast-1| apne1-az2 | ap-northeast-1d | +----------------+-------------+-------------------+
参考
Goの文法学習 ( 自分用Memoで適宜更新 )
先人たちのブログや技術書を活用して自分用の備忘録です。 参照した記事は参照に一覧で表示させていただきます。
( 適宜更新 )
Map
mapを作成
make
make(map[<キーの型>]<値の型>)
で mapを生成
- mapから値を取得
package main import ( "fmt" ) func main() { m := make(map[string]int) m["test1"] = 100 m["test2"] = 90 fmt.Println(m["test1"]) fmt.Println(m) } /* 結果 100 map[test1:100 test2:90] */
mapリテラルをつくる
※)リテラル
数値や文字列を直接に記述した定数のこと
package main import ( "fmt" ) func main() { m := map[string]int{ "test1": 100, "test2": 95, "test3": 50, } fmt.Println(m) } /* 結果 map[test1:100 test2:95 test3:50] */
値チェック
valueが存在するかを確認する。結果は true
, false
が返る
package main import ( "fmt" ) func main() { m := map[string]int{ "test1": 100, "test2": 95, "test3": 50, } val, chk := m["test1"] fmt.Println(val, chk) val, chk = m["test100"] fmt.Println(val, chk) } /* 結果 100 true 0 false */
key/valueを削除
package main import ( "fmt" ) func main() { m := map[string]int{ "test1": 100, "test2": 95, "test3": 50, } fmt.Println(m) delete(m, "test1") fmt.Println(m) } /* 結果 map[test1:100 test2:95 test3:50] map[test2:95 test3:50] */
key/valueの全取得と要素数の取得
package main import ( "fmt" ) func main() { m := map[string]int{ "test1": 100, "test2": 95, "test3": 50, } for key, val := range m { fmt.Println(key, val) } fmt.Println(len(m)) } /* 結果 test1 100 test2 95 test3 50 3 */
error
package main import ( "fmt" ) func main() { if err := doError();err != nil { fmt.Println(err) //return } fmt.Println("end of code") } func doError() error { message := "error occurred!!!" return fmt.Errorf(message) } /* 結果 ( return無し ) error occurred!!! end of code */ /* 結果 ( return有り ) error occurred!!! */
Slice (スライス)
- 特徴
- 可変長配列
- 配列全体のポインタ(ptr) / 配列の長さ(len) / 配列の容量(cap)からなるデータ構造
make()関数
make(型, 要素数, 容量)
で定義する。- スライスが初期化され、デフォルトでは0が格納される
- 容量は省略可能。省略すると
要素数
と容量
は同じになる - 組み込み関数
package main import ( "fmt" ) func main() { test1 := make([]int, 5, 10) fmt.Println(test1) fmt.Println(len(test1)) fmt.Println(cap(test1)) fmt.Println() test2 := make([]int, 10) fmt.Println(test2) fmt.Println(len(test2)) fmt.Println(cap(test2)) } /* 結果 [0 0 0 0 0] 5 10 [0 0 0 0 0 0 0 0 0 0] 10 10 */
append()関数
append(slice, val1, val2, val3, ...)
と使うslice
の末尾に val1, val2 ... と追加されていく- sliceの要素数は自動的に増える
- 容量に達すると、容量の2倍を確保してくれる
- 配列のメモリアドレスは容量が確保されると変わる
copy関数
copy(コピー先, コピー元)
として使う
package main import ( "fmt" ) func main() { test1 := []int{1, 2, 3, 4, 5} test2 := make([]int, 5) copy(test2, test1) fmt.Println(test2) } /* 結果 [1 2 3 4 5] */
構造体/JSON
構造体の初期化
package main import ( "fmt" ) type Person struct { Name string Age int } func main() { var p1 Person p1.Name = "yhidetoshi" p1.Age = 20 fmt.Println("p1: "+p1.Name, p1.Age) p2 := Person{"yhidetoshi", 21} fmt.Println("p2: "+p2.Name, p2.Age) p3 := Person{Name: "yhidetoshi", Age: 22} fmt.Println("p3: "+p3.Name, p3.Age) p4 := &Person{Name: "yhidetoshi", Age: 23} fmt.Println("p4: "+p4.Name, p4.Age) p5 := new(Person) p5.Name = "yhidetoshi" p5.Age = 24 fmt.Println("p5: "+p5.Name, p5.Age) p6 := &Person{} p6.Name = "yhidetoshi" p6.Age = 25 fmt.Println("p6: "+p6.Name, p6.Age) } /* 結果 p1: yhidetoshi 20 p2: yhidetoshi 21 p3: yhidetoshi 22 p4: yhidetoshi 23 p5: yhidetoshi 24 p6: yhidetoshi 25 */
構造体配列
フェレットを飼っている人には見やすいかもしれません。 フェレットの情報を保持した構造体を配列にして、3匹のフェレット情報を格納した例です。
package main import "fmt" type Ferret struct { Age int Color string Farm string } type Ferrets []Ferret func main() { maru := Ferret{ Age: 4, Color: "blackself", Farm: "furFarm", } hana := Ferret{ Age: 6, Color: "sable", Farm: "ruby", } natu := Ferret{ Age: 3, Color: "butterscotch", Farm: "mountainDew", } var ferrets Ferrets ferrets = append(ferrets, maru) ferrets = append(ferrets, hana) ferrets = append(ferrets, natu) fmt.Println(ferrets) } /* [{4 blackself furFarm} {6 sable ruby} {3 butterscotch mountainDew}] */
ネスト構造体
package main import ( "fmt" ) type Ferret struct { Name string Food Food } type Food struct { Name string Volume int } func main() { ferret := Ferret{ Name: "maru", Food: Food{ Name: "zupurimeGrainFreeDiet", Volume: 5, }, } fmt.Println(ferret.Name) fmt.Println(ferret.Food.Name) fmt.Println(ferret.Food.Volume) } /* 実行結果 maru zupurimeGrainFreeDiet 5 */
ネスト構造の実用例
- 別ブログでこのネスト構造体を利用しました。
GuardDutyのjson形式が以下で、 Resource
がネストし、さらに InstanceDetails
がネストしています。
{ "schemaVersion": "2.0", "accountId": "XXX", "region": "ap-northeast-1", "type": "UnauthorizedAccess:EC2/TorRelay", "resource": { "resourceType": "Instance", "instanceDetails": { "instanceId": "i-99999999", "instanceType": "m3.xlarge" } }, "title": "EC2 instance i-99999999 is communicating with Tor Exit node.", "description": "EC2 instance i-99999999 is communicating with IP address 198.51.100.0 on the Tor Anonymizing Proxy network." }
なので、構造体の宣言は以下のようにしました。
- go( 一部抜粋 )
// GuardDutyFindings set guardduty GuardDutyFindingsValue type GuardDutyFindings struct { AccountID string `json:"accountId"` Region string `json:"region"` Type string `json:"type"` Severity json.Number `json:"severity"` Title string `json:"title"` Description string `json:"description"` Resource Resource `json:"resource"` } // Resource set guardduty ResourceValue type Resource struct { ResourceType string `json:"resourceType,omitempty"` UserName string `json:"userName,omitempty"` InstanceDetails InstanceDetails `json:"instanceDetails,,omitempty"` AccessKeyDetails AccessKeyDetails `json:"accessKeyDetails,,omitempty"` } // InstanceDetails set guardduty value type InstanceDetails struct { InstanceID string `json:"instanceId,omitempty"` InstanceType string `json:"instanceType,"` }
Marshalを使う
b, _ := json.Marshal(&ferret) fmt.Println(string(b)) fmt.Println() bmi, _ := json.MarshalIndent(&ferret, "", " ") fmt.Println(string(bmi))
- 実行結果
{"Name":"maru","Food":{"Name":"zupurimeGrainFreeDiet","Volume":5}} { "Name": "maru", "Food": { "Name": "zupurimeGrainFreeDiet", "Volume": 5 } }
Unmarshalを使う
[ { "Name": "maru", "Food": { "Name": "zupurimeGrainFreeDiet", "Volume": 5 } }, { "Name": "hana", "Food": { "Name": "ferretSelectionSenior", "Volume": 3 } }, { "Name": "natu", "Food": { "Name": "totalyComplete", "Volume": 7 } } ]
- main.go
package main import ( "encoding/json" "fmt" "io/ioutil" ) type Ferret struct { Name string Food Food } type Food struct { Name string Volume int } func main() { var ferrets []Ferret bytes, err := ioutil.ReadFile("ferret.json") if err != nil { fmt.Println(err) } errJson := json.Unmarshal(bytes, &ferrets) if errJson != nil { fmt.Println(errJson) } for _, value := range ferrets { fmt.Printf("%s\t%s\t%d\n", value.Name, value.Food.Name, value.Food.Volume) } } /* 実行結果 maru zupurimeGrainFreeDiet 5 hana ferretSelectionSenior 3 natu totalyComplete 7 */
NewDecoderを使う
- ストリームから情報を読み込んだ時はjson.NewDecoderを使う
- http.Getで返ったresp.Bodyなどの io.Reader型に対して使う
req, err := http.NewRequest("GET", "https://api.nature.global/1/devices", nil) if err != nil { fmt.Println("Error") } req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Error request") } err = json.NewDecoder(resp.Body).Decode(&devices) if err != nil { fmt.Println("Error decode") }
詳細はこちら。 yhidetoshi.hatenablog.com
JSON-to-Go ( Convert JSON to Go struct )
JSON --> 構造体 してくれるサービスがる。
JSON-to-Go: Convert JSON to Go instantly
ポインタ
参考・引用について
このポインタのところは、以下の2つのブログで大変わかりやすく解説されています。自分が理解するために必要なところを引用・参照して記載しています。(多謝)
ポインタとは
説明
- int型変数のポインタの場合は
*int
と記述する - 変数のアドレス取得するときは
&
をつけ、アドレスから変数の中身へアクセスする時は*
を使う 変数
とは、メモリのアドレスに付けられた名前
のこと変数c
があるのではなくて、0x7fffffffe494
という場所アドレス
に変数c
という名前を付けている- コンパイラが
変数cという場所
が必要なようだから、そのぶんメモリを用意しておこう。0x7fffffffe494
を変数c
と名付けよう」と決めている - コンパイラがこういったことを考えてくれるおかげで、われわれプログラマーは「変数c」というものの正体を深く考えずにプログラミングできる
- ポインタは
メモリ上の他のアドレスを指す変数
- int型変数のポインタの場合は
Pointerを使う理由
- サイズの大きな変数を効率よく扱う
- 関数の引数として値を渡す際に、「引数用のアドレス」に変数をコピーします。int型やchar型のような小さい変数であれば問題ないですが、大きなサイズの構造体を引数に取るような関数の場合、メモリコピーにCPUを使ってしまって性能が落ちてしまう。どれだけ大きな構造体でも、先頭のアドレスだけ渡してしまえば効率よくやり取りできる
- サイズの大きな変数を効率よく扱う
void warizan(int warareru, int waru, int* syou, int* amari) { *syou = warareru / waru; *amari = warareru % waru; } void main () { int a; int b; int c; int d; a = 17; b = 5; warizan(a, b, &c, &d); // c = 3, d = 2になる }
- 値渡し
- ある変数を関数の引数として渡す場合、値のコピーが渡されます。そのため呼び出された関数内で変数の値を変更しても、元の値には影響がありません。これを「値渡し」と呼びます。
- ポインタ渡し(参照渡し)
- ポインタ変数を関数に渡した場合は、ポインタが指し示す値が、呼び出し元も、呼び出された方も同じものであるため、呼び出された関数内で元の値を変更することができます。これを「ポインタ渡し(または参照渡し)」と呼びます。
値渡しとポインタ渡しの簡単な例
package main import "fmt" func call(a int, b *int){ a = a + 1 *b = *b + 1 } func main(){ a,b := 5,5 call(a, &b) fmt.Println("値渡し", a) // ---> 値渡し 5 fmt.Println("ポインタ渡し", b) // ---> ポインタ渡し 6 }
- ポインタ配列
- Go 言語の配列名は配列の先頭アドレスを表していません。また、配列にアドレス演算子を適用すると、配列へのポインタが生成されますが、それは配列の先頭アドレスを表しているわけではありません。Go 言語の配列はひとつの「値」なので、配列へのポインタは配列そのものを指し示すことになります。
メソッドの ポインタレシーバ
と 変数レシーバ
type Vertex struct { X, Y float64 } func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f } func ScaleFunc(v *Vertex, f float64) { v.X = v.X * f v.Y = v.Y * f }
- ポイントレシーバ
- 呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取得できる
var v Vertex ScaleFunc(v) // Compile error! ScaleFunc(&v) // OK var v Vertex v.Scale(5) // OK p := &v p.Scale(10) // OK
- 変数レシーバ
- メソッドが変数レシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができる
var v Vertex fmt.Println(AbsFunc(v)) // OK fmt.Println(AbsFunc(&v)) // Compile error! var v Vertex fmt.Println(v.Abs()) // OK p := &v fmt.Println(p.Abs()) // OK
- ポインタレシーバを使う理由
- メソッドがレシーバが指す先の変数を変更するため
- メソッドの呼び出し毎に変数のコピーを避けるため。 例えば、レシーバが大きな構造体である場合に効率的
reflect
-
- interface{}型から具体的な型に落とし込む必要がある
- アサーションする型が実体と合わない場合はpanicが発生する
reflectパッケージを利用する
package main import ( "fmt" "reflect" ) func p(v ...interface{}) { fmt.Println(v...) } func main() { // 型チェック p(reflect.TypeOf(0)) // int //型比較 p(reflect.TypeOf(0).Kind() == reflect.Int) // true p(reflect.TypeOf(0) == reflect.TypeOf("hoge")) // false p(reflect.TypeOf(0) == reflect.TypeOf(1)) // true //reflectで値をセット /* Elem https://golang.org/pkg/reflect/#Value.Elem Elem returns the value that the interface v contains or that the pointer v points to. It panics if v's Kind is not Interface or Ptr. It returns the zero Value if v is nil. */ var n int v := reflect.ValueOf(&n).Elem() v.SetInt(99) p(n) // 99 v.Set(reflect.ValueOf(999)) p(n) // 999 /* Slice */ // 型チェック p(reflect.TypeOf([]int{})) // []int // 型比較 p(reflect.TypeOf([]int{}).Kind() == reflect.Slice) // true p(reflect.TypeOf([]int{}) == reflect.TypeOf([]int{1, 2})) //true }
テスト
書き方
- 書き方のルール
- テストコードは同一のディレクトリに配置する
- テストコードのファイル名は
_test.go
とする -->calc_test.go
- Testから始まる名前にする -->
func TestSum(t *testing.T)
- 引数は *testing.Tを渡す -->
func TestSum(t *testing.T)
シンプルなコードで確認する。
- ディレクトリ構成
. ├── calc │ ├── calc.go │ └── calc_test.go └── main.go
- main.go
package main func main() {}
- calc.go
package calc func Sum(a, b int) int { return a + b }
- calc_test.go
package calc import "testing" func TestSum(t *testing.T) { if Sum(1 ,2) != 3{ t.Fatal("Sum vaule should be 3, but not match" ) } }
実行方法
実行のルール
$ go test -v ./calc
=== RUN TestSum --- PASS: TestSum (0.00s) PASS ok _/Users/hidetoshi/TestGoStudy/calc (cached)
$ go test -run TestSum ./calc
ok _/Users/hidetoshi/TestGoStudy/calc (cached)
$ go clean -testcache
(キャッシュをクリア)$ go test -run TestSum ./calc
ok _/Users/hidetoshi/TestGoStudy/calc 0.008s
Tips
Goで開発するときに利用すべきツール
gofmt
- コードをフォマットしてくれる
goimports
- パッケージを読み込むimport文の挿入ど削除を自動でおこなってくれる
govet
- バグの原因になりそうなコードを検出してくれる
golint
- Goらいしくない書き方を検出してくれる
-set_exit_status
- 警告が発生したらエラーで終了させる場合に使う
-min_confidence
- 厳しさを変更できる
- デフォルト値は0.8
-min_confidence=0.5
のように指定する
- 厳しさを変更できる
Go Module
- Go1.13から正式導入予定
- 環境変数
export GO111MODULE=on
go mod init
- go.modファイルが生成される
- 依存関係の定義と管理のためのファイル
go: cannot determine module path for source directory... ( outside GOPATH, no import comments )
- gitのリポジトリ設定がされていなければエラーになる ? と思う。
go get
を実行するとgo.modが自動更新される。go.sumファイルが生成される- go.sumファイル
- バージョンロックのためのファイル
- go.sumファイル
- go.modファイルが生成される
表示形式
- go-humanize
- Size
- ファイルサイズ等
humanize.Bytes(12345678)
- ファイルサイズ等
- Time
- 現在時刻からの相対時刻
humanize.Time(t)
- 現在時刻からの相対時刻
- Comma
- 3桁区切り
humanize.Comma(123456)
- 3桁区切り
- SI単位
humanize.SI()
- Ftoa
- 末尾の0を除去
humanize.Ftoa(1.23)
- 末尾の0を除去