N予備校で利用しているAWS CodeBuildについて

この記事は、ドワンゴ Advent Calendar 2022の8日目の記事です。

N予備校では、過去の記事でも触れた通りAWS CodeBuildを日常に利用しています。

この記事では、導入した経緯や実際の利用例などについてご紹介できればと思います。

AWS CodeBuildを導入した経緯

開発当初についてはGo製のCIツールであるDroneを利用しており、v0.5からv0.8まで利用していました。

Droneを利用していた際にはたびたび以下のような事象に遭遇しました。

  • サーバーがなんらかの理由でやたら停止してしまう
  • cacheがよく壊れる
  • エージェント数が十分に確保できておらずビルド開始まで待たされる

構築したインフラ起因での問題も多数ありましたが、開発を進める上で障害にはならないもののやや開発者体験を損なうという、なかなかイマイチな環境の中で開発していたかと思います。

Drone自体はDockerを用いた独立した環境でCIを実現できるという当時では珍しいOSSプロダクトでN予備校においてはテストの自動実行など開発初期から便利に利用していましたが、オンプレミス環境で構築されており社内のインフラ事情により廃止されることになったため、現在ではこのブログのタイトルでもある通りAWS CodeBuildを利用しています。

選定をしていた際には以下の点を重要視していました。

  • 実行環境のスペック
  • メンテナンス性

ドワンゴではオンプレミス環境で運用されているGitHub Enterprise Serverを利用しており、限られたネットワークからのアクセスのみ許可されている事情があっため選定においても選択肢にも限りがありました。

N予備校はAWS上で運用されており、GitHub Enterprise Server上でホスティングされているコードのデプロイや当時オンプレミス環境下で運用されていたニコニコの基盤サービスを利用するというニーズもあったため、オンプレミス環境との通信ではAWS Direct Connectを用いた構成となっています。

ざっくりとした構成図

こういった事情や当時の状況と照らし合わせた結果、AWS内で完結するCodeBuildを採用するに至りました。

とはいえ、これまで課題に感じていた部分などについては以下のような効果が期待できていたため安心して導入を決断できたという背景もあったりします。

  • ビルドプロジェクトごとに柔軟にスペックを選択できる
    • オンデマンドにコストが発生するため、CIを使っていない時間のインフラコストが減る
  • フルマネージドなサービスであるためメンテナンスコストがほとんど発生しない
  • 勝手にスケールしてくれるのでビルド開始までほとんど待たなくて良い

タイミング的にも導入した2019年の1年前にはGithub Enterprise Serverのサポート1が入り、導入から全面的な移行についてもスムーズに進めることができました。

現在では、N予備校の各サーバーサイドアプリケーションやwebフロントなどリポジトリへの導入しており、40個ほどのビルドプロジェクトが作成されており、普段の開発を支えています。

ここ1年間での1日あたりのビルド回数

実際の利用例

Railsアプリケーション

N予備校ではマイクロサービスアーキテクチャを採用しており、構成されるサービスの多くにRuby on Railsが採用されており、継続的に開発しています。

Railsアプリケーションに対してCodeBuild上では主にRuboCopやRSpecを実行しています。

CodeBuildではRubyのランタイムも用意されているものの可能な限りローカルでの開発環境と同じ環境かつDBやKVSと結合したテストを実行したいというモチベーションから、docker-composeを利用した所謂Docker in Dockerの構成をとっています。

またアプリケーションの規模によってはRSpecの実行について、バッチビルドを導入し並列で実行するなどして効率化を図っています。

これらに加えて、OpenAPIを採用しているサービスについてはRedocを用いたドキュメントの生成とGithub Pagesへのpushなども行っています。

Github Pagesへのpushについては工夫をしており、以下のようなことをしています。

  • Pull Requestを作成・更新時には環境変数 CODEBUILD_WEBHOOK_HEAD_REF から得られるマージ元のブランチ名を用いたHTMLファイルを生成し公開
  • Pull Requestがマージされた際には環境変数 CODEBUILD_WEBHOOK_BASE_REF から得られるマージ先のブランチ名を用いたHTMLファイルを生成し公開すると同時にマージ元のブランチ名を用いて生成されたHTMLファイルを削除

こちらについてはシェルスクリプトで実現されており、一部抜粋しこの記事向けに改変して掲載します。

※ あくまでやっているイメージを伝えるもので動作を保証するものではありません

# ./openapi/index.html が生成される
npm run build

git clone --depth 1 --branch gh-pages ${CODEBUILD_SOURCE_REPO_URL} gh-pages

# イベントがPR作成、PR更新の場合
if [ ${CODEBUILD_WEBHOOK_EVENT} = "PULL_REQUEST_CREATED" -o ${CODEBUILD_WEBHOOK_EVENT} = "PULL_REQUEST_UPDATED" ]; then
  # 生成されたHTMLを適切にリネーム
  cp ./openapi/index.html ./gh-pages/${CODEBUILD_WEBHOOK_HEAD_REF#refs/heads/}/


# イベントがPRマージの場合
elif [ ${CODEBUILD_WEBHOOK_EVENT} = "PULL_REQUEST_MERGED" ]; then
  # 生成されたHTMLを適切にリネーム
  if [ ${CODEBUILD_WEBHOOK_BASE_REF#refs/heads/} = "main" ]; then
    cp ./openapi/index.html ./gh-pages/
  fi
  cp ./openapi/index.html ./gh-pages/${CODEBUILD_WEBHOOK_BASE_REF#refs/heads/}/

  # 不要なHTMLを削除
  rm -rf ./gh-pages/${CODEBUILD_WEBHOOK_HEAD_REF#refs/heads/}
fi

cd ./gh-pages
git add .
git commit -a -m "Pull Request: ${CODEBUILD_WEBHOOK_TRIGGER}, Base Branch: ${CODEBUILD_WEBHOOK_BASE_REF#refs/heads/}, Topic Branch: ${CODEBUILD_WEBHOOK_HEAD_REF#refs/heads/}"
git push origin gh-pages:gh-pages

RSpecによるテストの書き方指針やOpenAPIの採用とOpenAPIを用いた開発については以下の記事をご覧ください。

コンピューティングタイプについては、Linux Small(3GB メモリ、2 vCPU) を選択するようにしており、そこそこの規模感のアプリケーションにおいても10分程度でビルドの実行が完了しています。

今後は費用対効果を見極めながら、より性能が高いコンピューティングタイプの採用についても考えていきたいと思います。

Terraform

AWS上のリソースの構成管理にTerraformを利用しています。

主にCodeBuild上では、terraform fmt の実行と terraform plan の実行結果を tfnotify2 を用いて該当するPull Requestにコメントするなどを行っています。

複数のアカウントに対して構成管理を行っている以下のようなディレクトリ構成をとっているようなリポジトリで同様のビルドを実行したい場合、バッチビルドが有効に働きます。

.
├── account-1
│   └── account-1に対するterraform
├── account-2
│   └── account-2に対するterraform
├── account-3
│   └── account-3に対するterraform
└── buildspec.yml

リファレンスによれば、ビルドの設定に関しては batch/<batch-type>/buildspec にてそれぞれのビルドに設定ができますが、この設定がない場合バッチビルドのタイプにより異なるものの、ほとんどのバッチビルドのタイプにて現在のbuildspecファイルが利用されます。

詳細については割愛しますが、バッチビルドを用いたbuildspecファイルの例は以下の通りです。

version: 0.2

batch:
  build-list:
    - identifier: account-1
      env:
        # このバッチビルドで設定されるオプション
        variables:
          TARGET_DIR: account-1

    - identifier: account-2
      env:
        variables:
          TARGET_DIR: account-2

    - identifier: account-3
      env:
        variables:
          TARGET_DIR: account-3
    
env:
  # どのバッチビルドにおいても共通で設定されるオプション

phases:
  # どのバッチビルドにおいても共通で実行されるビルドのシーケンス

Terraformの場合ですと、実行時に -chdir オプションに環境変数 TARGET_DIR 渡すことで、バッチビルド間で同様のコマンドを異なるディレクトリで実行できるようになります。

N予備校の場合ですとこういった一連の処理についてはシェルスクリプトに切り出していたりすることが多く、今回例に挙げたリポジトリについても実際のコマンド実行についてはシェルスクリプト化しています。

ビルドプロジェクトを円滑に用意する工夫

Terraformの話が続きますが、導入時よりCodeBuildのビルドプロジェクトについてはTerraformにて構成管理を行っています。

こちらでは以下のようなシェルスクリプトを用意しており、リポジトリのルートから見た scripts/add-repo ディレクトリ内にある codebuild.tf をenvsubstコマンドを用いて変数展開しTerraformのワーキングディレクトリとなる infra ディレクトリ内に適切な名前で出力しています。

#!/usr/bin/env bash
readonly ROOT_DIR=$(cd $(dirname $0)/../; pwd)
readonly TEMPLATE_DIR=$(cd ${ROOT_DIR}/scripts/add-repo; pwd)
readonly TEMPLATE="${TEMPLATE_DIR}/codebuild.tf"
readonly OUTPUT_DIR=$(cd ${ROOT_DIR}/infra; pwd)

readonly repo=$1
if !(echo $repo | grep -q /); then
  echo "$0: repo must be in 'organization/name' format"
  exit 1
fi

readonly repoOrgs=$(dirname ${repo})
readonly repoName=$(basename ${repo})
if (echo $repoOrgs | grep -q /); then
  echo "$0: repositories with more then one '/' are not suported"
  exit 1
fi

readonly OUTPUT_FILE_NAME="codebuild-${repoOrgs}-${repoName}.tf"
readonly OUTPUT=${OUTPUT_DIR}/${OUTPUT_FILE_NAME}
if [ -e ${OUTPUT} ]; then
  echo "$0: ${OUTPUT}: File exists"
  exit 1
fi

RepoOrgs=${repoOrgs} RepoName=${repoName} envsubst '$RepoOrgs $RepoName' < ${TEMPLATE} > ${OUTPUT}

codebuild.tf は単純にTerraformのmodule を呼び出す構成となっており、呼び出されるmoduleでは以下のことを設定します。

  • CodeBuildのビルドプロジェクトで利用するIAMロール及びポリシーの設定
  • CodeBuildのビルドプロジェクトの設定
  • Github Enterprise Serverで該当するリポジトリに対するWebhookの設定

例えば nyobi/new-app を引数に与えて実行した場合には infra/codebuild-nyobi-new-app.tf ファイルが以下のように生成されます。

module "nyobi-new-app" {
  source = "./modules/codebuild-project"

  iam_role_name = "codebuild-nyobi-new-app"

  codebuild_project_name = "nyobi-new-app"


  # ビルドプロジェクトに環境変数として設定したい情報など
  codebuild_environment_variable = concat(
    # 全プロジェクトで共通設定したい変数のリスト
    local.environment_variables,
  )

  codebuild_source_auth_resource          = var.github_enterprise_machineuser_token
  codebuild_source_location               = "${local.github_base_url}/nyobi/new-app"
  codebuild_vpc_config_security_group_ids = [aws_security_group.allow_incoming_webhook_from_github_enterprise.id]
  codebuild_vpc_config_subnets            = data.aws_subnet.protected.*.id
  codebuild_vpc_config_vpc_id             = data.aws_vpc.private.id

  github_repository_full_name = "nyobi/new-app"
}

# CodeBuildで生成されるビルドバッジのURL
output "nyobi-new-app" {
  value = module.nyobi-new-app.codebuild_project_badge_url
}

ビルドプロジェクトを追加したい開発者は上記のようなシェルスクリプトを実行し、必要に応じて編集の上リポジトリに対してPull Requestを作成しインフラ担当者のレビューを経てマージ時に実行されるCodeBuildのビルド内でビルドプロジェクトが作成されるようなワークフローを思い描いていましたが、現状ではビルドプロジェクトの作成依頼に対してインフラ担当者が上記のシェルスクリプトを実行しファイル作成後にTerraformを適用するというような運用がされています。

運用のさらなる改善の余地はありますが、こういったシェルスクリプトを用意することで移行時のビルドプロジェクトの準備についてもスムーズに進めることができましたし、現在においても新規プロダクトに対するビルドプロジェクトの設定もスピーディーに対応することができています。

まとめ

N予備校におけるCodeBuildの導入の経緯や実際の利用例についてご紹介しました。

特に導入後にサポートが開始されたバッチビルドについては簡単に並列実行が実現できるということもあり、試行錯誤しながらではありましたがビルドの高速化やbuildspecの単純化に取り組んでいます。

コンピューティングタイプの最適化やビルドプロジェクトの整備における課題もありますが、現状ではフルマネージドによるメンテナンスからの解放やビルド開始まで待たなくて良いといった運用面や導入前に感じていた課題も解決できました。

一方でDockerのレイヤーキャッシュの挙動が不可解な点やテストレポートがうまく活用できていないなど、使い続けることで見えてきた課題もあったりします。

今後も継続的にCI環境の改善を行っていき開発者体験向上させつつ、N予備校を含むサービスの品質向上を目指していければと思います。

We are hiring!

株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。 カジュアル面談も行っています。 お気軽にご連絡ください!

カジュアル面談応募フォームはこちら

www.nnn.ed.nico

開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com


  1. ユーザーガイドのドキュメント履歴によると2018年1月25日にドキュメントが更新されている。
  2. 感謝 https://github.com/mercari/tfnotify/pull/23