この記事はユニークビジョン株式会社 Advent Calendar 2024の11日目です。
はじめに
最近、APIリクエストを受けた際に自動的にGitHubブランチを作成する機能を実装しました。
単純にGitHub APIを直接呼び出すことも可能ですが、そのアプローチには以下のような課題があります:
- APIトークンの安全な管理
特定のユーザーにトークンを紐づけておくと、そのユーザーが異動や退職した際にトークンの管理が困難になります。 - 大規模システムにおける権限管理の複雑さ
複数の作業を自動化する場合、APIトークンの実装だとスコープが広くなり、権限管理が難しくなります。
これらの課題に対応するため、今回はGitHub Appを活用した実装方法を選択しました。本記事では、その実装と得られた知見について紹介します。
Github Appとは
GitHub Appは、GitHubの機能を拡張するツールです。GitHub上での操作(Issue作成、PRへのコメント、プロジェクト管理など)が可能であったり、GitHub上のイベントに基づいて外部サービスと連携可能(例:IssueがオープンされたらSlackに投稿)です。
以下のような特徴があります。
- 細かい権限設定が可能
- リポジトリ単位で必要最小限の権限を付与できます
- 例:イシューの作成のみ、PRの作成のみなど
- webhook対応
- GitHubのイベントをリアルタイムで検知し、自動処理を実行可能
- JWT認証による安全な通信
- 一時的なトークンを使用するため、セキュリティリスクを軽減
GitHub Marketplaceから、様々なGitHub Appをインストールできます。自分でGitHub Appを作成することも可能です。どちらの場合も、使いたいリポジトリに対してインストールする必要があります。
GitHub Appのしくみ
大きく分けてWebhookをトリガーとして実行する方法と、APIを直接呼び出す方法、Github Actionsをトリガーとして実行する方法があります。今回はAPIを直接呼び出す方法を紹介します。
シーケンス図は以下のようになっています。
この図は、クライアントからのリクエストを起点に、以下の流れでGitHub操作を実行します:
- JWT認証によりGitHub Appの認証
- 認証後、一時的なアクセストークンを取得
- トークンを使用してGitHub APIを呼び出し
- 結果をクライアントに返却
GitHub Appの作成
- GitHubのGitHub Appsにアクセスします。
- 「New GitHub App」をクリックします。
- GitHub App nameを入力します。ここは全世界でユニークである必要があるので、個人で使う限りはmisty1999-github-appのようにprefixとして自分のIDを入れると良いと思います。Homepage URLはなんでもいいです。Webhook URLは今回は設定する必要はありません。
- 「Create GitHub App」をクリックします。このような画面になります。
- 下のほうにある「Generate a private key」をクリックします。「Generate」をクリックします。秘密鍵がダウンロードされます。
- 権限を設定します。左側の「Permissions&events」をクリックします。Repository permissionsのところで、Repository permissions>issuesをRead & writeを選択します。
以上でGitHub Appの作成は完了です。作成したGitHub AppのClient IDと秘密鍵をバックエンドAPIで使います。
続いて、作成したAppを自分のリポジトリにインストールします。作成したAppのページの左側にある「Install App」をクリックします。自分のユーザー名の右側の「Install」を選択します。only select repositoriesから自分のリポジトリを選択して「Install」を押せば、リポジトリへのインストールが完了します。
Octocrabについて
octocrabは、GitHub APIをラップしたRustのクレートです。GitHub AppやGitHub Actionsに関する操作を直感的に実装することができます。先ほどの図でバックエンドからGitHubにアクセスする処理がありましたが、今回の実装ではバックエンドからAppを呼び出す代わりにoctocrabを使ってクライアントを作成し、そこから呼び出しています。クライアントを使用することで、認証情報やレートリミットの機能を一元的に管理できるメリットがあります。
バックエンドAPIの実装
https://github.com/misty1999/my-git-apps
こちらにGitHub Appを使ったAPIの実装をしています。メインの処理はcreate_issue.rsで行っています。今回はissueを自動作成できるようにしてみました。
use jsonwebtoken::EncodingKey;
use octocrab::Octocrab;
use std::error::Error as StdError;
const REPO_OWNER: &str = "misty1999";
const REPO_NAME: &str = "my-git-apps";
pub async fn create_issue(title: &str, body: &str) -> Result<String, String> {
// 設定を取得
let config = crate::config::Config::from_env()
.map_err(|e| format!("設定の読み込みに失敗: {}", e))?;
let app = config.github_app
.ok_or("GitHub App設定が見つかりません")?;
ここまでで、GitHub Appの設定を読み込んでいます。REPO_OWNER
とREPO_NAME
は自分のリポジトリを指定しています。config.github_appには、GitHub AppのIDと秘密鍵が入っています。config.rsで、.env
とprivate-key.pem
を読み込んでいます。Github AppのIDと秘密鍵をそれぞれのファイルに入れています。
// GitHub Appクライアントの作成
let octocrab = Octocrab::builder()
.app(
octocrab::models::AppId(
app.id.parse::<u64>()
.map_err(|e| format!("GitHub App IDのパースに失敗: {}", e))?
),
EncodingKey::from_rsa_pem(app.secret.trim().as_bytes())
.map_err(|e| format!("RSA秘密鍵のパースに失敗: {}", e))?
)
.build()
.map_err(|e| format!("Octocrabインスタンスの作成に失敗: {}", e))?;
// インストールトークンの取得
let installation = octocrab
.apps()
.get_repository_installation(REPO_OWNER, REPO_NAME)
.await
.map_err(|e| format!("インストレーション情報の取得に失敗: {}\nステータスコード: {}",
e.to_string(),
e.source().map_or("不明".to_string(), |s| s.to_string())
))?;
let access_token: octocrab::models::InstallationToken = octocrab
.post(
format!("/app/installations/{}/access_tokens", installation.id),
None::<&()>
)
.await
.map_err(|e| e.to_string())?;
この部分は特に重要で、GitHub Appの認証フローの核となる処理です:
- 秘密鍵をセットしたクライアントを作成
- 秘密鍵から作成したjwtによる認証を行う
- 認証が完了したので、取得したインストールIDを使用してアクセストークンを発行
// Issueの作成
let issue = Octocrab::builder()
.personal_token(access_token.token)
.build()
.map_err(|e| e.to_string())?
.issues(REPO_OWNER, REPO_NAME)
.create(title)
.body(body)
.send()
.await
.map_err(|e| e.to_string())?;
Ok(format!("Issue '{}' を作成しました (#{})", title, issue.number))
}
認証が完了したので、GitHub APIを呼び出してIssueを作成しています。
まとめ
・GitHub Appを使うことで、属人化を防ぎつつ、よりセキュリティに配慮した実装が可能
・GitHub Appをクライアントから使う場合、jwtを使って認証を行い、それを使ってアクセストークンを取得するという多段階の認証を行う必要がある
・octocrabを使うと、この認証を直感的に実装できる