GeekFactory

int128.hatenablog.com

GitHub GraphQLで新しいPull Requestを作成する

GitHub GraphQLで新しいPull Requestを作成するにはcreatePullRequest mutationを利用します。RESTの場合と同様に、以下のようにheadとbaseを指定します。

  • head ref: 適用したい変更が含まれるブランチ。cross repositoryの場合はforkされたリポジトリ
  • base ref: 変更を適用するブランチ。cross repositoryの場合はfork元のリポジトリ。一般にはデフォルトブランチ。

本稿の内容はすべてGraphQL API Explorerで再現可能です。

Pull Requestの作成

ここでは以下のPull Requestを作成します。

まず、head repositoryのIDを取得します。

query {
  repository(owner: "octocat", name: "Spoon-Knife") {
    id
  }
}
{
  "data": {
    "repository": {
      "id": "MDEwOlJlcG9zaXRvcnkxMzAwMTky"
    }
  }
}

createPullRequest mutationを実行します。以下の引数が必須です。

repositoryId (ID!) The Node ID of the repository.

baseRefName (String!) The name of the branch you want your changes pulled into. This should be an existing branch on the current repository. You cannot update the base branch on a pull request to point to another repository.

headRefName (String!) The name of the branch where your changes are implemented. For cross-repository pull requests in the same network, namespace head_ref_name with a user like this: username:branch.

title (String!) The title of the pull request.

https://developer.github.com/v4/input_object/createpullrequestinput/

repositoryIdにはhead repositoryのIDを指定します。また、cross repositoryの場合はheadRefNameにユーザ名のプレフィックスを付けます。以下の例を見てください。

mutation {
  createPullRequest(input: {
    repositoryId: "MDEwOlJlcG9zaXRvcnkxMzAwMTky",
    baseRefName: "master",
    headRefName: "int128:patch-1",
    title: "Example",
    body: "Using the GraphQL mutation."
  }) {
    pullRequest {
      id
    }
  }
}
{
  "data": {
    "createPullRequest": {
      "pullRequest": {
        "id": "MDExOlB1bGxSZXF1ZXN0MzY1MDUyNTMy"
      }
    }
  }
}

これで下記のPull Requestが作成されました。

github.com

Pull Requestの検索

前項で作成したPull Requestを検索します。

  1. head refに紐づくPull Requestを検索する。
  2. base repositoryに存在するPull Requestを検索する。

まず、head refに紐づくPull Requestを検索してみましょう。以下のようにassociatedPullRequests connectionを利用します。

{
  repository(owner: "int128", name: "Spoon-Knife") {
    ref(qualifiedName: "refs/heads/patch-1") {
      associatedPullRequests(baseRefName: "master", first: 1) {
        nodes {
          id
          title
        }
      }
    }
  }
}
{
  "data": {
    "repository": {
      "ref": {
        "associatedPullRequests": {
          "nodes": [
            {
              "id": "MDExOlB1bGxSZXF1ZXN0MzY1MDUyNTMy",
              "title": "Example"
            }
          ]
        }
      }
    }
  }
}

head refから検索する方法はhead refが存在する場合にのみ使えます。Pull Requestをマージ(or クローズ)した後にブランチを削除した場合は、head refが存在しないので検索できなくなります。

次は、base repositoryに存在するPull Requestを検索してみましょう。

{
  repository(owner: "octocat", name: "Spoon-Knife") {
    pullRequests(baseRefName: "master", headRefName: "patch-1", first: 1) {
      totalCount
      nodes {
        id
        title
        url
      }
    }
  }
}
{
  "data": {
    "repository": {
      "pullRequests": {
        "totalCount": 620,
        "nodes": [
          {
            "id": "MDExOlB1bGxSZXF1ZXN0MTU5NzIz",
            "title": "Edited index.html via GitHub",
            "url": "https://github.com/octocat/Spoon-Knife/pull/21"
          }
        ]
      }
    }
  }
}

まったく関係のないPull Requestが出てきてしまいました。この方法では patch-1 というhead refのPull Requestがすべて表示されてしまいます。pullRequests connectionでは検索条件にhead repositoryを入れられないため、フォークされたリポジトリがすべて検索対象になる問題があります。

今のところ、head refから検索する方が確実にPull Requestを絞り込めるのでよさそうです。

アプリケーションの開発フローとGitOps

アプリケーションの開発フローとKubernetesへのデプロイを考えた軌跡を残します。特に結論はないです。

前提

以下を前提とする。

  • Kubernetesにアプリケーションをデプロイする場合を考える。
  • すべての変更はPull Requestを通して適用する。
  • アプリケーションはソースコードを変更することなく、どの実行環境でも動作可能とする。
  • アプリケーションの設定は実行時に環境変数などで注入される。
  • 開発チームはアプリケーションと設定に責任を持ち、自分自身でデプロイを行う。
  • Platformチームは実行環境の健全性に責任を持つ。

以下はスコープ外とする。別に考える必要がある。

  • 実行環境自体のデプロイや変更(例えば、Kubernetesのバージョンアップ)
  • アプリケーションが依存するステートフルなリソース
  • データベーススキーママイグレーション
  • アプリケーションやイメージのセキュリティ検査
  • などなど

選択肢

以下の選択肢が考えられる。ここでは具体的なツールは考えない。

  1. ビルドとデプロイ
    1. アプリケーションのビルドとデプロイを分離する
    2. アプリケーションのビルドとデプロイを連続して行う
  2. ブランチ戦略
    1. tagから実行環境にデプロイする
    2. masterブランチから実行環境にデプロイする
    3. 実行環境ごとにブランチを用意する(例えば、GitLab Flow)
  3. イメージタグの管理
    1. イメージタグをリポジトリで管理する
    2. イメージタグはクラスタ内部でのみ管理する(リポジトリマニフェストには書かない)
  4. アプリケーションとマニフェスト
    1. アプリケーションとマニフェストを別のリポジトリで管理する
    2. アプリケーションとマニフェストを同一リポジトリで管理する

デプロイサイクルの速いチームは、(a-2)のようにブランチの更新を契機にアプリケーションをデプロイした方が無駄な作業が少なくなる。そうしないとdeveloper experienceが大幅に悪化するだろう。一方で、本番環境で問題が発生してすぐに旧バージョンに切り戻したい場合は、(a-1)のようにデプロイを切り離しておく方が早く確実に対応できる。

a, b, c, dには以下の制約がある。

  • アプリケーションのビルドとデプロイを連続して行う場合(a-2)
    • アプリケーションとマニフェストは同じリポジトリに入れる必要がある(d-2)。別のリポジトリにあるとアトミックな更新ができない。
    • 実行環境ごとにブランチを用意する必要がある(b-3)。単一のブランチではデプロイ先の実行環境を判断できない。

以降は具体的なツールを挙げて比較してみよう。

(a-1)(b-1)(c-1)(d-1)

GitOpsを採用する場合に最も簡単なフローから考えてみよう。デプロイの頻度が低いチームはこのフローで十分と思う。

  1. 開発者がアプリケーションリポジトリに新しいタグをpushする。
  2. CIが新しいイメージをビルドする。リポジトリのタグとイメージのタグは名前を合わせておく。
  3. 開発者がマニフェストリポジトリを更新するPRを作成する。
  4. 開発者がマニフェストリポジトリのPRをマージする。
  5. GitOpsツールがマニフェストリポジトリクラスタを同期させる。

メリデメ:

  • 👍 CI/CD外でリソースが変更されても不整合が起こらない。
  • 👍 旧イメージタグへの切り戻しが確実で早い。
  • 😱 デプロイに必要な作業が多すぎる。

(a-1)(b-2)(c-1)(d-1)

前項からdeveloper experienceを改善するためにイメージタグの更新を自動化する。イメージタグはcommit hashから生成する。

  1. 開発者がアプリケーションリポジトリのmasterブランチを更新する。
  2. CIが新しいイメージをビルドする。
  3. CIがマニフェストリポジトリのイメージタグを更新するPRを作成する。
  4. 開発者がマニフェストリポジトリのPRをマージする。もしマニフェストを変更したい場合はPRと合わせて更新する。
  5. GitOpsツールがマニフェストリポジトリクラスタを同期させる。

メリデメ:

  • 👍 開発者はPRをマージするだけで新イメージタグをデプロイできる。
  • 👍 CI/CD外でリソースが変更されても不整合が起こらない。
  • 👍 旧イメージタグへの切り戻しが確実で早い。
  • 😱 マニフェストリポジトリの更新やPRの作成を作り込むのが大変そう。

(a-2)(b-3)(c-1)(d-2)

前項ではPRを挟んでイメージタグを更新していた。ビルドとデプロイを連続して行いたい場合(a-2)、アプリケーションとマニフェストをアトミックに更新できないことが課題となる。そこで、アプリケーションとマニフェストを同じリポジトリで管理する(d-2)。

アプリケーションとマニフェストのライフサイクルが合わなくなるので、実行環境ごとにブランチを用意する(b-3)。

  1. 開発者がリポジトリの (master | staging | production) ブランチに対するPRをマージする。
  2. CIが新しいイメージをビルドする。アプリケーションリポジトリに含まれるマニフェストレンダリングする。
  3. CIがマニフェストリポジトリを更新する。
  4. GitOpsツールがマニフェストリポジトリクラスタを同期させる。

メリデメ:

  • 👍 開発者は実行環境に対応するブランチを更新するだけで新イメージタグをデプロイできる。
  • 👍 CI/CD外でリソースが変更されても不整合が起こらない。
  • 😱 切り戻す場合はアプリケーションのrevert commitが必要になる。
  • 😱 マニフェストの変更だけでもイメージが新しくなってしまう。
  • 😱 マニフェストリポジトリの更新やPRの作成を作り込むのが大変そう。

(a-2)(b-3)(c-2)(d-2)

GitOpsを使わない方法も考えてみよう。まずは、アプリケーションとマニフェストを同じリポジトリで管理する。

  1. 開発者がリポジトリの (master | staging | production) ブランチに対するPRをマージする。
  2. CIが新しいイメージをビルドする。
  3. CIがhelm upgradeを実行する。

もしビルドとデプロイを切り離したい場合(a-1)は、CIの手動ジョブを使えばよい。

メリデメ:

  • 👍 開発者は実行環境に対応するブランチを更新するだけで新イメージタグをデプロイできる。
  • 👍 CIで完結する。複数ツールのピタゴラスイッチがない。
  • 👍 緊急の切り戻しが必要な場合はリソースを直接変更できる。(直接変更できてしまうデメリットでもある)
  • 😱 新しいマニフェストにないリソースを確実に削除するため、Helmが必須になる。他のツールは使えない。
  • 😱 CI/CD外でリソースが変更されると不整合が発生する。
  • 😱 マニフェストの変更だけでもイメージが新しくなってしまう。
  • 😱 リポジトリクラスタの差分を確認することは可能(helm-diffを利用)だが、Argo CDのような分かりやすさはない。

(a-1)(b-2)(c-2)(d-1)

アプリケーションとマニフェストを別のリポジトリで管理する方法もあるが、GitOpsを利用する場合とほぼ同じフローで、わざわざHelmを利用するメリットは小さい。

まとめ

思いつくままに挙げていったところこんな感じです。誰かカルノー図で組み合わせ網羅をお願いします😓

ちなみに、EC2 + Auto Scaling Group + Immutable Infrastructureなサービスの開発チームにいた時は以下のフローでやっていました。

  1. 開発者がアプリケーションリポジトリに新しいタグをpushする。
  2. CIが新しいTar ballをビルドしてS3に配置する。
  3. 開発者がタグ名を指定してデプロイツールを実行する。(社内の内製ツール)
  4. デプロイツールがAuto Scaling GroupのLaunch Configurationを更新する。
  5. デプロイツールがEC2 Instanceをterminateする。

一方で、少人数で速いサイクルの開発を行っているチームでは以下のようなフローでした。

  1. 開発者がアプリケーションリポジトリのmasterブランチにpushする。
  2. CIが開発環境にデプロイする。
  3. 開発者が後続の手動ジョブを実行する。
  4. CIが本番環境にデプロイする。

というわけでオチは特にないです。

GitHub GraphQL APIで新しいブランチを作成する

GitHub GraphQL APIで新しいブランチを作成できるようになっていたので試してみました。本記事の内容はGraphQL API Explorerで実行できます。

リポジトリに新しいブランチやタグを作成するにはcreateRef mutationを利用します。createRefを実行するには以下の引数が必要です。

name (String!) The fully qualified name of the new Ref (ie: refs/heads/my_new_branch).

oid (GitObjectID!) The GitObjectID that the new Ref shall target. Must point to a commit.

repositoryId (ID!) The Node ID of the Repository to create the Ref in.

リポジトリID(repositoryId)とコミットID(oid)を取得するには以下のクエリを実行します。リポジトリ名やブランチ名は必要なものに置き換えてください。

query GetCommitID {
  repository(owner: "int128", name: "sandbox") {
    id
    ref(qualifiedName: "refs/heads/master") {
      target {
        oid
      }
    }
  }
}
{
  "data": {
    "repository": {
      "id": "MDEwOlJlcG9zaXRvcnk2OTgzMjM2Ng==",
      "ref": {
        "target": {
          "oid": "7a59fd9a6334706b942f1707531a0cf8a8523ce7"
        }
      }
    }
  }
}

クエリの実行結果からリポジトリIDは MDEwOlJlcG9zaXRvcnk2OTgzMjM2Ng==、コミットIDは 7a59fd9a6334706b942f1707531a0cf8a8523ce7 であることが分かります。これらのIDをcreateRefに渡します。

以下のmutationを実行するとexample1ブランチが作成されます。

mutation CreateBranch {
  createRef(input: {repositoryId: "MDEwOlJlcG9zaXRvcnk2OTgzMjM2Ng==", name: "refs/heads/example1", oid: "7a59fd9a6334706b942f1707531a0cf8a8523ce7"}) {
    clientMutationId
  }
}

REST APIの場合は1回のリクエストでブランチ作成を実行できますが、GraphQLの場合はqueryとmutationで2回のリクエストが必要です。