長生村本郷Engineers'Blog

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

Golang で関数のデフォルト引数を指定する

f:id:kenzo0107:20191105183003j:plain

概要

Ruby で関数のデフォルト引数を設定する場合は以下のように指定できます。

def hoge(a, b = 2)
    c = a + b
end

c = hoge(1)

p c
// c = 3

Golang だと以下の様なデフォルト引数の定義ができません。

func hoge(a, b = 2 int) string {
    c := a + b
    return c
}

こんな時にどうしようかと探っていると、 Functional Option Pattern に当たりました。

Functional Option Pattern

以下 2 記事が有名で必読です。

任意の値を定義する・しないで関数を分けると引数分、メソッドが増え、煩雑になります。

const defaultB = 2

func Hoge(a int) int {
    return a + defaultB
}

func HogeWithB(a, b int) int {
    return a + b
}

こういった煩雑さを解決する Golang の特性を活かした解決法が Functional Option Pattern です。

Functional Option Pattern で書き換え

package main

const (
    defaultB = 2
)

type configs struct {
    b int
}

type Option func(c *configs)

func WithB(v int) Option {
    return func(c *configs) {
        c.b = v
    }
}

var args *configs

func init() {
    args = &configs{
        b: defaultB,
    }
}

func Hoge(a int, options ...Option) int {
    for _, option := range options {
        option(args)
    }

    c := a + args.b

    return c
}

func main() {
    c := Hoge(1)
    fmt.Println(c) // 3

    d := Hoge(1, WithB(9))
    fmt.Println(d) // 10
}

デフォルト引数の指定が効いているのがわかります。

Option という引数に *configs の設定を持つ関数を定義し、 その関数で各値を任意で設定することで、デフォルトの設定を上書いています。

こうすることで、 設定したい任意の値だけを設定し、その他はデフォルト値を参照する、ということができます。

よし完璧だ!と思ったら...テストでこける。。。

func main() {
    fmt.Println(reflect.DeepEqual(WithB(12), WithB(12)) // false
}

WithB は関数型が返るので、 reflect.DeepEqual では false になります。

reflect.DeepEqual のコードを見てみると関数型 (reflect.Func) で Nil でなければ、 false が返るようになってます。

コメントにある // Can't do better than this: が推して知るべし。

https://github.com/golang/go/blob/master/src/reflect/deepequal.go#L126-L131

Functional Option Pattern にもう一手間加える

const (
    defaultB = 2
)

type configs struct {
    b int
}

type Option interface {
    Apply(*configs)
}

type B int

func (o B) Apply(c *configs) {
    c.b = int(o)
}
func WithB(v int) B {
    return B(v)
}

var args *configs

func init() {
    args = &configs{
        b: defaultB,
    }
}

func Hoge(a int, options ...Option) int {
    for _, option := range options {
        option.Apply(args)
    }

    c := a + args.b

    return c
}

func main() {
    c := Hoge(1)
    fmt.Println(c) // 3

    d := Hoge(1, WithB(9))
    fmt.Println(d) // 10

    fmt.Println(reflect.DeepEqual(WithB(12), WithB(12))) // true
}

WithB は int 型の値を返し、 reflect.DeepEqual は true を返します。

Option を interface で定義することで、どの様な型にでも呼び出せるのを利用しつつ、 Apply メソッドを定義して、 configs の上書きを図る、という算段です。

以下 googleapis/google-api-go-client で定義されている Functional Option Pattern が素敵だったので、こちらとても参考になります。 https://github.com/googleapis/google-api-go-client/blob/master/option/option.go

まとめ

軽い気持ちで関数のデフォルト引数の設定どうやるんだろうか? と調べたら思わぬ展開になって、奥深さを感じました。

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

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

Datadog Agent for ECS Launch Type=EC2

f:id:kenzo0107:20191017124709p:plain

概要

ECS 起動タイプ EC2 にてタスク定義に datadog/agent:latest を設定したが メトリクスが取得できない事象がありました。

Infrastructure > Containers には datadog/agent:latest を設置したタスク定義内のコンテナ情報は一覧に表示されてますが、メトリクスが取れていない、という状況でした。

結論

https://docs.datadoghq.com/json/datadog-agent-ecs.json 参考に、以下の様な volume mount の設定が必要でした。

  datadog:
    image: datadog/agent:latest
    environment:
      DD_API_KEY: ${DD_API_KEY}
    logging:
      driver: awslogs
      options:
        awslogs-group: ${LOG_GROUP}
        awslogs-region: ${REGION}
        awslogs-stream-prefix: datadog
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /proc/:/host/proc:ro
      - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro

ドキュメントよく読もう、を身につまされる想いでした。

参考

Amazon Elastic Container Service (ECS)

terraform 0.11 系に対応した GitHub Actions 作った & tflint も入れてみた♪

概要

Terraform 用の GitHub Actions として hashicorp 社にて以下リポジトリが用意されています。

https://github.com/hashicorp/terraform-github-actions

ですが、上記のリポジトリでは、 terraform の最新版 (2019-09-30 時点 0.12.9) にのみ適用しています。

hashicorp/terraform-github-actions を folk して
0.11 系がなかった為、0.11 系に対応した terraform-github-actions を以下リポジトリに作成しました。

github.com

terraform の古いバージョンについての対応は、 terraform 公式に以下リンクにて記載があります。

Terraform Versions - Terraform GitHub Actions - Terraform by HashiCorp

古いバージョンは folk して自分で作ってね♪ と書いてあります。

ついでに

以下追加してみました。

使い方

以下のような terraform プロジェクトがあるとします。

├── envs
│   ├── prd
│   │   ├── backend.tf
│   │   ├── main.tf
│       ...
│   │   └── variable.tf
│   └── stg
│       ├── backend.tf
│       ├── main.tf
│       ...
│       └── variable.tf
└── modules
    ├── ...

GitHub Actions 設定方法

以下 2 ファイルを root ディレクトリに配置します。

├── .github
│   └── workflows
│       ├── main.yml
│       └── fmt.yml 
│
├── envs
│   ├── prd
│   │   ├── backend.tf
│   │   ├── main.tf
│       ...
│   │   └── variable.tf
│   └── stg
│       ├── backend.tf
│       ├── main.tf
│       ...
│       └── variable.tf
└── modules
    ├── ...

.github/workflows/main.yml

name: Terraform
on: [pull_request]

jobs:
  on-pull-request:
    name: On Pull Request

    strategy:
      matrix:
        env: [stg, prd]

    runs-on: ubuntu-latest

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

    - name: ${{ matrix.env }} Terraform Init
      uses: kenzo0107/terraform-github-actions/init@v0.6.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: './envs/${{ matrix.env }}'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    - name: ${{ matrix.env }} Terraform Validate
      uses: kenzo0107/terraform-github-actions/validate@v0.6.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: './envs/${{ matrix.env }}'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    - name: ${{ matrix.env }} Terraform Lint
      uses: kenzo0107/terraform-github-actions/lint@v0.6.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: './envs/${{ matrix.env }}'

    - name: ${{ matrix.env }} Terraform Plan
      uses: kenzo0107/terraform-github-actions/plan@v0.6.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: './envs/${{ matrix.env }}'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY }}

ちょっと解説

Pull Request をトリガーに実行されます。
on: [pull_request]
リポジトリをチェックアウト
    - name: Checkout Repo
      uses: actions/checkout@v1
matrix で stg, prd を並列実行します。
    strategy:
      matrix:
        env: [stg, prd]
terraform init, validate, lint, plan
    - name: ${{ matrix.env }} Terraform Init
...
    - name: ${{ matrix.env }} Terraform Validate
...
    - name: ${{ matrix.env }} Terraform Lint
...
    - name: ${{ matrix.env }} Terraform Plan

言わずもがな、以下を実行しています。

  • terraform init
  • terraform validate
  • terraform lint
  • terraform plan

kenzo0107/terraform-github-actions/init@v0.6.0 が terraform v0.11.14 に対応しています。

init, validate, lint は指摘事項がある場合は、Pull Request にコメントしてくれます。

terraform plan は必ず実行結果を貼り付けてくれます。

f:id:kenzo0107:20190930221527p:plain

これはレビュワーに有難い機能です。

コードの変更内容と terraform plan 内容の整合性が取れているかどうかが重要なレビュー観点となる為です。

実行パス指定

パスを移動してから terraform plan 等を実行したい場合に以下環境変数に指定します。

TF_ACTION_WORKING_DIR: './envs/${{ matrix.env }}'

kenzo0107/terraform-github-actions/plan@v0.6.0 では、
上記の設定した TF_ACTION_WORKING_DIR を terraform plan 実行内容と共に表示するようにしています。((hashicorp/terraform-github-actions では、実行したディレクトリパスは Pull Request コメントに乗らない様になってます。))

これは terraform plan 実行内容から stg, prd どちらで実行したかわかりずらく、レビュワーを困惑させる可能性がある為です。

secrets の設定
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY }}

secrets.GITHUB_TOKEN は secrets にトークンを設定する必要がありません。

個人的に GitHub が実行する CI/CD だからこそ実現できる秘匿性のある管理方法で、 Actions の大きな利点だと思います。

その他は、 settings > secrets で設定します。

f:id:kenzo0107:20190930223443p:plain

.github/workflows/fmt.yml

terraform fmtstg, prd 等は関係なく、リポジトリのルートディレクトリで実行するので
.github/workflows/main.yml とは分けました。

こちらも Pull Request をトリガーとして実行されます。

name: Terraform
on: [pull_request]

jobs:
  on-pull-request:
    name: On Pull Request

    runs-on: ubuntu-latest

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

    - name: Terraform fmt
      uses: kenzo0107/terraform-github-actions/fmt@v0.6.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

総評

GitHub Actions に触れてみようとした題材として非常に簡易だったのでとっつきやすかったです。

そして何より、手間かけましたが、 0.12 系に対応した方が早かったかもしれない...

やんごとなき理由で 0.11 にしている場合以外は、最新に追従した方が良いですね♪

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

AWS ApplicationLoadBalancerリスナールールで特定 IP 以外をメンテナンスページ表示

f:id:kenzo0107:20190929233947p:plain

概要

AWS で運用している Web サービスでメンテナンスが必要となり、ALB でメンテ切り替えをした際の対応をまとめました。

手順

ALB Listener 一覧からルール変更をします。*1

f:id:kenzo0107:20190929232804p:plain

その後、

  1. 送信元IP = 社内IP (ex. 11.22.33.44/32 ) → default のTargetGroup へ転送 で「保存」
  2. 社内IP以外の送信元 IP 全て ( 0.0.0.0/0 ) → 503 text/html メンテ文言をレスポンス で 「保存」

f:id:kenzo0107:20190929231656p:plain

以上で 社内 IP は、通常通りアクセス可、それ以外はメンテナンスページを表示させることができました。

まとめてルールを追加して保存が出来ず、1つずつルール追加で保存になります。

レスポンスできる Content-Type って何があるの?

Content-Type に application/json 等も返せるので、 API サーバのメンテ時にはこちらを利用して文言を渡しました。

f:id:kenzo0107:20190929232354p:plain

ちょっとした注意

最大文字数が 1024 文字でした♪ f:id:kenzo0107:20190929232942p:plain

CSSレスポンス本文 に追加すると文字数 1024 を超えてしまいそうなので、S3 にアップロードし公開し、そちらを参照するようにしたりしました。

*1:今回 2 ポートのみ解放しており、80 は 443 に転送してるので、443 のみ対応しました。

Ansible FAILED! => {"msg": "to use the 'ssh' connection type with passwords, you must install the ssh pass program"} on MacOS

MacOS で Ansible を利用した所、掲題のようなエラーが発生しました。

その際の対策です。

brew install http://git.io/sshpass.rb

インストール完了まで少々時間かかりました。

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

参照

everythingshouldbevirtual.com

puppeteer で radio ボタンチェック

f:id:kenzo0107:20190906230848p:plain
puppeteer radio button

概要

puppeteer というスクレイピングツールで radio ボタンをチェックする為の備忘録です。

例題

以下のような radio ボタングループがあるとします。

<input type="radio" name="maker" value="1"> クリスタル映像
<input type="radio" name="maker" value="2"> ポセイドン企画

答え

クリスタル映像 を選びたい場合は、以下のようにします。

page.evaluate でブラウザ内での操作結果を返すようにします。
ブラウザ内なので、 document.querySelector が使えます。
そこで、 checked = true しています。

const selectedRadioSelector = `input[type="radio"][value="1"]`
await page.evaluate(
  s => (document.querySelector(s).checked = true),
  selectedRadioSelector
)

失敗例

click 処理は軒並み失敗しました。

page.WaitFor すると成功する、という記事を見ましたが、自身の環境 puppeteer (version=1.19.0) では、失敗してしまいました。

  • page.click
const selectedRadioSelector = `input[type="radio"][value="1"]`
page.click(selectedRadioSelector)
  • radio 要素を捕まえて、click
r = page.$(selectedRadioSelector)
r.click()
  • document.querySelector().click()
const selectedRadioSelector = `input[type="radio"][value="1"]`
await page.evaluate(
  s => (document.querySelector(s).click()),
  selectedRadioSelector
)

応用

以下のような radio ボタングループがあるとします。
上のと比べると label タグが追加されてます。

<input type="radio" id="group1" name="maker" value="1"> <label for="group1">クリスタル映像</label>
<input type="radio" id="group2" name="maker" value="2"> <label for="group1">ポセイドン企画</label>

このラベルを正規表現でマッチする方をチェックしてみます。

  • "映像" という文字を含む方をチェックする
const regex = "映像"
const regexpLabel = new RegExp(regex, 'g')

const r = await page.$$('input[type="radio"]')

label: for (const i in r) {
  // radio ボタンの id 要素
  const id = await (await r[i].getProperty('id')).jsonValue()
  // radio ボタンの value 要素
  const value = await (await r[i].getProperty('value')).jsonValue()

  // ラベル textContent 取得 ("クリスタル映像", "ポセイドン企画" を取得)
  const label = await page.$(`label[for="${id}"]`)
  const labelContent = await (await label.getProperty(
    'textContent'
  )).jsonValue()


  // ラベルの textContent が "映像" を含む場合、 true
  if (labelContent.match(regexpLabel)) {
    const selectedRadioSelector = `input[type="radio"][value="${value}"]`
    await page.evaluate(
      s => (document.querySelector(s).checked = true),
      selectedRadioSelector
    )
    // radio ボタンにチェック入れたので処理終了
    break label
  }
}

まとめ

非常につまづきやすい点だったので備忘録として残しました。

参考になれば幸いです。

WEB+DB PRESS Vol.109

WEB+DB PRESS Vol.109

  • 作者: 佐藤歩,加藤賢一,原一成,加藤圭佑,大塚健司,磯部有司,村田賢太,末永恭正,久保田祐史,吉川竜太,牧大輔,ytnobody(わいとん),前田雅央,浜田真成,竹馬光太郎,池田拓司,はまちや2,竹原,原田裕介,西立野翔磨,田中孝明
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/02/23
  • メディア: 単行本
  • この商品を含むブログを見る

puppeteer on Docker

概要

備忘録です。

Puppeteer をローカル環境を汚さず、 Docker 環境上で実行すべくまとめました。

サンプルスクリプトexample.comスクリーンショットを取得する、というシンプルなものです。

github.com

WEB+DB PRESS Vol.109

WEB+DB PRESS Vol.109

  • 作者: 佐藤歩,加藤賢一,原一成,加藤圭佑,大塚健司,磯部有司,村田賢太,末永恭正,久保田祐史,吉川竜太,牧大輔,ytnobody(わいとん),前田雅央,浜田真成,竹馬光太郎,池田拓司,はまちや2,竹原,原田裕介,西立野翔磨,田中孝明
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/02/23
  • メディア: 単行本
  • この商品を含むブログを見る