TerraformとSnowflakeで考えること

はじめに

SnowflakeクラウドをベースとしたSaaS型のデータプラットフォームです。主要なクラウドAWS/GCP/Azure)に対応しており、企業/組織内の至る所に転がっているデータも「クラウド×Snowflake」で連携できるため、アジリティやスケーラビリティが求められるデータプラットフォームとして大きな強みがあります。

そんな注目を浴びているSnowflakeですが、これまたIaCとして人気のあるTerraformで構築できます。Snowflakeは大量のオブジェクトを組み合わせて管理するため、長くお世話になるならTerraformの利用をぜひとも考えたいところです。

本稿では、Terraform×Snowflakeを検討していく上で自分が感じた検討ポイントや悩みどころを脳内整理を兼ねて記載します。本内容がこれから検討する方の参考になれば幸いです。

SnowflakeとTerraformのざっくりHistory

こちらにまとまっています。

  • 2019年からChan Zuckerberg Initiative (CZI) によりSnowflake向けのTerraformプロバイダが整備開始
  • 2022年5月25日に所有権をChan Zuckerberg Initiative (CZI) からSnowflake-Labsに譲渡
  • 現在はSnowflake-Labsで管理されている

公式サポートも明言されたため、最近ではTerrfaorm×Snowflakeの記事も多く見かけるようになりました。

規模や目的にもよるとは思いますが、これからSnowflakeを始める方はTerraformの利用も視野に入れておくことをおすすめします!

最初の一歩:チュートリアル

まず何から手をつけたほうがよいのだろう...となると思います。幸いチュートリアルが用意されているので、下記サイトを一通り試してみるのをおすすめします。Terraform×Snowflakeの基礎を学ぶことができます。

Terraforming Snowflake

また、下記の内容を一読することもおすすめします。リアルな設計構築ポイントについてとてもわかりやすくまとめられています。このレベルの内容を無料で読ませて頂けるのは感謝感激です。

SnowflakeとTerraformで作るデータ基盤入門

Terraform×Snowflakeの検討ポイント

さっそく本題です。いざ現場で「Terraformをガンガン利用していくぞー!」となると、ちょろっと動作確認するだけでは見えてこなかった様々な検討課題が出てきます。本稿では自分が感じたこと、考えたことをまとめてみます。 注意点ですが、あくまで持論なので、正解か不正解かを述べているわけではありません、「こんなこと言っているやつがいるな」程度にみてもらえればと思います。ユースケースは山ほどありますし、私自身も考えをアップデートしていきます。むしろ、こうしたほうがよいんじゃない?などのコメント大歓迎です!是非是非ご意見頂けると幸いです。

① 役割分担

Terraformを管理するのは誰か?です。いきなりケースバイケース、が正解となる問です。

本稿ではTerraformを担当するチームと、データモデルや分析を検討するチームを分けて考えます。

開発者全員がTerraformを扱えるなら困らないと思いますが、世の中そんなにうまくはいきません。人材確保が困難な時代でSnowflakeもTerraformもできる人をアサインして、チームとして開発を継続していくのはとても難しいチャレンジになると思います。

昨今ではクラウドを扱うインフラエンジニアがTerraformを得意としているので、インフラチームがプロジェクトに存在するなら、餅は餅屋としてお任せした方が進めやすいケースも多いはずです。SQLに慣れているデータ分析のエンジニアにTerraformを一から学んでもらうよりも、得意な領域に注力してもらう方が価値を発揮できると思います。

Snowflakeの活用が広まっていくと同時に、スキルセットの課題はより顕著になってくると思います。また、規模が大きくなればなるほどチームで分担しながら開発を進めていくことになると思います。そのため、ここではTerraformを検討する「インフラチーム」と、データモデルやSQL検討する「データ分析チーム」として話を進めていきます。

② ロール階層

初めにロール階層を検討しましょう。Snowflakeは6つのデフォルトロールが用意されていますが、実際の開発ではカスタムロールの作成が必須です。カスタムロールに適切な権限を付与することで、柔軟なアクセス制御を実現できます。

このアクセス制御方針がTerrfaformの役割分担に関係してきます。つまり、何をどこまで「インフラチーム」にお任せして、どこから「データ分析チーム」で開発するのか、です。

ここでは「インフラチーム」がTerraformでデータベース・スキーマを作成し、「データ分析チーム」がそのデータベース・スキーマ上で自由にテーブルやビューを操作できるように、下記のようなロール階層とします。

ロール階層について詳細を述べませんが、正直難しいテーマだと思います。SYSADMIN配下にカスタムロールを紐づける方針まではよいですが、後の設計は自由です。先輩方がたくさんの知見を残してくれているので、参考にしながら検討を進めてくのがよいと思います。

公式ドキュメント:アクセス制御の概要

2021/03/01 Snowflake を始めるときのおすすめユーザ権限管理構成

2022/05/16 Snowflakeのアクセス制御

2023/01/31 Terraform による Snowflake ロール作成 ~ Functional role + Access role モデル ~

SnowDDLのドキュメント:Role hierarchy

メモ:運用を考慮すると、オンライン・バッチ処理で動かしているウェアハウスと、アドホックにクエリを実行するウェアハウスが重複しないようにウェアハウスのUSAGEを設計することもポイントになるでしょう。

③ Terraform管理対象オブジェクト

具体的に何をTerraformで構築するのか?です。

2023/2時点でSnowflake Providerは63のResourcesを提供しており、今後のアップデートで更に増えていくことが予想されます。つまり、いろいろできるけど結局何を構築したらよいのだろう...と悩むことになります。この検討には、先ほどのロール階層に加えて、要件と体制を合わせて考えます。

要件次第でSnowpipeからPIPEオブジェクトが登場し、データ共有ならSHAREオブジェクトを検討するようになります。ここは都度変更されるため、検討の枠組みを整備したり、改善のプロセスを整備できれば、自ずと管理対象が適正化されていくと思います。

体制はスキルセットや運用が関連します、例えばTerraformを知らない運用チームがメンテ作業をできるようにするなら、必ずしもTerraformの利用が正義とはならない、などです。

従ってこれも「プロジェクト次第だねー」が正解となります。もう何度聞いたかわからないセリフですね。

では具体的な検討内容をみていきます。

Terraform実行ユーザーに付与するロールは?

SnowflakeオブジェクトをTerraformで扱うには、Snowflake上にTerraform実行ユーザーを作成します。そして、Terraform実行ユーザーにSnowflakeオブジェクトの操作権限を付与します。

CREATE USER "TF_DEV" RSA_PUBLIC_KEY='RSA_PUBLIC_KEY_HERE' DEFAULT_ROLE=PUBLIC MUST_CHANGE_PASSWORD=FALSE;

GRANT ROLE SYSADMIN TO USER "TF_DEV";
GRANT ROLE SECURITYADMIN TO USER "TF_DEV";

つまり、Terraform実行ユーザーに付与した権限が、Terraformで扱えるSnowflakeオブジェクトになります。

個人的には、デフォルトロールのSYSADMIN・SECURITYADMIN権限を付与して、ACCOUNTADMIN権限を付与しない方針で問題ないと思います。理由はシンプルで、SYSADMIN・SECURITYADMINはデータベース作成や権限付与のためさすがに必要であること、ACCOUNTADMIN権限の付与は公式でも述べられているように最小限にしたほうがよいからです。実際、ACCOUNTADMINが必要となる操作はそこまで多くないので、個人的にはそこまで困らないと思っています。

もしACCOUNTADMIN権限が必要となるオブジェクトをTerraformで扱いたい場合は、Terraform実行ユーザーにACCOUNTADMINを付与する前に、まずはロールへの権限委譲を考えてみるのがよいと思います。例えば、リソースモニター共有オブジェクトストレージ統合あたりが判断に迷うポイントになると思います。どちらにしても、運用しながら改善していく方針でも十分だと思います。

Terraform対象とするオブジェクト

次に何のオブジェクトを対象とするか?です。

Terraform実行ユーザーにSYSADMIN・SECURITYADMIN権限を付与する前提として、下記Snowflakeオブジェクトを例にTerraform利用方針を記載してみます。備考は個人的なメモとして残しておきます。なお、クラウドAWSを利用する前提でIAMと記載しています。

オブジェクト TF対象 備考
DBSCHEMA タイムトラベルの保持期間 data_retention_time_in_days をチーム間で合わせて環境差異として設定する。
enable_multiple_grants期待した動きにならない報告もあるので権限付与をTerraformに統一して利用しないことにする。
GRANT ROLE TO DB 「データ分析チーム」の管理者にOWNERSHIPを付与する。
ロール階層でアクタを整理しながらUSAGEを付与する。
GRANT ROLE TO SCHEMA データベースと同様にOWNERSHIPとUSAGEを付与する、加えて要件に応じて必要な権限を付与する。
デフォルトで作成されるPUBLICスキーマの利用方針も検討しておく。
TABLE - 複雑になるためTerraformでは管理しない。
「データ分析チーム」が、テーブルを含むスキーマ配下のオブジェクトを自由に作成/管理できるようにする。
GRANT ROLE TO TABLE on_futuretrue となる場合はTerraformで管理する。
テーブル設計方針をチーム間で合意しておく。
全てのテーブルを対象に権限を付与することができないことに注意して実装する。
ROLE カスタムロールを管理する。
GRANT ROLE TO ROLE Terraformの実装にとりかかる前に、ロール階層の設計をチーム間で合意しておくこと。
WAREHOUSE パラメータに注意する。こちらが参考になる。
GRANT ROLE TO WAREHOUSE ロール階層と合わせて確認する、アドホックなクエリで利用するウェアハウスが業務処理のウェアハウスに影響を及ぼさないか、など。
STAGE ストレージ統合をSQLで作成するため、手続き的な構築となることに注意する。
GRANT ROLE TO STAGE ステージを参照するロール階層と照らし合わせて意識合わせしておく。
STORAGE INTEGRATION - ACCOUNTADMIN権限を必要とするのでTerraformで管理しない。SQLで作成してData Sourcesとして読み込む。
GRANT ROLE TO STORAGE INTEGRATION ストレージ統合の参照にもACCOUNTADMIN権限を必要とするので、Terraformで扱えるようにUSAGE権限を付与する。
AWS IAM ROLEIAM POLICY STAGE用に作成する。外部ID設定のため、IAMロール作成→ストレージ統合作成→ストレージ統合のように手続き的な対応となることに注意する。
USER ケースバイケースだが、個人的にはTerraform対象外にしてもよい考えている。
理由はSSOでユーザー管理を外出ししたり、運用ではTerraformなしで作業できることが望ましいケースもあるため。
GRANT ROLE TO USER 同上。

再掲ですが、Terraform対象とするSnowflakeオブジェクトは要件やプロジェクトの方針次第で変わります。ただ、どのようなケースであろうと方針を表や箇条書きで整理してみるのがよいと思います。

命名規則と環境差異

チーム間の意識合わせが超重要です。そりゃそうだろうという話ですが、ここを曖昧にすると後で傷口が広がるおそれがあります。

Terraformを利用するなら、モジュール化して各環境面の設定を効率化したり、可能な限り条件分岐を減らした記述を心掛けたいところです。もし命名規則や環境差異をチーム間で意識合わせしていないと、命名から用途を読み取りにくくなったり、コード内で無理やり分岐したりと、徐々に品質への影響が懸念されます。

中長期的に品質を確保していくためにも、チーム間でオブジェクトの命名規則と環境差異の方針を合わせておきましょう。

命名規則

よくある命名規則の方針と変わりませんが、Snowflakeクラウドと連携することが多いため、Snowflakeのアカウントとクラウドの環境面をラベルに追加しておくのもありだと思います。

# sample
[組織・プロジェクト名]_[Snowflakeアカウント名]_[Snowflake or クラウド環境面]_[機能識別子]_<[任意]>_[オブジェクト名]_[ナンバリング]

環境差異

まずはSnowflakeクラウドのアカウントと環境面について、どう連携するか意識合わせしましょう。 チーム間の意識合わせの際には、こんな問いをしてみるのがよいと思います。

  • クラウドアカウントとSnowflakeアカウントは1対1か?
  • クラウドアカウント内の環境面とSnowflakeアカウントは1対1か?それともN対1か?
  • Snowflakeに構築するオブジェクトは、クラウドのどのアカウント・環境面と連携するのか?
  • 上記オブジェクトはクラウドの各環境面に対して共通設定か?それとも設定差異はあるか?設定差異がある場合どこか?

環境差異の例を挙げてみます。

  • このデータベースとスキーマは開発面だけあればよいのだよね
  • この環境面のデータベースだけはこのスキーマを追加してほしい
  • この環境面だけステージが必要ほしいのよ、あと特定のスキーマだけを対象としたい

モジュール化の影響も含めてチーム間で目線を合わせられるとよいでしょう。なお、1回きりの意識合わせではないので、持続的なヒアリングを通じてチーム間のコミュニケーションの成熟度を上げていくことが品質担保につながるでしょう。

Terraform未経験の方が自律的にここまで考慮するのは難しいと思いますので、早い段階から本検討の必要性/重要性を伝えるような動きを「インフラチーム」ができるとよいと思います。

⑤ Terraformモジュールとディレクトリ構造

いきなりモジュールやディレクトリ構造を考えるのではなく、アカウントと環境面の構造を整理しておくのがよいと思います。ここではアカウント内に複数の環境面が存在するようなモデルを考えてみます。

この構成のモジュール分割について考えてみます。

  • Snowflakeアカウントレベルのモジュール
    • ロール、ウェアハウスと関連するGRANTを管理
  • ② ステージ準備用モジュール
    • ステージ用のIAMリソースを管理
  • ③ 開発用途のモジュール
    • 開発用途のデータベース/スキーマ/ステージと関連するGRANTを管理
    • ロール、ウェアハウス、IAMリソース、STORAGE INTEGRATIONをData Sourcesで参照
    • ①②の後に作成
  • ④ 商用用途のモジュール
    • 商用用途のデータベース/スキーマ/ステージと関連するGRANTを管理
    • 他は③と同様

この構成のメリデリは以下となります。

- 内容
メリット ・条件分岐を削減できる
・tfstateの肥大化を抑えられる
・商用への更新影響範囲を削減できる
デメリット ・モジュール追加方針を決めにくい
・一部手続き的な段取りが必要
・管理するモジュールや設定ファイルが多くなる

続いてディレクトリ構造の例です。上記方針さえ決まってしまえばあとは型にはめるような感じになると思います。

.
├── cloud(AWS_GCP_Azure...)
│   ├── envs
│   │   └── (略)
│   └── modules
│       └── (略)
└── snowflake
    ├── envs
    │   ├── account_dev
    │   │   └── snowflake_common
    │   │       └── main.tf
    │   ├── account_prod
    │   │   └── (略)
    │   ├── account_stg
    │   │   └── (略)
    │   ├── env_dev01
    │   │   └── snowflake_dev
    │   │       └── main.tf
    │   ├── env_dev02
    │   │   └── (略)
    │   ├── env_dev03
    │   │   └── (略)
    │   ├── env_dev04
    │   │   └── (略)
    │   ├── env_prod01
    │   │   └── (略)
    │   └── env_stg01
    │       └── (略)
    └── modules
        ├── snowflake_common
        │   ├── grant_role_to_role.tf
        │   ├── grant_role_to_warehouse.tf
        │   ├── locals.tf
        │   ├── main.tf
        │   ├── roles.tf
        │   ├── variables.tf
        │   └── warehouses.tf
        ├── snowflake_dev
        │   ├── data.tf
        │   ├── db_schema.tf
        │   ├── grant_role_to_db.tf
        │   ├── grant_role_to_schema.tf
        │   ├── grant_role_to_stage.tf
        │   ├── grant_role_to_table.tf
        │   ├── locals.tf
        │   ├── main.tf
        │   ├── stages.tf
        │   └── variables.tf
        ├── snowflake_iam
        │   └── (略)
        └── snowflake_main
            └── (略)

メモ:余力あればサンプルコードを公開する

おわりに

最初は技術的なことを中心に書いていこうと思っていましたが、感じたことを自由気ままにに記載していたら、Terraform×Snowflakeの検討を推進にはチーム間の連携がとても大切であると再確認でき、全体的にチーム間の意識合わせをしっかりやっていきましょう!という内容になってしまいました。また最後の方は力尽きたので気が向いたときに補足します。

もともと書こうと思っていた実装の具体的なところや変更/削除の注意点などについては次回書きたいと思います。(忘れないうちに残しておきたいな...)

リアルはより複雑になると思いますが、Terraform×Snowflakeの検討を推進していく際の参考になれば幸いです。

GCP リソースを Terraform で動かすときに doesn't match regexp エラーから学べること

問題のあるコード

以下のTerraform コードで test-name2 = aaa とすると doesn't match regexp エラーとなります。

variable "test-name" {
  type = string
}

locals {
  name_suffix = "${var.test-name}-aaaa"
}

resource "google_project_iam_custom_role" "my-custom-role" {
  role_id     = "${local.name_suffix}-bbbb"
(略)

resource "google_service_account" "custom_sample_user" {
  project      = var.project
  account_id   = "${local.name_suffix}-iam-custom-service-gcp"
(略)

エラー内容

│ Error: "role_id" ("aaa-aaaa-bbbb") doesn't match regexp "^[a-zA-Z0-9_\\.]{3,64}$"
│
│   with google_project_iam_custom_role.my-custom-role,
│   on iam_custom.tf line 17, in resource "google_project_iam_custom_role" "my-custom-role":
│   17:   role_id     = "${local.name_suffix}-bbbb"
│
╵
╷
│ Error: "account_id" ("aaa-aaaa-iam-custom-service-gcp") doesn't match regexp "^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$"
│
│   with google_service_account.custom_sample_user,
│   on iam_custom.tf line 39, in resource "google_service_account" "custom_sample_user":
│   39:   account_id   = "${local.name_suffix}-iam-custom-service-gcp"

原因

結論から述べると、エラーメッセージの通り正規表現を満たしていないことが原因です。

構文誤りを疑う前にメッセージの通り正規表現をチェックするのが正解

まず GCP カスタムロールの role_id^[a-zA-Z0-9_\\.]{3,64}$ です。これはアンダーバー(_)を満たすものの、ハイフン(-)は満たしません。 正規表現チェッカーで確認すると以下のようになります。

f:id:nyuuuk:20210727023948p:plain

次に GCP のサービスアカウントの account_id^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$ です。これはハイフン(-)を満たすもののアンダーバー(_)は満たしません、また文字数は30文字以内である必要があります。今回のケースでは 31 文字となりエラーとなりました。

f:id:nyuuuk:20210727024827p:plain

このようにリソースごとに求められるパラメータの正規表現が異なります。そのため、Terraform の命名規則に従ったのに動かない...なんてこともあると思います。とはいえ事前に見切るのも難しいので、設計した命名規則で早めに動作確認をしておくのがよいと思います。

まとめ

  • Terraform のリソース命名規則に注意
    • リソースごとに異なる
    • 文字数、ハイフン(-)、アンダーバー(_)などの制限に注意
  • GCP のカスタムロール(google_project_iam_custom_role)の role_id
    • 正規表現^[a-zA-Z0-9_\\.]{3,64}$
      • 文字数:3文字以上64文字以内なら OK、それ以外は NG
      • ハイフン: NG
      • アンダーバー: OK
  • GCP の GSA(google_service_account)の account_id
    • 正規表現^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$
      • 文字数:30文字以内は OK、31文字以上は NG
      • ハイフン:部分的に OK
      • アンダーバー: NG
  • 正規表現チェッカーが便利

Codebuild と Gitlab 連携

CodeBuild と Gitlab

AWS Codebuild はネイティブな Gitlab 連携をサポートしていません。

f:id:nyuuuk:20200615041404p:plain

一方で、Github Enterprise を選択することで Gitlab と連携する方法が紹介されています。 qiita.com qiita.com

こちらの方法を試してみようと思い、CodeBuild -> ALB -> Gitlab 経由で接続を試みましたが、「CLIENT_ERROR: authentication required for primary source」 というエラーに悩まされました...

f:id:nyuuuk:20200615041653p:plain

これは CodeBuild が Gitlab リポジトリからソースをダウンロードする際に発生する認証エラーになります。

設定ミスかな~?などの自問自答を繰り返し試行錯誤していましたが、接続先を Public なリポジトリにしても認証エラーが発生するのでメダパニーマ状態。

今回はこのエラーで調査した内容をまとめます。

環境

nginx をプロキシとして利用して、CodeBuild-> nginx -> Gitlab の経路で接続します。 nginx を間に挟む理由は、エラーの内容をデバッグするためです。

Gitlab

認証 Token の動きをみるため、まず初めに Internal / Private なリポジトリで検証を進めていきます。

docs.gitlab.com

nginx の設定

以下のように設定します。

upstream backend {
    server [gitlab host]:9010 weight=1;
}

server {
    listen 8888;
    server_name [proxy host];

    location / {
        proxy_pass http://backend;
    }
}

nginx のログ

nginx のログ level を debug にしておきます。

hiroyukim.hatenablog.jp

Gitlab Deploy Token

公式の手順に従ってユーザーと Token を作成します。 ここでは、ユーザー名を "hoge"、作成された Token を "m21md2SLq3o3ueKwcVYM" として進めていきます。

f:id:nyuuuk:20200615042929p:plain

Github Enterprise 個人用アクセストーク

Github Enterprise では個人用アクセストークンを入力するフィールドがあります。 この入力内容は Authorization Header の Basic 認証 Token として利用されることになります(後述)。 ここでは以下のように、ユーザー名を "hoge"、パスワードを "m21md2SLq3o3ueKwcVYM" として設定します。

f:id:nyuuuk:20200615042851p:plain

CodeBuild の実行とエラーログ確認

この状態で CodeBuild を実行すると、nginx の access.log は以下のようになります。

[NATGW_IP] - hoge [13/Jun/2020:18:47:33 +0000] "GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.1" 401 26 "-" "git/1.0" "-"
[NATGW_IP] - hoge [13/Jun/2020:18:47:34 +0000] "GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.1" 401 26 "-" "git/1.0" "-"
[NATGW_IP] - hoge [13/Jun/2020:18:47:35 +0000] "GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.1" 401 26 "-" "git/1.0" "-"
[NATGW_IP] - hoge [13/Jun/2020:18:47:36 +0000] "GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.1" 401 26 "-" "git/1.0" "-"
[NATGW_IP] - hoge [13/Jun/2020:18:47:37 +0000] "GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.1" 401 26 "-" "git/1.0" "-"
[NATGW_IP] - hoge [13/Jun/2020:18:47:38 +0000] "GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.1" 401 26 "-" "git/1.0" "-"

検証ということもあり、root ユーザーでリポジトリ(git-test)を作成しています。[NATGW_IP] は Private Subnet で CodeBuild を実行したときのソース IP(NAT Gateway の IP) となります。

nginx のデフォルトのログフォーマットでは、"hoge" が $remote_user になります。 この $remote_user は、Basic 認証時に利用されるユーザー名になり、リターンコードが 401 で返却されており、認証に失敗していることがわかります。 nginx の error.log を確認すると、以下のようなログが出力されています。

2020/06/13 19:31:06 [debug] 1089#0: *23 http proxy header: "User-Agent: git/1.0"
2020/06/13 19:31:06 [debug] 1089#0: *23 http proxy header: "Accept: */*"
2020/06/13 19:31:06 [debug] 1089#0: *23 http proxy header: "Authorization: Basic aG9nZTptMjFtZDJTTHEzbzN1ZUt3Y1ZZTTp4LW9hdXRoLWJhc2lj"
2020/06/13 19:31:06 [debug] 1089#0: *23 http proxy header: "Accept-Encoding: gzip"
2020/06/13 19:31:06 [debug] 1089#0: *23 http proxy header:
"GET /root/git-test.git/info/refs?service=git-upload-pack HTTP/1.0
Host: backend
Connection: close
User-Agent: git/1.0
Accept: */*
Authorization: Basic aG9nZTptMjFtZDJTTHEzbzN1ZUt3Y1ZZTTp4LW9hdXRoLWJhc2lj
Accept-Encoding: gzip

"

この Autorization の Token をデコードしてみます。

 $ echo -n "aG9nZTptMjFtZDJTTHEzbzN1ZUt3Y1ZZTTp4LW9hdXRoLWJhc2lj" | base64 -d
hoge:m21md2SLq3o3ueKwcVYM:x-oauth-basic

...あれ?末尾に ":x-oauth-basic" が付与されていますね。 上記 Deploy Token で作成したユーザーと Token は "hoge:m21md2SLq3o3ueKwcVYM" ですが、CodeBuild のリクエストでは "hoge:m21md2SLq3o3ueKwcVYM:x-oauth-basic" で Gitlab の認証を行うため、401 のエラーを返していました。

対策

"hoge:m21md2SLq3o3ueKwcVYM" を base64 エンコードすると以下になります。

 $ echo -n "hoge:m21md2SLq3o3ueKwcVYM" | base64
aG9nZTptMjFtZDJTTHEzbzN1ZUt3Y1ZZTQ==

これより nginx の設定を以下のように変更します。

upstream backend {
    server  [gitlab-host]:9010 weight=1;
}

server {
    listen 8888;
    server_name [proxy-host];

    location / {
        set $authorization_value $http_authorization;
        if ($http_authorization = "Basic aG9nZTptMjFtZDJTTHEzbzN1ZUt3Y1ZZTTp4LW9hdXRoLWJhc2lj") {
            set $authorization_value "Basic aG9nZTptMjFtZDJTTHEzbzN1ZUt3Y1ZZTQ==";
        }
        proxy_set_header Authorization $authorization_value;
        proxy_pass http://backend;
    }
}

nginx を再起動して、CodeBuild を実行すると... 失敗を繰り返していた DOWNLOAD_SOURCE に成功しました!

f:id:nyuuuk:20200615043117p:plain

Public リポジトリに接続するには?

上記は Private / Internal のリポジトリを対象としていましたが、Public なリポジトリに接続するにはどうしたらよいでしょうか? 想定では、nginx の設定で空の Authorization Header に置換すればできそうな気がします。 これを実際に試してみます。

まずは CodeBuild のソース編集画面から、Github Enterprise 個人用アクセストークンで hoge のみを指定。

ここでは hoge ユーザーのみを指定します。 ユーザーもパスワードも指定しない、ということもできます。

CodeBuild から Gitlab への Authorization Header リクエスト内容は以下のようになります。

$ echo -n "hoge:x-oauth-basic" |base64
aG9nZTp4LW9hdXRoLWJhc2lj

今回は Public なリポジトリから Pull したいので認証は不要です。nginx の設定で空の Authorization Header に置換するようにします。

upstream backend {
    server  [gitlab-host]:9010 weight=1;
}

server {
    listen 8888;
    server_name [proxy-host];

    location / {
        set $authorization_value $http_authorization;
        if ($http_authorization = "Basic aG9nZTp4LW9hdXRoLWJhc2lj") {
            set $authorization_value "";
        }
        proxy_set_header Authorization $authorization_value;
        proxy_pass http://backend;
    }
}

nginx を再起動して、CodeBuild を実行すると...無事に Public なリポジトリに接続することができました!

ちなみに nginx には以下のように Authorization Header が書き換えられたログが出力されています。

(略)
2020/06/13 21:57:25 [debug] 1879#0: *5 http script complex value
2020/06/13 21:57:25 [debug] 1879#0: *5 http script var: "Basic aG9nZTp4LW9hdXRoLWJhc2lj"
2020/06/13 21:57:25 [debug] 1879#0: *5 http script set $authorization_value
2020/06/13 21:57:25 [debug] 1879#0: *5 http script var
2020/06/13 21:57:25 [debug] 1879#0: *5 http script var: "Basic aG9nZTp4LW9hdXRoLWJhc2lj"
2020/06/13 21:57:25 [debug] 1879#0: *5 http script value: "Basic aG9nZTp4LW9hdXRoLWJhc2lj"
2020/06/13 21:57:25 [debug] 1879#0: *5 http script equal
2020/06/13 21:57:25 [debug] 1879#0: *5 http script if
2020/06/13 21:57:25 [debug] 1879#0: *5 http script value: ""
2020/06/13 21:57:25 [debug] 1879#0: *5 http script set $authorization_value
(略)

SSL 通信の場合でも CodeBuild から接続できる?

上記は全て HTTP プロトコルで検証を進めてきました。 本番環境では Gitlab や nginx へのアクセスも、SSL 通信を意識することが多いでしょう。 ここでは以下のような構成でもう少し動作確認を進めてみます。

  • CodeBuild
    • Github Enterprise を選択(ただし nginx で Authorization Header を削除)
    • 自己証明書の場合、CodeBuild -> nginx への SSL 通信で pem ファイルが必要(S3 格納)
  • nginx
    • 今回は自己証明書を作成(server.crt / server.key)
    • SSL のリバースプロキシ
upstream backend {
    server [gitlab host]:6443 weight=1;
}

# HTTP -> HTTPS リダイレクトなども考える(今回は入れませんでした)

server {
    listen 6443 ssl;
    server_name [nginx host];

    ssl_protocols TLSv1.2;
    ssl_ciphers EECDH+AESGCM:EECDH+AES;
    ssl_ecdh_curve prime256v1;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    location / {
        if ($http_authorization = "Basic aG9nZTp4LW9hdXRoLWJhc2lj") {
            set $authorization_value "";
        }
        proxy_set_header Authorization $authorization_value;
        proxy_pass https://backend;
    }
}

都合上 6443 ポートを使用するにようしています(gitlab 起動時に 6443 を SSL の Port に指定しています)

  • gitlab
    • 今回は自己証明書を作成(gitlab.crt / gitlab.key)

Gtilab リポジトリHTTPS プロトコルで指定(https://[nginx host]:6443/root/git-test.git)して CodeBuild を実行します。 nginx で SSL 終端が行われ、これまでと同様に Authorization Header が削除されます。 その後、gitlab から無事にソースをダウンロードすることができました。

実際に proxy_pass を指定する場合は、DNS cache に注意する必要があります。 www.subthread.co.jp

まとめ

  • CodeBuild から Gitlab を直接呼び出そうとすると、401 の 認証エラーが発生します
  • CodeBuild から ALB などを経由して Gitlab に接続しても同様のエラーが発生します
  • これは、CodeBuild の Github Enterprise で Gitlab リポジトリに接続しようとすると、Authorization Header に想定外の文字列(":x-oauth-basic")が付与されるからです
  • CodeBuild から Gitlab に接続するためには、Authorization Header を適切な形式に変換する必要があります(":x-oauth-basic" を削除)
  • nginx などのプロキシを用いることで、Authorizaiton Header を変換して CodeBuild から Gitlab の接続ができるようになります