こんにちは!技術部プラットフォームグループの@h0mirun_deux, @ryuichi_1208, @rsym1290 です。 今回は、社内で取り組んでいるPuppetのCI改善についてお話したいと思います。
ペパボにおけるインフラの構成管理とCIについて
弊社では様々なサービスを提供しており、その殆どのサービスでPuppetを用いてインフラの構成管理をしています。 また、GitHub Actionsを用いてCIを動かしており、リモートリポジトリへのpushのタイミングでの各ロールのCIが実行されます。 本稿では、カラーミーショップと30days Albumで取り組んだCI改善について掘り下げていきたいと思います。
従来のCI
カラーミーショップや30days AlbumではPuppetを用いて各ロールのミドルウェアの構成管理をしています。 なにか変更をする場合は、Pull Requestを出して変更対象となるロールのCIを実行してテストを通す必要があります。 通常CIを実行するときは、そのリポジトリで管理しているコード全体を対象にテストすることが一般的かと思います。 ですが、ペパボではPuppet関連のリポジトリのテストは変更を加えたロールに絞ってテストを実行できるようにしています。 これは一部の共通モジュールの利用を除き、ロールごとにマニフェストを分離して疎結合にしているためです。
テストはServerspecおよびGitHub Actionsを組み合わせて実現しています。 Pull Requestにcommitがpushされるとテストが自動で開始され、テストの実行結果をSlackに通知しています。
この運用にはいくつか課題がありました。
課題
課題1. 月日の経過とともに特定のロールのCIが通らなくなる場合がある
インフラのCIは時に「何もしていないのに壊れた」という状態が起きます。 原因は「マニフェストで指定されている最新版パッケージにバグが入っていた」や「パッケージの取得元の閉鎖」など様々です。 この時に困るのは「何かやりたい作業が別にあり修正を加えた」時に先ほどあげたCIが「何もしていないのに壊れた」状態だと加えた修正が原因なのかの判別ができません。 またこれが急ぎの作業の場合になぜCIが失敗したのかを調査するには時間がかかってしまいます。
では変更を加えるたびに全ロールのCIを実行すればよいかというとそうは行きません。 カラーミーショップは約40ロール、30days Albumは約20ロールあります。 1ロールあたりのCIの実行には最低でも5分はかかり、ロールによっては10分以上かかることもあります。 変更を加えるたびに2時間以上かけて全ロールのCIを動かすのはあまりにも非効率です。
CIを並列に実行することも解決策に思えますがここにも課題があります。 ペパボでは数多くのサービスを運用しており、1つのサービスをとっても複数のリポジトリでアプリケーションやインフラに関するCI環境があります。 また、ペパボで用いているself-hosted runnerは、OpenStackで構築された弊社のプライベートクラウド環境構築で運用しています。 サーバのリソースが有限なため、PuppetのCIを都度一斉に並列実行するとサーバのリソースが枯渇して他のリポジトリでのGitHub Actionsの利用に影響が出てしまいます。 これはCIに限らず、これまで本テックブログで取り上げたGitHub Actionsの活用事例に影響がでる可能性も考えられます。
課題2. CIの実行対象となるロールを手動で指定する必要がある
CIを実行するには構成変更を行いたい修正をPRとして出し、実行したいロール名のラベルを貼ることでそのロールのCIが実行されます。 ラベル貼りは手動で行っていました。
課題1でも述べたとおり各サービス複数ロールあるため、もし複数ロールに影響する変更を加える場合は該当ロール分のラベルを全て付与する必要がありました。 これにより、ラベルの付与が漏れたことでCIの実行漏れが発生してしまうことがありました。 例えば「www」と「app」というロールがあったとしてそれぞれのマニフェストを修正したのにCIは「www」しか行っていないケースです。数ロール程度の修正ならCIの実施漏れに気づくことが可能ですが多数になるとそれも厳しくなります。
課題3. bundle installやlibrarian-puppet installを都度実行することの非効率さ
Serverspecなど様々なgemを管理するためのGemfile、様々なPuppetモジュールを管理するためのPuppetfileも構成管理の対象となります。 これらに記載されたものはそれぞれbundle install、librarian-puppet installで導入することができます。 この2つの実行には1回あたり3分弱かかります。 GemfileやPuppetfileに変更がない場合でも毎度3分弱かかるため、ここを改善できると1CIあたりの時間短縮に大きく貢献できる可能性がありました。
解決策
課題1.の解決策:GitHub Actionsのcronを用いた日次CIの導入
GitHub Actionsではcron構文を使用したワークフローのスケジュール設定ができます。 この機能を活用して、勤務時間外に日次で全ロールのCIを実行するような運用を導入しました。
カラーミーショップの場合、平日0:00から各ロールを順次実行しています。 カラーミーショップはロール数が多いので全部のCIが完了するまでに3時間近くかかります。そのことから、日中帯にワーカー専有をしないように仕事をしている人の少ない0時からの実行としています。
30days Albumの場合、平日8:00から各ロールのCIを順次実行しています。 さらにmax-parallelを利用してCIを2並列に実行しています。 弊社では10時頃に勤務開始するエンジニアが多く、その頃にはCIがすべて実行完了しているため、勤務開始と同時にCIが通らないロールの有無を確認することができます。
以下は日次で実行する場合のyamlとslack通知の例です。1
通知例のようにCIが通らなくなるとfailure
として通知されます。
failure
になっている原因は通知に含まれているGitHub Actions URLより確認することができます。
これにより、CIが通らなくなる原因を素早く特定して修正することで、全ロールのCIを健全な状態に保つことができます。
name: Scheduled CI
on:
schedule:
- cron: '00 23 * * 0,1,2,3,4' # GMTで記載しており、JST 8:00に実行されます
jobs:
build:
runs-on: <runner>
container:
image: <CI image>
env:
LANG: C.UTF-8
BUNDLE_APP_CONFIG: .bundle
strategy:
matrix:
role: [role1, role2, role3, ...] # ここに全ロール名を記載します
max-parallel: 2
fail-fast: false
課題2.の解決策:Pull Request Labelerの導入
Pull Request Labelerとは、Pull Requestでの変更点に応じて自動でラベルを付与してくれる機能です。 この機能を利用してPull Requestにロール名のラベルが自動で付与されるようにしました。 ロール名のラベルが付与されるとGitHub Actionsがそれを検知して対象ロールのCIを実行します。 対象ロールが複数ある場合は、その分だけラベルが付与されて各ロールごとのCIが実行されます。
yamlの例とラベル付与の例
yamlの例とラベルが付与される例は以下のとおりです。
ここでは database1
というロールとdatabase2
というロールそれぞれに変更を加える場合のラベル付与の例を示します。1
CI用のyamlに加え、Pull Request Labeler用のyamlを2つ用意します。 jobを定義するyamlと、どのファイルに変更があったときにどのラベルを付与するかを定義するyamlです。
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
runs-on: normal
container:
image: path/to/images/ubuntu:20.04 # 弊社のGitHub Enterprise環境にあるubuntu20.04イメージを使用しています
steps:
- uses: actions/labeler@v3
with:
repo-token: "${{ secrets.TOKEN }}"
sync-labels: ''
database1: # ここに列挙したファイルに変更があると"database1"というラベルが付与されます
- roles/database1/**/*
- spec/database1/**/*
- hieradata/roles/database1.yaml
database2:
- roles/database2/**/*
- spec/database2/**/*
- hieradata/roles/database2.yaml
...
CI用のyamlでラベルの付与をCIのトリガーとして定義し、さらにgithub.event.pull_request.labels.*.name
で付与されたラベル名を取得することで、ラベル名に応じてCIを実行できるようになります。
name: CI
on:
pull_request:
types: [labeled, synchronize] # ラベルの付与をトリガーとしてCIを実行します
jobs:
build:
if: github.event.pull_request.labels[0] != null
runs-on: <runner>
container:
image: <CI image>
env:
LANG: C.UTF-8
BUNDLE_APP_CONFIG: .bundle
strategy:
matrix:
role: ${{ github.event.pull_request.labels.*.name }} # CI実行対象のロール名を取り出します
fail-fast: false
steps:
...
database1
用のcommitをpushしたあとにdatabase2
用のcommitをpushすると、画像のようにコミットに応じて自動でラベルが付与されます。
課題3.の解決策:pepacacheの導入
GitHub Actionsには再利用可能なファイルをキャッシュするactions/cacheというものがあります。 これを弊社のGitHub Enterprise環境で利用できるようにしたものがpepacacheです。 actions/cacheはGitHub Enterprise環境をサポートしていないため、弊社用にpepacacheが開発された経緯があります。 この機能を利用してbundle installによってインストールされるgemのライブラリ群、librarian-puppet installによってインストールされるPuppetのモジュール群をキャッシュするようにしました。
pepacacheの導入により、1回のCIあたりカラーミーショップは4分程度、30days Albumは2分40秒程度時間を短縮することができました。 この改善により、日頃の変更に加え先述したcronでのCI実行においても高速化に貢献しました。 以下は30days Albumで時間短縮した例です。
cacheなしの場合:2分45秒
cacheありの場合:3秒
まとめ
カラーミーショップと30days Albumで取り組んでいるPuppetのCI改善について紹介しました。 本稿では2つのサービスに焦点を当てましたが、あるサービスでの改善活動を他サービスへ横断して展開することで、ペパボの全サービスの品質を向上させています。 プラットフォームグループでは、今回紹介したCI改善だけでなくKubernetesの運用やSRE活動などを通して各事業部の成長を支えています。
今回は以上です。 最後までお読みいただきありがとうございました!