LambdaのContainer Image Supportでサブコマンド経由で呼ぶ方法

2020/12、Lambdaに待望のコンテナイメージサポートが追加されました。これにより、すでにECSなどでコンテナベースのデプロイをしている場合は既存のイメービルドの仕組みを流用できるようになりました。

同時にローカル環境でLambda関数をAPI経由で実行可能にするThe Lambda Runtime Interface Emulator (“RIE”)も公開されました。

実はDockerfileのCMDでサブコマンドを指定してLambda関数を呼び出す場合、RIEとAWS実環境で挙動が異なることがわかりました。

今回は両方で動く方法を実装して https://github.com/nabeken/go-lambda-subcommands-example に公開しています。

どういうこと

イメージサイズを小さく保つため、Goで複数のコマンド(e.g. APIやworker)を書く時はシングルバイナリにして、実際のコマンドはサブコマンドとして実装することが多いです。

例:

  • prj-main api
  • prj-main worker-foobar

prj-main がビルドされたバイナリで、実際のコマンドは引数で指定。

これをLambdaのContainer Image Supportで行なう場合は DockerfileCMD ["prj-main", "api"] のように指定しますが、RIEではこれが動きません。

推測ですが、RIEは従来のLambda関数のパッケージングもサポートする関係上、 _HANDLER 環境引数に指定されたバイナリを実行しようとします。この値はファイル名であることを期待しているため、引数が渡せません。

上のサンプルプロジェクトに含まれる entrypoint.sh_lambda_main はオリジナルの引数をファイルへ退避しながらruntimeを実行するentrypoint、Lambdaのruntimeがhandlerを実行する時にそれを復元するshimで構成されています。

すでにECSで運用しているコンテナイメージがあるなら、上記のような動きをするファイルを仕込み、Lambdaの設定でentrypointを変更すればECSとLambdaで共用できるイメージが出来上がります。どうぞお試しあれ。

MarkdownをSlackのBlock Kit UIへ変換する

Github上でMarkdownの文章を管理していると、その内容をSlackへポストしたくなるときがあります。

単純にコピペしてしまうとリンクなどが維持されないため、何らかの方法でMarkdownをSlackのメッセージ形式へ変換する必要があります。

世の中のソリューション

もちろん、この問題は世界共通の悩みなのですでにいろいろなソリューションがあります。

  • Slack で真の Markdown を使う

    今のところ、この実装が一番イケてる気がしますが、Node環境が必要なのでちょっと面倒……

    Block Kit UIになっているのもイケてますね。

  • html-to-mrkdwn

    Githubも実のところ、Slack通知を送る際にこの問題を解決する必要があり、これはその実装のようです。 Markdown -> HTML -> Mrkdwn にしているようです。

    こちらはBlock Kit UIでなく、mrkdwnテキストにゴリゴリ変換しているようです。

わたしのソリューション

Goのソリューションが欲しかったので、作りました。

https://github.com/nabeken/blackfriday-slack-block-kit

GoのMarkdownパーサであるblackfriday v2がAST (Abstract Syntax Tree)を吐くので、それを元にSlack Block Kit UIのJSONへ変換するライブラリと簡単なツールを書きました。

詳しいことはリポジトリ内のREADMEをどうぞ。

このコードは https://github.com/karriereat/blackfriday-slack をBlock Kitに対応させたものになります。ありがとうございました。

Github Actionsで使用するAWSのクレデンシャルをGithub Appsで安全にキーローテーションする

プライベートのAWSアカウントをGithub Actions上のTerraformで管理するようにしました。 その過程でIAMユーザーのクレデンシャルをGithubのSecretsに保存しつつ、安全にキーローテションする方法をGithub Actionsで実装しました。

Github ActionsでAWSのキーローテーションをする場合はRotate AWS Access Keysが有名です。

READMEでも触れられているように、Github Actionsで使える GITHUB_TOKEN にはSecretsを更新する権限が付与されていません。Personal Access Tokenを発行してセットする方法が推奨されていますが、このトークンはアカウントがアクセス可能なすべてのリポジトリに権限を付与してしまうため安全ではありません。

今回はPersonal Access Tokenの替わりに専用のGithub Appsを作成し、特定のリポジトリだけにインストールすることで 安全にSecretsを更新できるようにしました。

Github Appsを作成する

まずはCreating a GitHub Appを参考に作成します。

  • 与えるパーミッションは “Secrets” - “Read & Write” のみでOK
  • このAppは自分専用なので “Only on this account” でOK

作成できたら、Appの設定画面から秘密鍵を生成し、App IDを控えます。

作成したAppはInstalling GitHub Appsを参考に、このAppで管理したいリポジトリへインストールします。

インストールできたら、リポジトリの “Settings” - “Integration” からインストール済みAppの設定画面へ移動します。この時のURL https://github.com/settings/installations/XXXXXXXXXX の部分が Installation ID になるので、これを控えます。

ここで得た値をリポジトリのSecretsへ設定します。

  • APP_ID: Appの App ID を設定
  • APP_INST_ID: Appの Installation ID を設定
  • APP_GITHUB_PRIV_KEY: Appの秘密鍵を設定
  • AWS_ACCESS_KEY_ID: 対応するAWSの初期クレデンシャル
  • AWS_SECRET_ACCESS_KEY: 同上

Github Actionsを作成する

あとはいつも通り .github/workflows/aws_key_rotate.yml としてActionsをインストールします。

例:

Github Appsの GITHUB_TOKEN を取得する処理は拙作のnabeken/go-github-appsのGithub Actionsで処理しています。

これで、毎週月曜日の日本時間9時に自動的にキーが更新されるようになりました。 動作確認時または即時更新したい場合は actions/aws_key_rotate ブランチにpushしてください。

2021/02/21更新:

Gistの例で kneemaa/github-action-rotate-aws-secrets@v1.0.3 に渡すAWSのクレデンシャルをIAM userからローテーション用のIAM roleに変更しました。

万が一アクションに悪意のあるコードが混入された場合、IAM userが持つ権限あるいはAssume Role先の権限への昇格を許してしまうための対策です。

ローテーション用のIAM roleの例(Terraform):

resource "aws_iam_role" "key-rotate" {
  name = "example-key-rotate-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Principal": { "AWS": "${var.deployment_user_arn}" },
          "Action": [
            "sts:AssumeRole",
            "sts:TagSession"
          ]
      }
  ]
}
EOF
}

resource "aws_iam_role_policy" "iam" {
  name = "key-rotation"
  role = aws_iam_role.key-rotate.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "iam:CreateAccessKey",
          "iam:DeleteAccessKey",
          "iam:GetUser",
          "iam:ListAccessKeys",
        ]
        Effect   = "Allow"
        Resource = var.deployment_user_arn
      },
    ]
  })
}

自前メールホスティングの行く先 (2)

前回の続きです。

まずはOffice 365へ移行できるかを検討

徐に “office 365 imap 移行” と雑に検索。

さすがにちゃんと手順はありますね…顧客を引っぱるには必須ですからね…。 IMAPからの移行というよりかは、IMAPをソースにした定期的な一方向同期のようです。

  • 旧ホストにO365への転送設定を入れる
  • IMAP同期する
  • 最後にMXを向ける (いきなり向けても転送設定はどのみち新しいMXに切り替わるまでの間必要)

で自分以外のメールボックスはさくっと移行できそうです。 問題は…自分のメールボックスですね。30GBある。

とりあえず、雑に何通あるか数えてみました。なんでかというと、O365が提供している同期機能にはメールの数に制限があるためです。

ユーザーのメールボックスから移行できる項目数は最大 500,000 です (メールは新しいものから順に移行されます)。

で、

find Maildir/ -type f | wc -l
1906345

あ…うん…190万通ね…そんな気はしてたよ…。15年分あるからね。SpamAssassinがスパムと判定した数を数えてみよう。

find .Junk -type f | wc -l
195059

1割か…消しても焼け石に水ですね(もちろん移行前に消すけど)。 O365の用意する方法では難しそうなので自分でなんとかする必要がありそう。

たしかimapsyncというOSSがあったはず。 (昔ローカルのdovecotとsyncしてオフラインでメール読む環境作った時に使った)

で、これはOSSというかライセンスがちょっと特殊ですね…

そして、すごく詳しいFAQがあった。さすがに商売としてやっているだけある。

EC2のAmazon Linux向けの手順が一番簡単そう。

当面の方針はEC2のスポットインスタンスを使ってimapsyncを動作させて自前メールサーバからOffice 365(Exchang Online)へ移行かな……

今後

  • MXのTTLを短くしとく (2分くらい?)
  • Azure AD作って無料トライアル中にimapsyncを検証する

約90万通あるINBOXが移行できればたぶん問題ないので本移行の作業に入る。

  • 自前メールシステムからO365へ全転送
  • 自分以外のメールボックスをIMAP同期
  • クライアントをO365へ向ける
  • 自分のメールボックスをimapsyncで移行
  • MXを切り替える
  • 自前メールシステムをバックアップ
  • 自前メールシステムを破棄

お楽しみに!(めんどくさいけど)

自前メールホスティングの行く先 (1)

最近まったく記事を書いていませんが、いろいろ考えてはいます。 年単位で考えているのがプライベートで運用しているメールサービスの今後です。 ブログのリハビリも兼ねて書き出してみました。

現在の構成はさくらのVPS上のMXが1台で、そのなかでPostfix + Dovecot (IMAP) + LDAPでバーチャルドメインを使用し複数ドメインを収容しています。 もともと家族用に提供していましたが、利用者は私を入れて2名に減少しています。 自宅サーバ時代から数えてすでに15年以上提供していますが、当時IMAPを使うには自前でホスティングするしか選択肢がなかったはずです。

WikipediaによるとGmailは2004年ベータ版開始(招待制)、そしてIMAP対応と独自ドメインが使えるサービス(Google Apps; 現在のG Suite)が2007年でした。

1年ほど前まではMXが2台あり、LDAPもレプリケーションしていましたが、 バックアップ側のOSが老朽化してしまったのを機に潔く1台構成にしました。 しかし、現在運用中のプライマリもDebianのWheezyなので来年の5月までとなっており、対応が必要となってきました。

仕事柄、まっさきに考えるのは自前を諦めてどこかのサービスへ移管する案

  • G Suite (30GB 6000円/年/ユーザー)
    • ユーザーによっては30GB付近にいる(というか私)。追加料金をみると20GB追加で350円。
    • 仕事でも使っているので慣れている。
    • プライベートでのアカウントではすてに1TBのドライブ容量を買っているのでちょっと納得がいかない…
  • Office 365 (50GB 6480円/年/ユーザー)
    • G Suiteより月額で40円高いが、メールボックスの容量は足りているので追加の必要はなし。
    • 金額面でもG Suiteより安い。
    • しかし、私はGmailのほうが慣れているので、たぶん全転送する
    • 仕事ではあんまり使えていないでAzureADとかの勉強にいいかも (←前向き)
  • さくらのメールボックス (10GB 1029円/年)
    • 安いが容量が圧倒的に足りないし増やせない…
  • Amazon WorkMail (50GB $60/年/ユーザー)
    • 誰か使っている人いるのだろうか…?ユーザ管理にディレクトリが必要そうなので実際には数人レベルで使うものじゃなさそう。

4つ上げたところで、もしここから選ぶならOffice 365になりそうですね。

このまま自前でいく案

金銭面ではMXが1台なのは困るのでもう1台増やしたい。そうすると、月1000円* 2 で年間24000円、上でいう4ユーザー分に相当しますね。 容量は100GBあるので実際には8ユーザー程度くらい?ユーザー数的には10名越えるくらいなら細々と自前でもいいのかもしれません。

技術的な面では、まずOSに依存してしまうとディストリビューションのライフサイクルに強制的に引っ張られてしまうので PostfixとDovecotがいくら枯れていても作業が発生してしまいます。また、その作業自体もさくらのVPSだと安全にというかゆっくり作業ができません (スナップショットを用意するとか、インスタンスを別に用意して最後にIPアドレスを付け替えるなど)。 プライベートだと片手間に検証しつつ………になるので別インスタンスでこつこつ作業して最後に切り替えたい。 ここはコンテナで解決できます。

もう1つ厄介なのが、複数MX構成時のバーチャルアカウントの同期です。LDAPのレプリケーションはもう御免です。2人しかいないのにオーバーキルです。 今はLDAP上のパスワードを変更する昔書いたRailsアプリを必要に応じて使用。

実のところこの問題が解決できずコンテナ化が進んでいません。要件はパスワードの変更が自分でできて、アカウントの同期が簡単にできることです。

案はいくつかあります。

1つ目はコンテナの内部で動かすのであればシステムアカウント (/etc/passwd) でも問題ないので passwdとshadowを生成するWeb I/Fとそれをバラまく仕組みを自分で用意する。Goで書いてAmazon API Gateway -> Lambda -> S3 SSE-KMSに書いて、コンテナ起動時に取得する、など。 ちょっとダルい。

2つ目はサーバレスSTNS。API Gateway + DynamoDBで構成するSTNSをpam経由で使う。過去に検証したところdovecotで使うとstnsがcgoエラーで死ぬので断念しました。 そのまま使うのは厳しいと判断。ただ、Go + cgoでpamモジュールを書くのはよいと感じました。 もし自分がやるならpamモジュール内部ではローカル上のhttpサーバにunix socket経由で問合せるに留めて、以後バックエンドはユーザースペース側に別途用意する設計にするかな…。 ただ、この場合でもWeb I/Fは必要。

3つ目はAmazon Cognit User PoolsまたはAmazon Cloud DirectoryをバックエンドにしたpamモジュールとそれらをいじくるWeb I/Fを書く。 技術的には面白いし本命感ありますが、ユーザー2名なのにそこまで感がハンパない。

4つ目は……自前LDAPの代わりにAzureADを使う。だったら黙ってOffice 365使うよね…

結局のところ、どの案も手を動かす必要がありますね。

それで

私は自前メールサーバの辞め時を探しているのかもしれません。 今のところ、Office 365が最有力候補ですね。自分でも驚いていますが…。 進捗があったらまた続きを。