長生村本郷Engineers'Blog

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

ブログを移行しました → kenzo0107.github.io

以下にお引っ越ししました。記事は全て移行済みです。

https://kenzo0107.github.io/

こちらのはてなブログはしばらく残しておきます。

https://kenzo0107.hatenablog.com/

移行理由

ブログ以外の自身の活動等も管理していきたいと考えた為です。

元記事の引っ越し

hatena ブログの rss から jekyll-import で元記事を引っ張る方法を実施しましたが、 rss の pager の仕様がイマイチ分からず、スクレピングして引っこ抜きました。

hatenablogからgithub.ioへ移行

移行先のブログ構築

Github Pages で hexo (plugin:icarus) を利用して構築しました。 デザイン気に入ってます♪

以上 今後ともよろしくお願い致します。

Terraform ベストプラクティス 2020 春 ~moduleやめてみた~

#InfraStudy に刺激を受け、書きます!

2019年に以下記事を書いてから早1年、terraform 運用歴を重ね、2020年春のベストプラクティスを更新しました。

kenzo0107.github.io

例によって、まず結論、

結論

  • module やめてみた

↓これをやめて、

.
├──envs/
│   ├── bootstrap/
│   ├── prd/
│   └── stg/
│
└──modules
    ├── bootstrap/
    ├── common/
    ├── prd/
    └── stg/

↓これにした!

.
└──envs/
    ├── bootstrap/
    ├── prd/
    └── stg/

module やめてみた

一般的な Web サービスを構築する際に module を利用した時は以下の様に構成にしていました。

.
├──envs/
│   ├── bootstrap/
│   ├── prd/
│   └── stg/
│
└──modules
    ├── bootstrap/
    ├── common/
    ├── prd/
    └── stg/

bootstrap は tfstate や Lock 管理用 DynamoDB を作成します。 stg, prd に依存しないリソースはこちらに配置します。

modules/common は、 modules/stg, modules/prd でどちらも作成するリソースが置かれます。

  • 例: ログ保存用の S3 Bucket

Don't Repeat Yourself の精神で重複を避ける狙いがありました。

ですが、この構成にした場合、運用上問題が生じました。

これ modules/stg, modules/common どっち? 問題

modules/common/s3.tf で以下の様に S3 Bucket を管理しているとします

resource "aws_s3_bucket" "assets" {
  bucket = "${var.env}.${var.service_name}.assets"

  cors_rule {
    max_age_seconds = 3000
  }
}

ここで stg のみ検証の為、 max_age_seconds = 0 にしたい、としたらどうすると良いでしょう?

var.env == stg ? 1 : 0 は以前にも書きましたが、脳内リソースの消費が激しくなるので使うのを避けたいです。

modules/stg/iam.tf, modules/prd/iam.tf で処理を分ける、 という案ならできそうです。

  • modules/stg/s3.tf
resource "aws_s3_bucket" "assets" {
  bucket = "${var.env}.${var.service_name}.assets"

  cors_rule {
    max_age_seconds = 0
  }
}
  • modules/prd/s3.tf
resource "aws_s3_bucket" "assets" {
  bucket = "${var.env}.${var.service_name}.assets"

  cors_rule {
    max_age_seconds = 3000
  }
}

これで var.env == stg ? 1 : 0 を回避しコード上は回避できました。

但し、お気づきの通り、 tfstate の更新が必要です。

cd envs/stg
terraform state rm module.common.aws_s3_bucket.assets
terraform import module.stg.aws_s3_bucket.assets stg-hoge-assets

cd envs/prd
terraform state rm module.common.aws_s3_bucket.assets
terraform import module.prd.aws_s3_bucket.assets stg-hoge-assets

例では 1 resource のみなので、この程度ですが、複数リソースがある場合は、 複数リソースの state 削除 × 2 (stg,prd) となり、 state の更新処理が非常に面倒です。

コード的にも var.env == stg ? 1 : 0 避けたものの DRY は避けられていない。

そもそも何故分けた?

modules/common が再利用性がある、とし採用しました。

ですが、例題の様に、再利用性がなくなった時のコストが大きく、また、発生しやすいことが運用でわかりました。

だから module やめてみた

同じサービスで stg, prd で再利用性を求める必要が少なく、むしろ、その構成に大きく差分が生じやすい方が運用しやすいことがわかった為、 最初の結論の構成としました。

.
└──envs/
    ├── bootstrap/
    ├── prd/
    └── stg/

module が便利な例

とはいえ、 module の再利用性が発揮される効果は十分にあります。

例: 複数 AWS Account での IAM 管理

AWS Account a,b,c,d,e と複数所持し、 基本、開発者は a で IAM User を発行し、 他 b ~ e は a からスイッチロールできる様、 Role に紐付けする際には便利です。

.
├──envs/
│   ├── a/
│   ├── b/
│   ├── c/
│   ├── d/
│   └── e/
│
└──modules
    ├── backend/
    ├── iam_user/
    └── iam_role/
  • env/a/main.tf
module "backend" {
  source = "../../modules/backend"
  ...
}

module "iam_user" {
  source = "../../modules/iam_user"
  ...
}
  • env/b/main.tf
module "backend" {
  source = "../../modules/backend"
  ...
}

// Switch Role へ紐付け
module "iam_role" {
  source = "../../modules/iam_user"
  ...
}

再利用性がむしろ求められ、 module と相性がとても良いことがわかりました。

ベストプラクティスを探す旅は続く

運用してみて気付く問題があり、またそれを乗り越える度にまた新たな問題に出会います。

ここ最近は 「terraform プロジェクトで Pull Request のレビュー依頼する時についついファイル数盛り盛りになっちゃう問題」がありましたw

「お互いに知ってるいつもの構成だから」という甘い考えがあるとついつい攻撃的なファイル数になっちゃったり。

人のリソースを無為に奪ってしまう行為でもあるので、git cherry-pick して小分けにする様、レビューしやすさも大事な要素だなと考えています。

また、 GitHub Actions で terraform init, plan, fmt を実行し実行結果を PR コメントに追記することで、plan 情報の確認をしやすくしたり、 fmt の違反行為で失敗させたりしています。

  • .github/workflows/terraform.yml
---
name: Terraform
on: [pull_request]

env:
  TF_VERSION: 0.12.24
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

jobs:
  plan:
    name: Plan

    strategy:
      matrix:
        env: [stg, prd]

    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repo
        uses: actions/checkout@v2

      - name: Terraform Init
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.TF_VERSION }}
          tf_actions_subcommand: 'init'
          tf_actions_working_dir: 'envs/${{ matrix.env }}'
          tf_actions_comment: 'true'

      - name: Terraform Plan
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.TF_VERSION }}
          tf_actions_subcommand: 'plan'
          tf_actions_working_dir: 'envs/${{ matrix.env }}'
          tf_actions_comment: 'true'
          args: '-lock=false'

  fmt:
    name: Format

    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repo
        uses: actions/checkout@v2

      - name: 'Terraform Format'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.TF_VERSION }}
          tf_actions_subcommand: 'fmt'
          tf_actions_comment: 'true'

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

CodePipeline で CodeBuild へ環境変数を渡し、上書きすることで CodeBuild を再利用する

terraform で buildspec を管理してみる

  • buildspec.yml
---
version: 0.2

env:
  variables:
    FOO: "${foo}"
    ...
  • codebuild.tf
data "template_file" "buildspec" {
  template = file("buildspec.yml")

  vars = {
    foo = "foo"
  }
}

resource "aws_codebuild_project" "foo" {
  source {
    type      = "CODEPIPELINE"
    buildspec = data.template_file.buildspec.rendered
  }

terraform apply 実行し CodeBuild を作成すると、環境変数 FOO=foo が設定されます。

続きを読む

vscode で Go: Generate Unit Test が便利だった♪

Go でテストを書く際に vscodeGo extension単体テストのフォーマットを簡単に生成できる機能があったので利用すると非常に便利でした。

以下の様な main.go ファイルがあるとします。

package main

func hello(s string) string {
    if s == "" {
        return "world"
    }
    return s
}

関数名をクリックしてポインタを置いて、 Mac だと Command + Shift + p でコマンドパレットが表示されるので

Go: Generate Unit Tests For Function と打ち込んでエンターを押すと

f:id:kenzo0107:20200307221234p:plain

以下 main_test.go が生成されます。

package main

import "testing"

func Test_hello(t *testing.T) {
    type args struct {
        s string
    }
    tests := []struct {
        name string
        args args
        want string
    }{
        // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := hello(tt.args.s); got != tt.want {
                t.Errorf("hello() = %v, want %v", got, tt.want)
            }
        })
    }
}

関数がスネークケース (Test_hello) になるのがやや気になりますが、テスト対象の関数名が小文字で始まる場合にこうなる様です。

テストケースに必要な情報を struct で管理し、 for で順次回すというフォーマットです。

TABLE DRIBEN TESTS

func hello(string) の引数が args で指定されています。

type args struct {
    s string
}

もしテスト対象が func(string, int) なら以下の様に変わってくれます。

type args struct {
    s string
    i int
}

テストを以下の様に書いて、

package main

import "testing"

func TestHello(t *testing.T) {
    type args struct {
        s string
    }
    tests := []struct {
        name string
        args args
        want string
    }{
        {
            name: "return 'hello' if you set 'hello'",
            args: args{
                "hello",
            },
            want: "hello",
        },
        {
            name: "空文字を指定したら world が返ってくる",
            args: args{
                "",
            },
            want: "world",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := hello(tt.args.s); got != tt.want {
                t.Errorf("hello() = %v, want %v", got, tt.want)
            }
        })
    }
}

テストを実行すると name 毎にテストが PASS しているか確認できます。

半角スペースが _ に変換されます。

$ go test -count 1 -v .
                                 
[kenzo.tanaka]$ go test -count 1 -v ./hoge                            
=== RUN   TestHello
=== RUN   TestHello/return_'hello'_if_you_set_'hello'
=== RUN   TestHello/空文字を指定したら_world_が返ってくる
--- PASS: TestHello (0.00s)
    --- PASS: TestHello/return_'hello'_if_you_set_'hello' (0.00s)
    --- PASS: TestHello/空文字を指定したら_world_が返ってくる (0.00s)
PASS
ok      github.com/kenzo0107/hoge      0.212s

便利です♪

ちなみに、 Go: Generate は他にもあります。

f:id:kenzo0107:20200307224351p:plain

vscodegolang のテストを書く際に参考になれば何よりです。 以上です。

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 にしてます。