ecs-login-helperの実行時に環境変数を追加するやつ

複数のAWSアカウントのECRからdocker pullするときに、いちいちプロファイルを指定するのがめんどくさいことがあったので(docker-composeとか)、環境変数を追加するecs-loginのラッパーを作った。

github.com

~/.docker/ecr-login-env.json に以下のような感じで設定を書いておいて

{
  "123456789012.dkr.ecr.us-east-1.amazonaws.com": {
    "AWS_PROFILE": "my-profile"
  }
}

~/.docker/config.jsonecr-loginecr-login-with-env に差し替えると

{
  "credHelpers": {
    "123456789012.dkr.ecr.us-east-1.amazonaws.com": "ecr-login-env"
  }
}

docker pull/pushなどをするときにecr-loginに環境変数 AWS_PROFILE=my-profile が渡されるようになる。

GolangのRedashクライアントライブラリを書いた

GolangのRedashクライアントライブラリはいくつかあるものの、メンテナンスが活発でなかったり、一部のリソースしか操作できなかったりするため、新しくライブラリを書いた。

github.com

※その他のライブラリ

RedashのAPIは一応、ドキュメントがあるもの網羅的ではなく、リファレンスとしてはほとんど使えないので、Redashのソースコードredash-toolbeltを参考にした。

SPAなのですべての操作はAPIで行われているが、外部公開を考慮していないものもいくつかあるようで、正しくないパラメータを送るとすぐにInternal Server Errorになってしまったり、同じAPIが構造の異なるスキーマを返したりと癖があり、少し時間がたつとすぐに腐ってしまいそうなので、このライブラリではそれなりに頑張ってテストを書いている。ユニットテストだけでなく、DockerでRedashを立ち上げてのインテグレーションテストも一通り書いた。

使い方

以下はクエリを作成して、実行結果を表示する例。

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/winebarrel/redash-go"
)

func main() {
    client, _ := redash.NewClient("https://redash.example.com", "<secret>")
    //client.Debug = true

    ctx := context.Background()

    query, _ := client.CreateQuery(ctx, &redash.CreateQueryInput{
        DataSourceID: 1,
        Name:         "my-query1",
        Query:        "select 1",
    })

    var buf bytes.Buffer
    job, _ := client.ExecQueryJSON(ctx, query.ID, &buf)

    if job != nil {
        for {
            job, _ := client.GetJob(ctx, job.Job.ID)

            if job.Job.Status >= 3 {
                buf = bytes.Buffer{}
                client.GetQueryResultsJSON(ctx, query.ID, &buf)
                break
            }

            time.Sleep(1 * time.Second)
        }
    }

    fmt.Println(buf.String())
}

REST APIをマップしているので ListAlerts() でアラートの一覧を表示したり、CreateDashboard()ダッシュボードを作ったり…とリファレンスを見れば大体の操作はわかると思う。

クエリの実行、Jobまわりは癖があるのでAPIそのままではないが、普通に使える気はする。 Job一覧を表示するAPIは未だ見つけられず。

デバッグとppcat

APIの調査のため、ローカルでRedashを立ち上げてひたすらたたいたいた。 redash-goにデバッグフラグを追加して、HTTPのリクエストとレスポンスをダンプし、流れてくるJSONを観察。

https://github.com/winebarrel/redash-go/blob/main/client.go#L115-L118

    if client.Debug {
        b, _ := httputil.DumpRequest(req, true)
        fmt.Fprintf(os.Stderr, "---request begin---\n%s\n---request end---\n", b)
    }

そのJSONJSON-to-Go: Convert JSON to Go instantlyGolangの構造体に変換する、ということを繰り返していたが、レスポンスの一行JSONが見にくく、それをいちいちコピペで整形する手間がめんどくさかったのでppcatというツールも作った。

github.com

これはテキストにJSONっぽい行が含まれていたらパースしてみてJSONだったら整形して表示するというもの。 たとえば上の出力をppcatに通すと以下のようになる。

大分見やすくなったので開発も捗った。

それはさておき

discuss.redash.io

PRがマージされたら他のPRのブランチを更新するGitHub action

GitHubのPRがマージされたら、リポジトリの他のPRのブランチを更新するコマンドとそのGitHub actionを作った。

github.com

github.com

実行例

name: pru
on:
  push:
    branches:
      - main
    paths:
      - "**/*.go"
      - Makefile
permissions:
  contents: write
  pull-requests: write
jobs:
  pru:
    name: pru
    runs-on: ubuntu-latest
    steps:
      - uses: winebarrel/pru-action@v0.3.0
        with:
          paths: |
            **/*.go
            Makefile

この例だと、PR#1をマージしたときに、**/*.go,Makefileを含むPR#2のブランチを更新している。

仕組み

QuetaroというSQS+PostgreSQL+Lambdaのジョブキューシステムを作った

github.com

これは何?

SQS+PostgreSQL+Lambdaとその間をつなぐデーモンで構成されたジョブキューシステムです。 入り口のSQLにメッセージを投げると、メッセージで指定した関数名のLambdaが実行されます。 Lambdaの実行に失敗した場合は、適当な感覚でリトライされます。

Ruby製のジョブキューシステムQueとそのクローンであるqgを模した作りになっています。

モチベーションとか

コンテナ環境で使いやすいジョブキューシステムが欲しかったのが、モチベーションです。

Resqueのようにプロセスとしてジョブを起動するタイプだと、コンテナのライフサイクルとのミスマッチでスケールイン・スケールアウトが難しので、コンテナを使うならなるべくクラウド上の何か(ECS・Lambda…etc)でジョブを実行したいのですが、一方で完全にAWSリソースだけでジョブキューシステムを作ると、特にSQSの使い勝手が悪くキューの可観測性が低いので、もう少し使い勝手の良い何かが欲しいなーと。

アーキテクチャ

特徴

  • スケーラビリティとメンテナンシビリティを高くすることを目標に設計しています
  • 各デーモンはプロセスレベル・Goroutineレベルでスケールアウトできます
  • ジョブの実行は遅れますが各デーモンは停止可能です
    • たとえば「デーモンを一時的に停止してDBのアップグレードを行う」といったことが可能です
  • SQSのメッセージIDをDBのPK・構造化ログ・Lambdaのeventに出力しているので、メッセージIDでジョブの動作をトレースできます
  • ジョブキューとしてPostgreSQLを使っているので、SQSに比べて検索性が良いです

Getting Started

READMEに書いたとおりなんですが

1. docker-composeの起動

$ docker-compose up

2. terraformの適用

$ make tf-init
$ make tf-apply

※LocakStackに対してterraformを適用します

3. DBのセットアップ

$ make db

4. デーモンの再起動

初期状態ではデーモンが起動に失敗しているので、再起動します。

$ make restart


以上でサーバのセットアップは完了です。

最後にSQSにメッセージを投げるとLambdaが実行されることを確認できます。

$ make message

サンプルのLambdaはこんな感じです:


make failure を実行すると、Lambdaが失敗するメッセージをSQSに投げるので、ジョブがリトライする様子を確認できます。

$ make failure

DBに残っているジョブは http://localhost:8081 から確認できます。

QA

なんでLambdaなの?(Step Functions / ECSではないの?)

  • Lambda Destinationsが便利だったので…
  • SFn / ECSを実行しようとすると、AWSのリソースが増えてやや複雑になる/Destinationsのような処理を自前でやることになりそう
    • LambdaからDestinationsをなくした上でSFn / ECSをたたくようにして、SFn / ECSからsuccess/failureのキューにメッセージを投げるようにすることもできるかと
  • Lambdaだと15分の時間制限があるが、それを超えるようなジョブはバッチ処理に移譲してほしい気持ち
  • Dockerイメージが使えるのでECSとの併用はそんなに難しくはない…はず

クライアントから直接Lambdaを非同期で実行すればよいのでは?

  • Lambdaを非同期に実行するときに使われる内部的なキューが可視化されていないようなので、キューの状態を確認するのが難しそう
  • Lambdaの並列数上限に達してクライアントからのAPIコールが失敗するのが嫌

SQSからLambdaを実行すればよいのでは?

  • SQSの検索性が非常に悪いので、メッセージをSQSにためておきたくない
  • キューのメッセージの確認のために、ReceiveMessageでメッセージを受信して確認し、VisibilityTimeoutで再送を待つような破壊的な動作をやりたくない

DynamoDBを使えばよいのでは?

  • SKIP LOCKEDを使ったPostgreSQLのキューのように、排他制御を実装できるかわからなかった
  • PostgreSQLのかっちりとした型制約がほしかった

どこかのプロダクションで使ってるの?

  • まだです。そのうち突っ込みたい…

親に向かってなんだそのContextは。

親Contextの都合でcancelしてほしくないが、loggerの引き回しはContextでやりたい、みたいなときにDeadline()/Done()/Err()を無視すればいいのかなぁ…と思って実装してみた。

cancelされたくないというのは、SIGINTですべてのgoroutineを停止したいんだけど、goroutineのトランザクションは完了しててほしい、みたいな。

package ctxutil

import (
    "context"
    "time"
)

type withoutCancelCtx struct {
    parent context.Context
}

func (*withoutCancelCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*withoutCancelCtx) Done() <-chan struct{} {
    return nil
}

func (*withoutCancelCtx) Err() error {
    return nil
}

func (ctx *withoutCancelCtx) Value(key any) any {
    return ctx.parent.Value(key)
}

func WithoutCancel(parent context.Context) context.Context {
    return &withoutCancelCtx{parent}
}
   ctx = ctxutil.WithoutCancel(ctx)

ctxutil.WithoutCancel()を読んでいる関数の上位の関数では、親ContextのDone()をチェックして、子が終わったら終了する想定。

キャンセルされたくないトランザクションはいろいろあるきがしているけど、その対応の実装例をあまり見ないような気がしている。他の人、どうしているんだろう?