長生村本郷Engineers'Blog

千葉県長生村本郷育ちのエンジニアが書いているブログ

Backlog でコメント追加時に 「お知らせしたいユーザ」に Slack DM する

Backlog でコメント追加時に 「お知らせしたいユーザ」に Slack DM する AWS Serverless Application Model with Golang プロジェクト作りました♪

f:id:kenzo0107:20200227215819p:plain
slack DM by triggered by adding comments with users you want to notify in backlog

使い方は、 Git プロジェクトを見ていただければ!

github.com

もしよくわからんぞ!という時は連絡ください♪

構築するに辺り、検討した点

世の中には backlog API 関連の SDK などあるか?

griffin-stewie/go-backlog が諸々揃っていて良さそうだったので使ってみました。

利用したい箇所としては、以下です。 * コメント追加時のイベント情報を受ける type Activity struct * ↑で受け取れる通知したいユーザの ID から Email アドレスを取得する API 実行

但し、このライブラリでは、Activity には通知したい情報を含む Notifications がコメントアウトとなっていたので、直ちに利用できず、

急を要していたこともあり、 fork して kenzo0107/go-backlog で対応しました。

単純にコメントアウトを外して使える様にしただけでは、 json.Unmarshal 実行時にエラーとなっており、他のパラメータも幾分か対応する必要がありました。

Backlog からのアクセス制御はどうする?

Backlog からのアクセス制御はBasic 認証について言及があったので Basic 認証にしました。

IP アドレスの変更に影響されない方法であれば、 Webhook の URL に BASIC 認証をつけていただくことで、IP アドレスに依存しない認証できます。

support-ja.backlog.com

IP レンジは予告なく変更される可能性があり、作成者以外ではなかなか気付きにくいかもしれません。 その為、 Backlog Webhook に設定する URL は、 https://<user>:<password>@...... と Basic 認証の情報を埋め込む様にしました。

これを API Gateway + Lambda Authorizer (Request Type) で認証させる様にしました。

Backlog API 実行時に許可する ip はどうするか?

Backlog API を実行する Lambda を Nat Gateway をルーティングした Private Subnet に置くことで、出口 IP を固定する様にし、その IP を Backlog 側で許可 IP として設定しました。

注意点

Backlog Webhook 各プロジェクト毎に設定する必要があり

全プロジェクト一括して設定ということができませんでした。 2020-02-27 現在

各プロジェクト管理者に秘密情報として通知する様に対応をしました。

まとめ

まだテストを書き切れてないところはありますが、問題なく動作していることを確認しています。

Backlog を取り入れている方へ、何かしら参考になれば幸いです。

以上です。

GitHub Actions で job を 直列 と 並列 実行どっちにしよう?

概要

GitHub Actions で go の errcheck や lint 等、静的解析を実行していますが、 その job の直列構成と並列構成、どちらがいいんだろう? と悩んだ時の話です。

悩んだポイント

  • 並列構成だと、各 job でコンテナロードが発生し、実行時間は短いが、トータルの実行時間は長くなる。
  • 直列構成だと、コンテナロードは 1 回で済み、実行時間は長くなるが、トータルの実行時間は短くなる。

あまりお金かけず実行したいな、というとやはり直列だろうか。

実際の構成

job 直列

name: static check
on: push

jobs:
  imports:
    name: Imports
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: check
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: imports
        token: ${{ secrets.GITHUB_TOKEN }}

  errcheck:
    name: Errcheck
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: check
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: errcheck
        token: ${{ secrets.GITHUB_TOKEN }}

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: check
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: lint
        token: ${{ secrets.GITHUB_TOKEN }}

  shadow:
    name: Shadow
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: check
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: shadow
        token: ${{ secrets.GITHUB_TOKEN }}

  staticcheck:
    name: StaticCheck
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: check
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: staticcheck
        token: ${{ secrets.GITHUB_TOKEN }}

  sec:
    name: Sec
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: check
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: sec
        token: ${{ secrets.GITHUB_TOKEN }}

job 並列

name: static check
on: push

jobs:
  imports:
    name: Imports
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: imports
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: imports
        token: ${{ secrets.GITHUB_TOKEN }}
    - name: errcheck
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: errcheck
        token: ${{ secrets.GITHUB_TOKEN }}
    - name: lint
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: lint
        token: ${{ secrets.GITHUB_TOKEN }}
    - name: shadow
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: shadow
        token: ${{ secrets.GITHUB_TOKEN }}
    - name: staticcheck
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: staticcheck
        token: ${{ secrets.GITHUB_TOKEN }}
    - name: sec
      uses: grandcolline/golang-github-actions@v1.1.0
      with:
        run: sec
        token: ${{ secrets.GITHUB_TOKEN }}

実際に計測してみた

各構成で 10 回実行してみると 平均時間は以下の通り。

  • 並列構成では 1分30秒/job * 6 job = 9分
  • 直列構成だと 1分50秒/job * 1 job = 1分50秒

実行速度としては 20 秒くらいの差しかなかった。

トータルでは、 7分50秒の差!

どっちにしよう?

  • GitHub Actions の料金は job のトータル実行時間の従量課金制度なので、直列の方がお金に優しい。
  • だが、コミット頻度の少ないプロジェクトなら、並列 で時間を大事にでも良さそう。

今回は実行時間の差が 20 秒程度なので、直列でも全然問題ないレベルですが、 お金との兼ね合いで 直列・並列の使い分けは変わってきそう、と思った話でした。

改訂2版 みんなのGo言語

改訂2版 みんなのGo言語

ディスク使用量が増加した際の調査方法

備忘録

ディスク使用量増加のアラートが上がったので調査した際の手順をまとめました。

$ df -h

Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        2.0G   60K  2.0G   1% /dev
tmpfs           2.0G     0  2.0G   0% /dev/shm
/dev/xvda1       20G  8.6G   11G  90% /   ★ ここ高い!

調査法

$ cd /

// ① 現ディレクトリで ディスク使用率の高いベスト 10 を発表
$ sudo du -ms ./* | sort -nr | head -10

5047    ./var
1355    ./usr
1249    ./home
642     ./opt
194     ./lib
70      ./boot
42      ./tmp
20      ./lib64
13      ./etc
12      ./sbin

// ② 使用率の一番高いディレクトリへ移動
$ cd ./var

以上の ①, ② の繰り返しによってどのディレクトリが高いか調査してます。

du の option は以下

  • -m : MB 表示
  • -s : 総計表示

普段は -h で見やすくしてますが、 今回の調査時には sort した際に MB や kB が混ざって表示され、直感的にわかり辛くなるので、 -ms にしてます。

Go 静的解析 & 単体テスト on GitHub Actions

以前、複数の AWS Account EC2 インスタンスへの接続を EC2 Instance Connect を使用しインタラクティブssh 接続できるツールを作成しました。

kenzo0107.hatenablog.com

自分用にチャチャッと作ってアップしたもので手元でしかテストしておらず 、 lint や test もしてなかったのですが、

  • 後学の為、改めて lint, test した上でコードを管理できる様になりたい
  • 自分のリポジトリとして公開しているし、愛情持ちたい

という気持ちで取り組んでみたいと思いました。

成果物

github.com

静的解析 on GitHub Actions

grandcolline/golang-github-actions を利用させていただきまして以下解析しました。

GitHub Actions での設定ファイルは以下です。

gist.github.com

開発時にも静的解析

開発時に解析できる様、 Makefile に設定しました。

.PHONY: lint
lint: devel-deps
    go vet ./...
    go vet -vettool=$(shell which shadow)
    staticcheck ./...
    errcheck ./...
    # exclude G106: Audit the use of ssh.InsecureIgnoreHostKey
    gosec -quiet -exclude=G106 ./... 
    golint -set_exit_status ./...

goimports に関してはエディタの設定で保存時にチェックする様にしてます。

実際に設定して動かしてみたらわかりやすいですが、以下にどんなことを指摘してくるかまとめます。

go vet ./...

構文エラーを指摘してくれます。

a := fmt.Sprintf("%s", 1)
fmt.Println(a)

go vet -vettool=$(shell which shadow)

スコープの外側で定義した変数と同名の変数がスコープの内部で使用されている場合に警告されます。

潜在的にエラーが発生する可能性があり、変数名を変更する等の対応が必要です。

func hoge() error {
    err := errors.New("error occured")
    {
        err := errors.New("error occured")
        if err != nil {
            return err
        }
    }

    if err != nil {
        return err
    }
    return nil
}
./main.go:14:3: declaration of "err" shadows declaration at line 12

上記の場合、以下の様にスコープ内でスコープ外の変数を利用することで回避できます。

   err := errors.New("error occured")
    {
        err = errors.New("error occured")

ですが、この場合だと最初は err := で定義し、その後は err = で定義しなければならず、わかりづらいので、 変数 err を定義した直後に処理を実施する様にすると回避できます。

func hoge() error {
    err := errors.New("error occured")
    if err != nil {
        return err
    }

    {
        err := errors.New("error occured")
        if err != nil {
            return err
        }
    }
    return nil
}

staticcheck ./...

バグの検出、コードの簡素化の提案、デッドコードの指摘などをしてくれます。

以下のコードで staticcheck ./... を実行すると

import "github.com/aws/aws-sdk-go/aws/session"
...

session.New(&config)

session.New 使うのは非推奨だよ、と警告してくれました。

deprecated を指摘してくれるのありがたい!

session.New is deprecated: Use NewSession functions to create sessions instead. NewSession has the same functionality as New except an error can be returned when the func is called instead of waiting to receive an error until a request is made.  (SA1019)

以下の様に修正することで、対応できました。

session.Must(session.NewSession(&config))

errcheck ./...

error を返す function の処理をチェックしているか警告してくれます。

以下のコードで errcheck ./... を実行すると

f, err := os.Open(fpath)
...
defer f.Close()

以下の様に警告されます。

pkg/utility/profile.go:19:15:   defer f.Close()

f.Close() が error を返しますが、その error がチェックされていないですよ、という指摘です。

Close closes the File, rendering it unusable for I/O. On files that support SetDeadline, any pending I/O operations will be canceled and return immediately with an error.

func (*os.File).Close() error

無名関数で wrap してその中で error check する様にし対応しました。

defer func() {
    if err := f.Close(); err != nil {
        log.Fatalln(err)
    }
}()

gosec -quiet ./...

使用コードでの脆弱性を指摘してくれます。

現状の omssh では gosec -quiet ./... を実行してみると以下の様な警告が出ます。

G106 として管理されている ssh.InsecureIgnoreHostKey() を利用していることへの脆弱性が指摘されています。

Results:


[/Users/kenzo.tanaka/src/github.com/kenzo0107/omssh/omssh.go:50] - G106 (CWE-322): Use of ssh InsecureIgnoreHostKey should be audited (Confidence: HIGH, Severity: MEDIUM)
  > ssh.InsecureIgnoreHostKey()


Summary:
   Files: 9
   Lines: 718
   Nosec: 0
  Issues: 1

対応法を把握できていないので -exclude=G106 オプションで回避しています。

gosec で警告されがちなコード

よくありがちな gosec に警告されがちなコード例としては以下ではないでしょうか。

引数として、ファイルパスを渡して、そのファイルを開こうとしています。

func GetProfiles(credentialsPath string) (profiles []string, err error) {
    f, err := os.Open(credentialsPath)
        ...

このコードを gosec -quiet ./... してみると以下の様に警告されます。

[/Users/kenzo.tanaka/src/github.com/kenzo0107/omssh/pkg/utility/profile.go:16] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM)
  > os.Open(credentialsPath)

こちらの対処法としては、ファイルパスを綺麗にしてくれる filepath.Clean(string) を噛ませると回避できました。

- f, err := os.Open(credentialsPath)
+ f, err := os.Open(filepath.Clean(credentialsPath))

../kenzo\hoge/moge というファイルパスだと ../kenzo\hoge/moge というファイルパスが返ります。

golint -set_exit_status ./...

-set_exit_status を指定しているのは、終了ステータスを返してくれます。

以下の様なコードがあるとします。

type EC2Iface interface {
   ...
}

golint -set_exit_status ./... すると以下の様なエラーを出力されます。

pkg/awsapi/ec2.go:12:6: exported type EC2Iface should have comment or be unexported
Found 1 lint suggestions; failing.

EC2Iface と大文字始まりなので、他パッケージからも参照できる exported type なのでコメントを書きましょうね、
という指摘です。

VSCode を使っていますが、この様にカーソルを合わせるとコメントが表示されてくれます。

丁寧に書いて置いて上げるとこんな値を返すよ〜とかわかりやすいですね。

f:id:kenzo0107:20191225151004p:plain

面倒くさがらずコメント書きましょう。

単体テスト on GitHub Actions

こちらはシンプルで test を実行し coverage を CodeCov に上げる様にしました。また、そのカバレッジを README にラベルとして表示できます。

CODECOV_TOKEN は GitHub の Settings > Secrets で設定しておきます。

codecov

gist.github.com

まとめ

静的解析と単体テストを追加したことで

  • コード変更がしやすくなった。
  • Golang の求めるコードの書き方を学ぶことができた。
  • aws-sdk-go の mock の作成の仕方を学ぶことができた。

というご利益がありました。

単体テストで 100% を目指しましたが、

ssh 接続周りで手こずってカバレッジが微増でした。

ssh 接続する際に仮想的な ssh server を起動する所まではよかったんですが、 (*ssh.Session).RequestPty するとそこで処理待ちが発生し、テストが進まなくなってしまいました。

良い案ありましたらご教示いただけましたら幸いです m(_ _)m

今回の知見を活かして

AWS の EC2 や RDS 等の Reserved Instance の使用率・カバレッジ率を Datadog にプロットする Lambda を生成する SAM プロジェクトを Go で作ってみました。

プロットしたメトリクスに監視することで使用率・カバレッジ率低下をアラートできます。

github.com

また、より OSS らしくロゴを作ってみました♪

愛情が増します。

f:id:kenzo0107:20191225173147p:plain

参照

stackoverflow.com

gosec で警告される os.Open() 対応

gosec で以下のようなコードがあると

os.Open(fname)

以下のように警告されます。

G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM)

変数でファイルパスを指定するのは、意図しないファイルパスを指定される危険性があります。

対応

filepath.Clean() を使用し、よろしくないパスを綺麗に整えるようにします。

os.Open(filepath.Clean(fname))

以上 ご参考になれば幸いです。

Golang errcheck による defer 警告対応

概要

このようなコードを書いていると errcheck を実行した場合、 defer f.Close() と指摘されてしまいます。

    f, err := os.Open(fpath)
    if err != nil {
        log.Println(err)
        return hoge, err
    }

    defer f.Close()

f.Close() は返り値が error であり、その error の返り値をハンドリングしていない、という警告です。

対応

その為、以下のように修正することで回避

    f, err := os.Open(fpath)
    if err != nil {
        log.Println(err)
        return hoge, err
    }

    defer func() {
        err = f.Close()
        if err != nil {
            log.Fatalln(err)
        }
    }()

参考

随分前に既に掲題について話をしていた

github.com

Fix: can't find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException)

rbenv で複数 ruby バージョンが存在する環境下で bundle install しようとすると以下のエラーが出てしまいました。

can't find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException)
  • ruby バージョンは合ってる、
  • Gemfile もある、
  • gem install bundler して bundle もある ← ここがダメだった

けど、エラー

ちょいちょいハマってたので備忘録とりました。

結論

bundle のバージョン (2.0.2) が Gemfile.lock (1.17.1) と異なることで発生していました。

  • gem インストール時の bundler は 2.0.2
$ gem install bundler

Successfully installed bundler-2.0.2
Parsing documentation for bundler-2.0.2
Done installing documentation for bundler after 2 seconds
1 gem installed
  • Gemfile.lock での bundler は 1.17.1
...
RUBY VERSION
   ruby 2.5.3p105

BUNDLED WITH
   1.17.1

なので、実行する bundle のバージョンを Gemfile.lock 側に合わせてあげれば実行できるようになりました。

対応

$ gem install bundler -v 1.17.1
$ gem uninstall bundler -v 2.0.2

(>= 0.a) というのがパッと見、ん?となってしまい、あれ、設定したのにな、と思ってるとハマるので、このエラーメッセージを見たら反応できるようにしておきたい内容でした。

以上 参考になれば幸いです。