Amazon Web Services ブログ

AWS 上で大規模な GitHub Actions のセルフホステッドランナーを使用する際のベストプラクティス

注記: お客様は自身の GitHub ランナーを管理する必要がなくなりました。AWS CodeBuild を使用すると、管理された GitHub Actions セルフホストランナーを利用できるようになり、強力なセキュリティ境界と低い起動レイテンシーを備えた一時的でスケーラブルなランナー環境を提供します。CodeBuild を使えば、独自のインフラストラクチャを維持したり、スケーリングロジックを構築する必要がありません。すべてが CodeBuild によって完全に管理されます。開始するには、単に Webhook を作成して、CodeBuild で GitHub Actions ジョブを自動的にトリガーするだけです。

概要

GitHub Actions は、ワークロードのビルド、テスト、デプロイ活動を自動化できる継続的インテグレーションおよび継続的デプロイプラットフォームです。GitHub セルフホステッドランナーは、GitHub Actions パイプラインを実行するための柔軟でカスタマイズ可能なオプションを提供します。これらのランナーを使用すると、独自のインフラストラクチャ上でビルドを実行でき、コードのビルド、テスト、デプロイ環境を制御できます。これにより、セキュリティリスクとコストを削減し、GitHub ホステッドランナーでは利用できない特定のツールやテクノロジーを使用できるようになります。このブログでは、AWS 環境に GitHub セルフホステッドランナーをデプロイする際に考慮すべきセキュリティ、パフォーマンス、コストのベストプラクティスを探ります。

ベストプラクティス

あなたのセキュリティにおける責任を理解する

GitHub のセルフホステッドランナーは、設計上、ワークフロースクリプトやリポジトリのビルドプロセスを通じて、 GitHub リポジトリ内で定義されたコードを実行します。 AWS ランナー実行環境のセキュリティは、 GitHub の実装のセキュリティに依存することを理解する必要があります。 GitHub のセキュリティの完全な概要は本ブログの範囲外ですが、 GitHub の環境を AWS の環境と統合する前に、少なくとも以下の GitHub のセキュリティ設定を確認し、理解することをお勧めします。

  • GitHub のユーザーを連携し、ディレクトリを通じてアイデンティティのライフサイクルを管理します。
  • GitHub リポジトリの管理特権を制限し、権限を管理できるユーザー、リポジトリへの書き込み、リポジトリ設定の変更、GitHub アプリのインストールを制限します。
  • セルフホステッドランナーの登録と、グループ設定への制御を制限します。
  • GitHub ワークフローへの制御を制限し、サードパーティのアクションを使用する際は GitHub の推奨事項に従います
  • パブリックリポジトリからセルフホステッドランナーへのアクセスを許可しません。

一時的なAWS 認証情報を使用してセキュリティリスクを軽減する

できるだけ一時的な認証情報を使用してください。デフォルトで1時間以内に期限切れになり、ローテーションさせたり明示的に取り消す必要はありません。一時的な認証情報は、 AWS Security Token Service (STS) によって作成されます。フェデレーションを使用してAWSアカウントにアクセスする場合、 assume role 、または Amazon EC2 インスタンスプロファイルや Amazon ECS タスクロールを使用する場合は、すでに STS を使用しています。

ほとんどの場合、 AWS で「実行」されないサービスでも、一時的でない AWS Identity and Access Management (IAM) 認証情報 (アクセスキー) は必要ありません。 AWS の外部のワークロードに IAM ロールを拡張でき、長期的な認証情報を管理する必要がなくなります。 GitHub Actions では、 OpenID Connect (OIDC) を使用することをお勧めします。 OIDC は分散型認証プロトコルで、 sts:AssumeRoleWithWebIdentity を使って STS によってネイティブにサポートされています。 GitHub やその他の多くのプロバイダーもサポートしています。 OIDC を使えば、個々の GitHub リポジトリとそれらの対応するアクションに最小特権の IAM ロールを作成できます。 GitHub Actions はこの目的で利用できる OIDC プロバイダーを各アクションの実行に公開しています。

一時的な AWS 認証情報と GitHub セルフホステッドランナー

個別にロールを付与したいリポジトリが多数ある場合、単一のアカウントでの IAM ロールの数に制限に達する可能性があります。私はマルチアカウント戦略でこの問題を解決することを提案しますが、かわりに以下に挙げるアプローチで拡張することもできます:

  • 属性ベースのアクセス制御 (ABAC) を使用して、GitHub トークン内の要求 (リポジトリ名、ブランチ、またはチームなど) を AWS リソースタグと照合する。
  • GitHub のリポジトリをチームまたはアプリケーションに論理的にグループ化することで、役割のサブセットを少なくするための役割ベースのアクセス制御 (RBAC) を使用します。
  • GitHub ワークフローに提供されたIDに基づいて、ID ブローカーパターンを使用して認証情報を動的に提供します。

エフェメラルランナーを使用する

GitHub Action のランナーを「エフェメラル」モードで実行するように設定します。これによりジョブごとに要求に応じて個別の一時的な実行環境が作成 (および破棄) されます。この短い環境の存続期間とビルドごとの分離により、マルチテナントの継続的インテグレーション環境であっても基盤となるホストで各ビルドジョブが他のジョブから分離されているため、データ漏洩のリスクが軽減されます。

ジョブごとに新しい環境がオンデマンドで作成されるためアイドル状態のランナーを待つ必要がなく、auto-scalingが簡単になります。オンデマンドでランナーをスケーリングできるため、ビルドインフラストラクチャを不要な時 (例えば営業時間外) に停止する必要がなくコスト効率の良い設定が可能です。さらに最適化するには、開発者がワークフローにインスタンスタイプタグを付けて、それぞれのワークフローに最適なインスタンスタイプを起動できるようにすることを検討してください。

エフェメラルランナーを使用する際には、いくつかの考慮事項があります:

  • ジョブはランナー EC2 インスタンスが起動し、準備が整うまでキューに残されます。これには最大 2 分かかる場合があります。このプロセスを高速化するには、前提条件がすべてインストールされた最適化された AMI を使用することをご検討ください。
  • 各ジョブが新しいランナーで起動されるため、ランナーでキャッシングを利用することはできません。例えば、 Docker イメージやライブラリは常にソースから取得されます。

ランナーグループを使用しセキュリティ要件に基づいてランナーを分離する

単一の GitHub ランナーグループでエフェメラルランナーを使用すると、そのランナーグループを共有するすべてのリポジトリで使用される同じ AWS アカウントのリソースプールを作成することになります。組織のセキュリティ要件により、実行環境をリポジトリごとやデプロイ環境 (開発、テスト、本番など) ごとにさらに分離する必要がある場合があります。

ランナーグループを使うと、リポジトリごとにワークフローを実行するランナーを定義できます。複数のランナーグループを作成すると、さまざまな種類のコンピューティング環境を提供できるだけでなく、ワークフローの実行を相互に分離された AWS の場所に配置できます。たとえば開発ワークフローを1つのランナーグループに、テストワークフローを別のランナーグループに配置することができ、それぞれのエフェメラルランナーグループは別の AWS アカウントにデプロイされます。

ランナーとは、 GitHub ユーザーに代わってコードを実行するものです。最低限、エフェメラルランナーグループを AWS アカウントに含め、この AWS アカウントの組織の他のリソースへのアクセスを最小限に抑えることをお勧めします。組織のリソースへのアクセスが必要な場合は、 OIDC による IAM ロールの引き受けを通じて、リポジトリごとに付与できます。そしてこれらのロールには、必要なリソースに対する最小特権アクセスのみを与えることができます。

Amazon EC2 ウォームプールを使用してランナーの起動時間を最適化する

エフェメラルランナーは、強力なビルド分離、シンプルさ、セキュリティを提供します。ランナーは必要に応じて起動されるため、ジョブはランナーの起動と GitHub への登録を待つ必要があります。通常 2 分以内に行われますが、この待ち時間はある状況では許容できない可能性があります。

事前に登録されたエフェメラルランナーのウォームプールを使用すると、待ち時間を短縮できます。これらのランナーは、 GitHub ワークフローイベントの受信を積極的に待ち受け、ワークフローイベントが発生するとすぐに、登録済みの EC2 ランナーのウォームプールでピックアップされます。

ウォームプールを管理するための複数の戦略がありますが、 AWS Lambda を使ってエフェメラルランナーをスケールアップ・ダウンする以下の戦略をお勧めします:

GitHub セルフホステッドランナーのウォームプールの流れ

GitHub ワークフローイベントは、マスターリポジトリへのコードのプッシュやプルリクエストのマージなどのトリガーで作成されます。このイベントは、Webhook と Amazon API Gateway エンドポイントを介して Lambda 関数をトリガーします。Lambda 関数は、GitHub ワークフローイベントペイロードの検証とオブザーバビリティ用のイベントログ、メトリクスの構築に役立ちます。オプションで、ウォームプールの補充にも使用できます。ウォームプールの EC2 インスタンスを起動、スケールアップ、スケールダウンするための別の バックエンド Lambda 関数があります。EC2 インスタンス (ランナー) は起動時に GitHub に登録されます。登録されたランナーは、GitHub の内部ジョブキューを使って着信の GitHub ワークフローイベントをリッスンし、ワークフローイベントがトリガーされるとすぐに、ウォームプールの 1 つのランナーにジョブの実行が割り当てられます。ジョブが完了すると、ランナーは自動的に登録解除されます。ジョブは、GitHub ワークフローで定義されたビルドやデプロイリクエストです。

ウォームプールを導入することで、待ち時間が 70〜80% 短縮されることが期待されます。

考慮事項

  • ランナーのオーバープロビジョニングの可能性があるため、複雑さが増します。これは、ランナーの EC2 インスタンスが起動して準備完了状態に達するまでの時間と、スケールアップ Lambda が実行される頻度に依存します。例えば、スケールアップ Lambda が 1 分ごとに実行され、 EC2 ランナーの起動に 2 分かかる場合、スケールアップLambdaは 2 つのインスタンスを起動します。これを軽減するには、 Auto Scaling グループを使用して EC2 ウォームプールと所望の容量を管理し、 GitHub ワークフローイベント(ビルドジョブリクエストなど)に基づく予測スケーリングポリシーを関連付けます。
  • この戦略は、Windows または Mac ベースのランナーをサポートする場合、起動時間が変わる可能性があるため、修正する必要があるかもしれません。

GitHub セルフホステッドランナーの起動を高速化するために最適化された AMI を使用する

Amazon Machine Images (AMI) は、ランナーの EC2 インスタンスを起動するために使用できる、事前構成され最適化されたマシンイメージを提供します。AMI を使用することで、依存関係とツールがすでにインストールされているため、新しいランナーの起動時間を短縮できます。すべてのインスタンスが同じバージョンの依存関係とツールを実行するため、ビルド間の一貫性が保証されます。マシンイメージはランナーインスタンスとして使用される前にテストおよび承認されるため、ランナーは安定性と セキュリティコンプライアンスの向上の恩恵を受けることができます。

GitHub のセルフホステッドランナーとして使用する AMI を構築する際には、以下の点に留意する必要があります:

  • ビルドに適切な OS ベースイメージを選択します。これはご利用されている技術スタックとツールセットに依存します。
  • イメージの一部として GitHub ランナーアプリをインストールします。ランナーの管理オーバーヘッドを減らすために、自動ランナー更新を有効にしてください。特定のランナーバージョンを使用する必要がある場合は、テストされていない変更を回避するためにランナーの自動更新を無効にすることができます。無効にした場合、新しいバージョンが利用可能になった後 30 日以内にランナーを手動で更新する必要があることに注意してください。
  • 信頼できるソースからビルドツールと依存関係をインストールします。
  • ランナーログがキャプチャされ、選択した SIEM (Security Information and Event Management )に転送されることを確認します。
  • ランナーには GitHub にアクセスするためのインターネット接続が必要です。これには、ネットワーク設定に応じてインスタンスでプロキシ設定を構成する必要がある場合があります。
  • ランナーが必要とするあらゆるアーティファクトリポジトリを構成します。これにはソースと認証が含まれます。
  • EC2 Image Builder などのツールを使用して、AMI の作成を自動化し、一貫性を実現します。

スポットインスタンスを利用してコストを削減する

ランナーのスケーリングアップおよびホットプールの維持に関連するコストは、オンデマンド価格と比較して最大 90% の節約につながるスポットインスタンスを使用することで最小限に抑えることができます。ただし、 2 分間の通知でスポットインスタンスが終了することを許容できない、長時間実行されるビルドやバッチジョブが必要となる場合があります。そのため、そのようなジョブはオンデマンド EC2 インスタンスにルーティングし、その他のジョブはスポットインスタンスで処理するという、インスタンスの混合プールを持つことが適切なオプションとなります。これは、ランナーの起動 / 登録時にラベルを割り当てることで実現できます。その場合オンデマンドインスタンスが起動され、コスト削減のために Savings Plans を適用することができます。

Amazon CloudWatch を使用して、監視のためのランナーメトリクスを記録する

EC2 ベースの GitHub セルフホステッドランナーのメトリクスを生成することは、全体的なプラットフォームの可観測性にとって非常に重要です。GitHub ランナーのメトリクスの例としては、1 分間に待機中または完了した GitHub ワークフロー イベントの数、ウォームプールで起動して利用可能な EC2 ランナーの数などがあります。

トリガーされたワークフローイベントとランナーログを Amazon CloudWatch に記録し、 CloudWatch の組み込みメトリクスを使用して、キューイングされたワークフローイベント数、進行中のイベント数、完了したイベント数などのメトリクスを収集できます。ワークフローイベントペイロードの一部である “started_at” と “completed_at” のタイミング要素を使用すると、ビルド待ち時間を計算できます。

例として、以下は Amazon Cloud Watch Logs に記録された GitHub ワークフローイベントのサンプルです。

{
    "hostname": "xxx.xxx.xxx.xxx",
    "requestId": "aafddsd55-fgcf555",
    "date": "2022-10-11T05:50:35.816Z",
    "logLevel": "info",
    "logLevelId": 3,
    "filePath": "index.js",
    "fullFilePath": "/var/task/index.js",
    "fileName": "index.js",
    "lineNumber": 83889,
    "columnNumber": 12,
    "isConstructor": false,
    "functionName": "handle",
    "argumentsArray": [
        "Processing Github event",
        "{\"event\":\"workflow_job\",\"repository\":\"testorg-poc/github-actions-test-repo\",\"action\":\"queued\",\"name\":\"jobname-buildanddeploy\",\"status\":\"queued\",\"started_at\":\"2022-10-11T05:50:33Z\",\"completed_at\":null,\"conclusion\":null}"
    ]
}

上記のログから \”status\”:\”queued\”,\”repository\”:\”testorg-poc/github-actions-test-repo\c, \”name\”:\”jobname-buildanddeploy\” ,and workflow \”event\” の要素を取り込んでメトリクスを使用するには、選択した言語に基づいた CloudWatch メトリクスクライアントライブラリを使用して、アプリケーションコードまたは メトリクスを送信するための Lambda を利用して埋め込みメトリクスを構築できます。( クライアントライブラリを使用した埋め込みメトリクス形式でのログの作成 )

原則それらのライブラリの 1 つが内部で行うのは、ログイベントから要素をディメンションフィールドにマップすることで、CloudWatch がそれを読み取りメトリクスを生成することができるようになります。

console.log(
   JSON.stringify({
      message: '[Embedded Metric]', // Identifier for metric logs in CW logs
      build_event_metric: 1, // Metric Name and value
      status: `${status}`, // Dimension name and value
      eventName: `${eventName}`,
      repository: `${repository}`,
      name: `${name}`,

      _aws: {
         Timestamp: Date.now(),
         CloudWatchMetrics: [{
            Namespace: `demo_2`,
            Dimensions: [
               ['status', 'eventName', 'repository', 'name']
            ],
            Metrics: [{
               Name: 'build_event_metric',
               Unit: 'Count',
            }, ],
         }, ],
      },
   })
);

サンプルアーキテクチャー

GitHub Webhook イベントの処理

CloudWatchメトリクスは、要件に基づいてダッシュボードに公開したり、外部ツールに転送したりできます。メトリクスを取得できれば、CloudWatchアラームと通知を構成して、プールの枯渇を管理できます。

まとめ

このブログ記事では、AWS の EC2 セルフホステッドランナーを使用する際のセキュリティ、スケーラビリティ、コスト効率性に関するベストプラクティスをいくつか説明しました。一時的な認証情報とエフェメラルランナーを組み合わせることで、セキュリティとビルド汚染のリスクを軽減できることを説明しました。また、AMI と EC2 ウォームプールを使用することで、ランナーの起動とジョブ実行を高速化できることも示しました。最後に、適切なシナリオでランナーにスポットインスタンスを使用することで、コスト効率を最大化できることを説明しました。

リソース:

本ブログはソリューションアーキテクトの紙谷が翻訳しました。原文はこちらです。