Github ActionsでクロスコンパイルしてGithub Releaseにアップロードする

追記: GoReleaserのbabarotさんの記事があったので紹介。GoReleaser使ったほうが手軽でよさそう。 Go で書いた CLI ツールのリリースは GoReleaser と GitHub Actions で個人的には決まり | tellme.tokyo

追記: GoReleaserのプロジェクトがGithub Actionを公開したようなのでこちら使ってみるといいかもしれません。 https://github.com/goreleaser/goreleaser-action

この前kube-promptのリリースでミスをしてしまいissueが立て続けに2件上がったことがあったのですが、Github Actionsが自分のリポジトリで使えるようになったのでリリースを自動化することにしました。

触ってみてわかったのですが、↓の記事を書いたときとは多くの変更がありました。workflowの記述フォーマットがyamlに変更されていたり、Azure Pipelinesを裏側で使うようになったことでSupported virtual environmentsにmacOSWindowsが増えたり、その影響でCustom actionはDockerコンテナベースではなくJavaScriptアプリケーションとして記述できるようになったりしています。JavascriptでCustom actionを記述するのはまた今度にして、とりあえず今回はyaml形式のworkflowを使っていきます。

nwpct1.hatenablog.com

ロスコンパイルとGithub releasesへのアップロード

kube-promptではこんな感じでいくつかバイナリをGithub Releaseに含めています。今回は tagが作成されたタイミングでフックしてこれらのbinaryをcross compile, zipで圧縮してGithub Release画面を作成します。

f:id:nwpct1:20190820063438p:plain
kube-prompt github release画面

まずworkflowのevent trigger指定します。 今回からcronのように定期実行もできるようになっていますが、基本的には GithubのWebhook event を起動トリガーにすることがほとんどかと思います。git tagのcreateイベントをトリガーにしたいので次のようになります 1

on:
  create:
    tags:
    - v*.*.*

次にjobs propertyより実際にGithub Actionsで行いたい操作を記述します。 そんなに長くなくて↓のような感じです。

jobs:
  release:  # job_id
    name: Build
    runs-on: ubuntu-latest
    steps:

    - name: Set up Go 1.12
      uses: actions/setup-go@v1
      with:
        version: 1.12
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@master

    - name: Build
      env:
        GO111MODULE: on
        GOPATH: /home/runner/work/
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        export CREATE_EVENT_REF_TYPE=$(jq --raw-output .ref_type "$GITHUB_EVENT_PATH")
        export TAGNAME=$(jq --raw-output .ref "$GITHUB_EVENT_PATH")
        if [ "$CREATE_EVENT_REF_TYPE" != "tag" ]; then echo "ref_type is not a tag: ${CREATE_EVENT_REF_TYPE}" && exit 78; fi
        make cross
        go get -u github.com/tcnksm/ghr
        $GOPATH/bin/ghr -n ${TAGNAME} -b "ʕ◔ϖ◔ʔ Release ${TAGNAME}" -draft ${TAGNAME} pkg/
  • jobs.<job_id>.runs-on に実行環境を指定することができて、今回からmacOSWindowsが追加されました。kube-promptはCGOの依存とかもなく普通にクロスコンパイルできるので、一番速度や動作が安定してそうなubuntuにしています。他のCIサービスと同様に、matrixも組めるのでテストとか必要に応じてやっておくといいかと思います。
  • GITHUB_TOKENは以前までだと自分でsecretsに登録する必要がありましたが、今回からはその必要も無いようです。yaml内で ${{secrets.GITHUB_TOKEN}} とするだけで取り出せます。
    • どこまでが古いドキュメントにしか書かれてなくて、どこまでが新しいドキュメントに書かれてるのか分からずこれに気づくの少し時間がかかってしまいました。
  • $GITHUB_EVENT_PATH にはWebhook eventのJSONファイルがそのまま入っています。今回は create イベントにフックしているので、 こちら にあるように、 ref_type フィールドからtag名、 ref フィールドからrevision hashが取り出せます。
  • make cross の処理はこんな感じです https://github.com/c-bata/kube-prompt/blob/4e31373f5a10443746b4ab93d584ea0ec17d4e61/Makefile#L31-L43
  • 最後にghrをgo getしてきてアップロードします。Githubのtokenは GITHUB_TOKEN 環境変数を見てくれているので特にコマンド実行時には指定する必要がありません。
  • ~ちなみに actions/setup-go$GOPATH が空で $GOPATH/bin にもパスが通っていないようなので、明示的に指定しました。直したほうがいい気がしますが、JavaScriptベースのCustom actionとして定義されているらしく、まだ自分も調べてないところだったので今度必要になって調べることがあればついでに提案してみたいとおもいます。~
    • uses で何もDockerイメージが指定されていないstepは、 runs-on で指定したホスト上でそのまま動いているかもです。 setup-go はそのホスト上にGoの環境を構築してくれているということな気がします。ただ環境変数とかは引き継げないのかな?
    • https://github.com/actions/setup-go

全体像はこんな感じです。試しに v0.0.1 みたいなタグをpushして無事にバイナリ付きでGithub Releaseが作成されることは確認しました。

https://github.com/c-bata/kube-prompt/blob/master/.github/workflows/release.yml

おまけ: golangci-lintとgo testの実行

Goptunaという最近開発してるプロジェクトはまだCIを導入していなかったので、Lintとテストを実行できるようにしました。 他のプロジェクトでもほとんどそのまま使える点も多いと思うのでよければ参考にしてみてください。

name: Run tests and lint checks
on:
  pull_request:
    branches:
    - master
jobs:
  lint:
    name: Lint checking on Ubuntu
    runs-on: ubuntu-latest

    steps:
    - name: Set up Go 1.12
      uses: actions/setup-go@v1
      with:
        version: 1.12
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@master

    - name: Running golangci-lint
      env:
        GO111MODULE: on
        GOPATH: /home/runner/work/
      run: |
        go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
        GOCILINT=${GOPATH}/bin/golangci-lint make lint
  test:
    name: Test on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macOS-latest]

    steps:
    - name: Set up Go 1.12
      uses: actions/setup-go@v1
      with:
        version: 1.12
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@master

    - name: Running go tests
      run: make test

https://github.com/c-bata/goptuna/blob/master/.github/workflows/run-tests.yml

追記:

Go関係ないですが、ついでにSphinxのドキュメントをビルドしてGCSにアップロードするscript書いたのでgcloudとかgsutil使いたい方とか参考にしたい方はどうぞ。本当は https://github.com/actions/gcloud を使おうと思ったのですが柔軟性なくて今回の用途には使えなかったのでやめました。

Github Actions Workflow to build your sphinx documentation and upload it to Google Cloud Storage. https://gist.github.com/c-bata/ed5e7b7f8015502ee5092a3e77937c99

おわりに

travis CIを長く使ってきましたが、Windowsの動作が不安定だったり苦労している点が結構ありました。 ActionsはAzure Pipelinesがバックエンドにあることもあり、立ち上がりがかなり速い印象です。 forkしたらそのままActionsが使えるというメリットもあるので、OSSでは積極的に使っていこうと思います。


  1. ちなみに複数のイベントにトリガーさせることもできて、 この例 がわかりやすかったです。

LLVMのauto-vectorizationとc2goasmによるGo Plan9 Assemblyの生成によるSIMD最適化

InfluxDBの開発チームはApache Arrowの技術に注目していて、ArrowのGo実装の開発にも積極的に参加しています。Stuart Carine (InfluxDBの開発チームメンバー)がApache ArrowのGo実装に取り入れたc2goasmとLLVMを使った最適化が以前話題になりました。

www.influxdata.com

c2goasm はClangで生成したアセンブリをGo Plan9 Assemblyに変換できる汎用的なコマンドラインツールです。しかしその活用事例はほとんどが、Goコンパイラーが行っていないSSEやAVX命令を用いたSIMD最適化です。この記事ではAVX2によるSIMD演算を実現するための c2goasm の使い方を整理します。最終的にどれくらい速くなるかでいうとfloat64のsum演算は10x以上高速化しました。

$ go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/c-bata/sample-c2goasm
BenchmarkSumFloat64_256-4              5000000           282 ns/op
BenchmarkSumFloat64_1024-4             1000000          1234 ns/op
BenchmarkSumFloat64_8192-4              200000         10021 ns/op
BenchmarkSumFloat64_AVX2_256-4        50000000            23.5 ns/op
BenchmarkSumFloat64_AVX2_1024-4       20000000            95.9 ns/op
BenchmarkSumFloat64_AVX2_8192-4        2000000           904 ns/op
PASS
ok      github.com/c-bata/sample-c2goasm    10.911s

c2goasm

c2goasm の使い方は次のとおりです。

  1. C/C++で記述した関数からClangでアセンブリを生成
  2. Goのバインディング用のシグネチャを定義
  3. c2goasmのコマンドラインツールでGo Plan9 Assemblyを生成

ツールとしての作り込みが少し雑な印象はありハマりどころもありますが、この3つのステップを踏めばSIMDを使って最適化したC/C++のコードを少ないオーバーヘッドでGoから呼び出せます。

CgoによるCの関数呼び出し

Goにおいてデファクトスタンダードとなっている cgo は性能面において優れたソリューションではありません 1 。c2goasmで生成されたGo Plan9 Assemblyのサブルーチン呼び出しは、他のGoの関数呼び出しと同程度の効率で実行できるようです。

GithubリポジトリのREADMEにはcgoとの性能比較が紹介されています。 https://github.com/minio/c2goasm#benchmark-against-cgo

Cのコードの記述

float64のarrayに入った値の合計値を計算する関数を用意します。 SIMDによる演算で合計値の計算をするのは少しアルゴリズム的な工夫が必要です。

#include <immintrin.h>

void sum_float64_avx_intrinsics(double buf[], size_t len, double *res) {
    __m256d acc = _mm256_set1_pd(0);
    for (int i = 0; i < len; i += 4) {
        __m256d v = _mm256_load_pd(&buf[i]);
        acc = _mm256_add_pd(acc, v);
    }

    acc = _mm256_hadd_pd(acc, acc); // a[0] = a[0] + a[1], a[2] = a[2] + a[3]
    *res = _mm256_cvtsd_f64(acc) + _mm_cvtsd_f64(_mm256_extractf128_pd(acc, 1));
}

このようにintrinsicsを使って自分でかくこともできますが、実装が少し大変です。 LLVMのAuto-Vectorization にこの辺の最適化は任せられるなら楽ができます。

void sum_float64(double buf[], int len, double *res) {
    double acc = 0.0;
    for(int i = 0; i < len; i++) {
        acc += buf[i];
    }
    *res = acc;
}

これをClangでコンパイルしてみます。

$ clang -S -mavx2 -masm=intel -mno-red-zone -mstackrealign -mllvm -inline-threshold=1000 -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -c sum_avx_intrinsics.c

オプションはこの辺を参考にしてください。

実行すると ~.sアセンブリファイルが生成されるので中を見るとこのような感じです。

 .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 14
    .intel_syntax noprefix
    .globl  _sum_float64            ## -- Begin function sum_float64
    .p2align    4, 0x90
_sum_float64:                           ## @sum_float64
## %bb.0:
    ...
    vxorps  xmm0, xmm0, xmm0
    mov qword ptr [rsp + 40], rdi
    mov dword ptr [rsp + 36], esi
    mov qword ptr [rsp + 24], rdx
    vmovsd  qword ptr [rsp + 16], xmm0
    ...

vmovsdといった命令やレジスタ xmm0 を利用していることからLLVMSIMD命令を活用していることがわかります。サブルーチン名 _sum_float64 はGoの関数定義で必要なので覚えておきます。

※ xmm0レジスタは128 bitsしかないレジスタのため、64 bitsを消費するdoubleの演算は2要素しか一度に演算できません。おそらくSSEの命令を利用しています。ここで ymm0 など128 bitsのレジスタを使い AVX2の命令を呼び出してほしいのですが、Clangのバージョンや最適化オプションによりうまくいったりイカなかったりするようです。詳細はGithubをご覧ください。

Goの関数定義

package c2goasm_sample

import "unsafe"

//go:noescape
func __sum_float64(buf, len, res unsafe.Pointer)


func SumFloat64Avx2(a []float64) float64 {
    var (
        p1 = unsafe.Pointer(&a[0])
        p2 = unsafe.Pointer(uintptr(len(a)))
        res float64
    )
    __sum_float64(p1, p2, unsafe.Pointer(&res))
    return res
}

シグネチャアセンブリ内のサブルーチン名に _ を追加したものです。今回のようにもしサブルーチン名が _ から始まっていたら、 __ で始まることに注意してください。

https://github.com/minio/c2goasm/blob/0325a40cfd1fc6a5097e69eaf0292990eb6cee6a/arguments.go#L85

c2goasm実行

$ go get -u github.com/minio/asm2plan9s
$ go get -u github.com/minio/c2goasm
$ go get -u github.com/klauspost/asmfmt/cmd/asmfmt
$ c2goasm -a -f _lib/sum_avx_intrinsics.s sum_avx.s

これでGoアセンブリが生成されます。第2引数が出力ファイルですが、Goのファイル名を sum_avx.go としたときは拡張子を .s に変えただけの sum_avx.s を指定します。 https://github.com/minio/c2goasm/blob/0325a40cfd1fc6a5097e69eaf0292990eb6cee6a/c2goasm.go#L252

ここまでが一連の流れです。

ベンチマーク

Pure Goで実装した次の関数とベンチマークにより比較します。

package c2goasm_sample

func SumFloat64(a []float64) float64 {
    var sum float64
    for i := range a {
        sum += a[i]
    }
    return sum
}

ベンチマークのコードは次の通り。

package c2goasm_sample_test

import (
    "math/rand"
    "testing"
)

func init() {
    rand.Seed(0)
}

func initializeFloat64Array(n int) []float64 {
    var max float64 = 1024
    var min float64 = 0
    x := make([]float64, n)
    for i := 0; i < n; i++ {
        x[i] = rand.Float64() * (max - min) + min
    }
    return x
}

func benchmarkFloat64Sum(b *testing.B, n int) {
    x := initializeFloat64Array(n)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        SumFloat64(x)
    }
}

func benchmarkFloat64SumAvx2(b *testing.B, n int) {
    x := initializeFloat64Array(n)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        SumFloat64Avx2(x)
    }
}

func BenchmarkSumFloat64_256(b *testing.B) {
    benchmarkFloat64Sum(b, 256)
}

func BenchmarkSumFloat64_1024(b *testing.B) {
    benchmarkFloat64Sum(b, 1024)
}

func BenchmarkSumFloat64_8192(b *testing.B) {
    benchmarkFloat64Sum(b, 8192)
}

func BenchmarkSumFloat64_AVX2_256(b *testing.B) {
    benchmarkFloat64SumAvx2(b, 256)
}

func BenchmarkSumFloat64_AVX2_1024(b *testing.B) {
    benchmarkFloat64SumAvx2(b, 1024)
}

func BenchmarkSumFloat64_AVX2_8192(b *testing.B) {
    benchmarkFloat64SumAvx2(b, 8192)
}

実行結果

$ go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/c-bata/sandbox-go/c2goasm
BenchmarkSumFloat64_256-4                5000000               277 ns/op
BenchmarkSumFloat64_1024-4               1000000              1205 ns/op
BenchmarkSumFloat64_8192-4                100000             10401 ns/op
BenchmarkSumFloat64_AVX2_256-4           2000000               768 ns/op
BenchmarkSumFloat64_AVX2_1024-4           500000              2872 ns/op
BenchmarkSumFloat64_AVX2_8192-4           100000             23946 ns/op
PASS
ok      github.com/c-bata/sandbox-go/c2goasm    10.474s

遅くなっちゃった...

追加調査

Apache Arrowでの利用事例がある以上、なにかコンパイラーオプションとかが理由で性能が十分に引き上げられていない。気になってる点は次のあたり

追記: うまくいきました。

Clang-7.0.1を利用する際には、次のコンパイルオプションを使用してください。

$ /usr/local/Cellar/llvm/7.0.1/bin/clang -S -O2 -mavx2 -masm=intel -mno-red-zone -mstackrealign -mllvm -inline-threshold=1000 -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -c sum_float64.c

追記: SSEのコードはなぜ遅くなっていたのか。

SSEでも遅くはならないんじゃないかなというのが気になっていたけれど、なんとなく原因がイメージついた気がします。 InfluxDataの記事にあるとおりsumの計算を最適化してCで直接書くとAVX2のコードはこんな感じになります。

void sum_float64_avx_intrinsics(double buf[], size_t len, double *res) {
    __m256d acc = _mm256_set1_pd(0);
    for (int i = 0; i < len; i += 4) {
        __m256d v = _mm256_load_pd(&buf[i]);
        acc = _mm256_add_pd(acc, v);
    }

    acc = _mm256_hadd_pd(acc, acc); // a[0] = a[0] + a[1], a[2] = a[2] + a[3]
    *res = _mm256_cvtsd_f64(acc) + _mm_cvtsd_f64(_mm256_extractf128_pd(acc, 1));
}

重要なのはこの _mm256_hadd_pd の処理で こちらの説明にあるように詰め込まれている要素の加算 を担当します。128 bitsしかないSSE用のレジスタ xmm0 とかだと8 bytes 必要な double の値を詰め込んでも2つしかのらないので、それにたいして hadd_pd みたいな処理(これはAVX2の処理ですがそれに相当する処理がSSEにもあるのかと思います) をすると全く並列化されてないことになります。 細かいところはアセンブリが使ってる命令をみればはっきりしそうです。


  1. GoのポインタをCに渡せないようにポインタのバリデーション処理が走るなど、GC側の都合による性能劣化が発生するようです。詳細は Why cgo is slow @ CapitalGo 2018 - Speaker Deck をご覧ください。

Github Actionsでbranch作成/削除にフックしてFeature環境を構築する

最近Github Actionsを触る機会があったのですが、まだ自分のgithub accountはbetaのwait list待ちで業務で使ってるrepostioryでしか使えないので、使い方とかポイントを忘れないようにメモ。ついでにいくつか公式のactionにPR送ったり、KubernetesIngress Rulesを編集するためのActionを公開していたりもするので、こちらも紹介します。

Feature環境の自動作成

業務では開発中の機能を手軽にDev環境で確認するために、特定の命名規則に従ったブランチ名でGithubにpushすると、自動でdev環境のKubernetesクラスターにリリースしIngressでエンドポイントを用意して閲覧できるようにしています。これ自体はそんなに珍しくなくて検索するといくつか同じような記事が見つかります。

これまでもSpeeeさんの事例のようにwebhook eventを監視してoperationを行うサーバーを用意して解決することはできました。弊社ではCircle CIなどでfeature環境を作成したりもしています。 ただSpeeeさんの事例では自分たちでサーバーを用意して運用しないといけません。また弊社がこれまでやっていたようにCIサービスでfeature環境を作成する場合にはbranchの削除にトリガーできません。社内の別のチームではbotを立ててbranchの削除を監視してたりもしたみたいですが、これだけのためにbotたてるのも少し手間になります。

GithubのあらゆるイベントにトリガーできるGithub Actionsを使えば、branchがpushされたときにfeature環境を作成し、branchが削除されたときにfeature環境を削除するといったオペレーションを、自分たちでサーバーを管理することなく実現できます。業務ではKubernetesを使っているので、全体像としては次のような感じになります。

f:id:nwpct1:20190222231923j:plain
ざっくりした全体像。実際にはmicroserviceが30以上あり、それらのtraffic routingまではコントロールできる環境を用意できていないので、webのツールや動画配信サーバー(HLS playlist, MPEG-DASH manifest, MPEG-2 TSやFragmented MP4といったメディアセグメントを提供)など一部のコンポーネントだけがこの機能を有効にしています。

  1. feature-abc のように feature-*命名規則に従ってbranchを作成しGithubにPush
  2. Kubernetes Deploymentをbranch用に作成
  3. Kubernetes Serviceをfeatureブランチ用に新規で作成 1
  4. Ingress (ingress-gce) でエンドポイント作成 2
  5. Google Cloud DNSのRecordsetsの作成
  6. https://feature-abc-webapp.foo.com でアクセスして動作確認

Github Actions

基本的な使い方は公式ドキュメントをみてください。

https://developer.github.com/actions/

いくつか悩んだり調べた中でメモしておきたいポイントを中心に残します。

credential情報の管理

外部に漏れては困る情報は Secret によりGithub RepositoryのSettingsで指定できます (参照 https://developer.t.com/actions/creating-workflows/storing-secrets/)。 Actionsを追加する際にも「Secret」というフィールドがありますが、そこから指定してもやってることは同じです。

実は1月頃に一度Github Actionsの利用を検討したことがあったのですが、当時はまだLimited Public Beta期間中でProduction Secretsを保存してはいけませんでした。 今回はLimited Public Betaがとれたため、改めてGithub Actionsを調査することにしました。

f:id:nwpct1:20190222164218p:plain
github-action-secrets

ブランチ名のフィルター

pushイベントに対してすべてトリガーしてほしいわけではなく、特定の命名規則に従ったbranchでのみ実行してほしいものです。GITHUB_REFS という環境変数の中に refs/head/feature-A のような形式でブランチ名やタグ名が入っています。 refs/head/のprefixを削除して利用すればOKです。公式で用意されている↓のactionがこの操作をしてくれているのでこちらを利用しましょう。

bin/filter at master · actions/bin · GitHub

f:id:nwpct1:20190222164042p:plain
github-actions-filter

ただbranch削除時のfilterにはこの方法が使えません。 delete triggerは GITHUB_REF にdefault branchつまりmasterを指定が指定されています。環境変数からbranch名を取り出すことはできません。そのかわり GITHUB_EVENT_PATH 環境変数が示す場所にWebhookのevent情報がそのままjson形式で入っています。

delete でtriggerしたときは DeleteEvent の形式なので、 ref フィールドよりブランチ名が取り出せます。公式で用意してほしい機能なので↓にPRをだしました。

github.com

まだマージされていないので c-bata/bin/filter@master を指定して使っています。 deleted_branch feature-* のようにargsを指定すれば使えます。

マージされたので公式の actions/bin/filter@master を使用してください。そちらには deleted_tag フィルターも追加しています。

f:id:nwpct1:20190227010255p:plain
c-bata/actions/filter deleted_branch feature-*

GCPのService Accountからgcloudの認証を行う

f:id:nwpct1:20190227010437p:plain
Activate gcloud command using service-account

公式で用意されている↓のactionを用いることで実現できます。Service AccountはSecret GCLOUD_AUTHbase64 encodeしたservice accountのjsonファイルを与えればOKです (ex: base64 ./service-account.json )。

gcloud/auth at master · actions/gcloud · GitHub

少し驚いたのですがgcloudコマンドの実行は別のactionとして定義し、↓を利用して実行します。

gcloud/cli at master · actions/gcloud · GitHub

gcloudコマンドのcredential情報は、Homeディレクトリ以下に作成されます。Github Actionsは裏側で /github/workspace を常にマウントしそこをHomeディレクトリに設定しているようです。このディレクトリは次のactionでもそのままの状態で引き継がれます。gcloudの認証とgcloudコマンドの実行は別のactionでやるのがGithub Actionらしいやり方なようです。

kubectlの実行

gcloud authができるようになったので、kubernetes clusterのcredentials情報を取得してkubectlを実行します。既存でよさそうなものがなかったのですが、https://github.com/actions/gcloud で管理されるのがみんな幸せかと思うので PRを出しました。

Add github action for kubectl by c-bata · Pull Request #9 · actions/gcloud · GitHub

gcloudコマンドにならって、PROJECT_IDやZONE、K8S clusterをセットするactionとkubectlの実行用actionを分割しました。まだマージはされていないので c-bata/gcloud/kubectl-config@master および c-bata/gcloud/kubectl@master を指定して使っています。

f:id:nwpct1:20190227010829p:plain

ingress rules書き換えツールの実行

deploymentsやserviceをfeature環境ごとに個別に作っていたようにingressもfeature環境ごとにつくることもできるのですが、大きいチームだったので大量にFeature環境が立ち上がりLoadbalancerの作成上限に引っかかったことがありました。そのため全てのfeature環境で1つのingressを使いまわし、Spec.Rulesに振り分け設定を追加して webapp.foo.comfeature-a-webapp.foo.com を振り分けています。管理の都合上もその方がいいかなと思います。IngressのSpec.Rulesの編集にはもともとnodeで書かれたscriptが社内で使われていたのですが、kubectlのwrapperになっていてclient-goが使えるGoで書いたほうが色々楽だったので今回書き直しました。↓で公開しています。

GitHub - abema/github-actions-ingress-rules-editor: Edit ingress rules to build feature environments automatically on Github Actions.

c-bata/go-actions

github actionsの調査もかねてutilityライブラリ作りました。 正直使うほどでもないシーンが多いと思いますが、よければ使ってみてください。

GitHub - c-bata/go-actions: go-actions provides the utilities for Github Actions.

面倒だったこと

branchを削除したときにはmasterブランチのmain.workflowが参照され、実行されます。そのためbranchの削除にtriggerして何らかの処理を行いたいとき、一度そのbranchをmasterにマージして削除しないと動作確認ができません。

Add deleted_branch and deleted_tag filters by c-bata · Pull Request #42 · actions/bin · GitHub みたいな機能はとりあえず書いてmasterにマージしてbranchを削除して、問題があればまたbranchを作ってmasterにマージしてbranchを削除しないと確認できずmasterのcommit logが結構汚れます。仕事のrepositoryでそれをやることになったので申し訳ないなと思いながら開発してました。

おわりに

はやく自分のrepositoryでも使ってみたい


  1. 執筆時点では ingress-gce がClusterIPへのヒモ付に対応してないのでServiceTypeはNodePortを使用しています。NodePortの番号は特に指定していないのでKubernetes側にrandomに割り振ってもらっています。

  2. 次の手順でGoogle Cloud DNS Recordsetsを作りますが、もしそちらをterraformで管理して消し忘れとかをなくしたいのであれば、Google compute address(静的IPアドレス)の払い出しもTerraformで行って、Ingress側の metadata.annotations.kubernetes.io/ingress.global-static-ip-name で指定して使うのが管理の都合上いいかと思います。