継続的インテグレーション (CI) (英語版) のことを何一つ知らず、ソフトウェア開発ライフサイクルに どうして CI が必要なのか (英語版) 分からない、と仮定しましょう。
いま、あるプロジェクトで作業をしていて、そこには2つのテキストファイルから成るコードがあるものとします。さらに、これらの2つのファイルには「Hello world」というフレーズが含まれている必要がある、という点に注意してください。
このフレーズが含まれていなければ、開発チームがその月のお給料を受け取れないことになるかもしれないくらい、重要なポイントです。
そこで、責任感のあるソフトウェアデベロッパーが、顧客にコードを納品する前に実行する、短いスクリプトを書きました。
以下のような非常に洗練されたコードです。
cat file1.txt file2.txt | grep -q "Hello world"
ここでの懸念事項はチームには10名のデベロッパーがいて、人的要因がコードの品質に大きな影響を及ぼす可能性があるという点です。
1週間前、新しくチームに入ったメンバーがこのスクリプトを実行し忘れ、3件のクライアントに機能しないビルドが納品されるという事態が発生しました。この事態の解決にあたり、幸いにもコードは既にGitLab にあり、ビルトインの CI があることが分かりました。さらに、あるカンファレンスで、テスト実行にCIを使うのが一般的ということを耳にしていました。
CI 内で最初のテストを実行する
ドキュメントによると、CI の実行に必要なのは .gitlab-ci.yml
ファイル内に 以下の2 行のコードを追加することだけでした。
test:
script: cat file1.txt file2.txt | grep -q 'Hello world'
コミットして...無事にビルドが成功しました。
では、2 つ目のファイルの「world」という文言を「Africa」に置き換え、何が起こるか確認してみましょう。
ビルドは予想どおり失敗します。 ここで、自動化テストが完成しました。GitLab CI は、DevOps 環境内でソースコードのリポジトリに新しいコードをプッシュするたびにこのテストスクリプトを実行します。
注: 上記例では、file1.txt と file2.txt がGitLabランナーを実行するホストに存在すると仮定しています。
この例を実際に GitLabで実行するには、以下のようにファイルを作成するコードを実行した後、テストスクリプトを実行する必要があります。
test:
before_script:
- echo "Hello " > | tr -d "\n" | > file1.txt
- echo "world" > file2.txt
script: cat file1.txt file2.txt | grep -q 'Hello world'
なお、わかりやすくするために、この 2 つのファイルは既にホストに存在していると仮定し、以降の例では作成しないものとします。
CIビルド結果をダウンロード可能にする
次にすることは、顧客に納品するコードをパッケージ化することです。ソフトウェア開発プロセスのこの部分も自動化してしまいましょう。
まず、CI に別のジョブを定義する必要があります。このジョブは「package」という名前にしましょう。
test:
script: cat file1.txt file2.txt | grep -q 'Hello world'
package:
script: cat file1.txt file2.txt | gzip > package.gz
よって、今ここにはタブが 2 つあります。
しかし、新たに作成されるファイルがダウンロードできるようにビルドの「アーティファクト」であることを指定し忘れてしまいました。。修正するには、artifacts
セクションを追加します。
test:
script: cat file1.txt file2.txt | grep -q 'Hello world'
package:
script: cat file1.txt file2.txt | gzip > packaged.gz
artifacts:
paths:
- packaged.gz
修正した結果を確認すると、アーティファクトが作成されダウンロードできるようになっています。
しかし、ここで修正が必要な新たな問題があります。2 つのジョブは現在は並列実行されていますが、テストに失敗した場合、アプリケーションをパッケージ化しないように変更をする必要があります。
ジョブを順次実行する
そこで「package」ジョブは、テストが成功した場合のみ実行するものとします。stages
を指定し、ジョブの実行順序を定義しましょう。
stages:
- test
- package
test:
stage: test
script: cat file1.txt file2.txt | grep -q 'Hello world'
package:
stage: package
script: cat file1.txt file2.txt | gzip > packaged.gz
artifacts:
paths:
- packaged.gz
上記のようになりました。
ちなみに、コンパイル (我々のケースでは2つのファイルを連結することを意味します) には時間がかかるため、2 回も実行したくはありません。コンパイルは別のステップとして定義しましょう。
stages:
- compile
- test
- package
compile:
stage: compile
script: cat file1.txt file2.txt > compiled.txt
artifacts:
paths:
- compiled.txt
test:
stage: test
script: cat compiled.txt | grep -q 'Hello world'
package:
stage: package
script: cat compiled.txt | gzip > packaged.gz
artifacts:
paths:
- packaged.gz
それでは、アーティファクトを見てみましょう。
この「コンパイル」ファイルを常にダウンロード可能にする必要はないようです。一時的なアーティファクトとして「20 分」で保存期間切れとなるよう、expire_in
を設定します。
compile:
stage: compile
script: cat file1.txt file2.txt > compiled.txt
artifacts:
paths:
- compiled.txt
expire_in: 20 minutes
構成ファイルは見たところ問題なさそうです。
-
アプリケーションをコンパイル、テスト、パッケージ化するために、3 つの連続したステージを作成しました。
-
コンパイル済みアプリを次のステージに渡すと、コンパイルを 2回実行する必要がなくなります、(それにより実行が高速化されます)。
-
パッケージ化されたアプリケーションは、今後も使用できるようビルドアーティファクトとして保管します。
どのDockerイメージを使用するのか学ぶ
ここまでは順調です。しかし、CIビルドにはまだ時間がかかります。ログを見てみましょう。
ここに注目してください。Ruby 3.1とあります。
なぜ Ruby が必要なのかといえば、GitLab.com が ビルド実行 (英語版) に Docker イメージを使用しており、デフォルトで (英語版) ruby:3.1
イメージを使用するからです。間違いなく、このイメージには必要のないパッケージが多数含まれています。Google で検索して調べたところ、alpine
というイメージがあり、ほとんど空の Linux イメージであることが分かりました。
それでは、.gitlab-ci.yml
に image: alpine
を追加して、このイメージを使用したいと明示的に指定しましょう。
うまくいきました。パイプラインの実行が3 分ほど短縮できたようです。
パブリックイメージはたくさんあるようです。
それにより、我々の技術スタックに適したものを選ぶことができます。不必要なソフトウェアが含まれていないイメージを指定することで、ダウンロード時間が最短で済みます。
複雑なシナリオに対応する
さて、ここで新しいクライアントが、アプリを .gz
ではなく、.iso
イメージとしてパッケージ化してほしい、と希望しているとします。CI がすべての作業を行なってくれるため、コードにはジョブを 1 つ追加するだけです。ISO イメージは mkisofs コマンドを使用して作成できます。構成ファイルは次のようになります。
image: alpine
stages:
- compile
- test
- package
# ... "compile" and "test" jobs are skipped here for the sake of compactness
pack-gz:
stage: package
script: cat compiled.txt | gzip > packaged.gz
artifacts:
paths:
- packaged.gz
pack-iso:
stage: package
script:
- mkisofs -o ./packaged.iso ./compiled.txt
artifacts:
paths:
- packaged.iso
ジョブ名は同じにする必要はありません。ジョブ名が同じだと、ソフトウェア開発プロセスの同じステージでジョブを並列実行できません。そのため、ジョブやステージの名前が同じになるのは、偶然のことだと考えてください。
さて、ビルドは失敗しました。
mkisofs
が alpine
イメージに含まれていないことが原因です。まずはこのパッケージをインストールする必要があります。
欠落しているソフトウェアやパッケージの対応
Alpine Linux Web サイト (英語版) によると、mkisofs
は xorriso
パッケージと cdrkit
パッケージの一部です。次のコマンドを実行することでパッケージをインストールできます。
echo "ipv6" >> /etc/modules # enable networking
apk update # update packages list
apk add xorriso # install package
CI ではこれらは他のコマンドと何ら変わりません。script
セクションで実行する必要があるコマンドの全リストは、このようになります。
script:
- echo "ipv6" >> /etc/modules
- apk update
- apk add xorriso
- mkisofs -o ./packaged.iso ./compiled.txt
構文的に正しくするため、パッケージのインストールに関連したコマンドは before_script
内に置きましょう。before_script
を構成の最上位レベルで使うと、そのコマンドがすべてのジョブの前に実行されることに留意してください。今回は特定のジョブの前で実行させます。
DAG(有向非巡回グラフ):より高速で柔軟なパイプラインのために
ステージを定義して、テストに合格した場合にのみパッケージジョブを実行するようにしました。後のステージに定義されているジョブに対し、いくつかのジョブのステージ順序を並び替えて先に実行させたい場合はどうすればいいでしょうか。場合によっては、従来のステージ順序がパイプライン全体の実行時間を遅くしてしまう可能性があります。
テストステージに、実行に時間のかかる負荷の高いテストがいくつか含まれており、それらのテストが必ずしもパッケージジョブに関連していないとします。この場合、テストの完了を待たずにパッケージジョブを開始できれば、より効率的になります。それにはDAG (有向非巡回グラフ) が役立ちます。特定のジョブのステージ順序を変えるためには、ジョブの依存関係 (通常のステージの順序をスキップするもの) を定義します。
GitLab には、ジョブ間の依存関係を作成する特殊キーワード needs
があります。これを使うことで、依存しているジョブが完了するとすぐにジョブを前倒しで実行できるようになります。
次の例では、テストジョブが完了するとすぐにパックジョブが実行を開始します。そのため、将来誰かがテストをテストステージに追加した場合に、新しいテストジョブの完了を待たずにパッケージジョブが実行を開始します。
pack-gz:
stage: package
script: cat compiled.txt | gzip > packaged.gz
needs: ["test"]
artifacts:
paths:
- packaged.gz
pack-iso:
stage: package
before_script:
- echo "ipv6" >> /etc/modules
- apk update
- apk add xorriso
script:
- mkisofs -o ./packaged.iso ./compiled.txt
needs: ["test"]
artifacts:
paths:
- packaged.iso
.gitlab-ci.yml
の最終バージョン:
image: alpine
stages:
- compile
- test
- package
compile:
stage: compile
before_script:
- echo "Hello " | tr -d "\n" > file1.txt
- echo "world" > file2.txt
script: cat file1.txt file2.txt > compiled.txt
artifacts:
paths:
- compiled.txt
expire_in: 20 minutes
test:
stage: test
script: cat compiled.txt | grep -q 'Hello world'
pack-gz:
stage: package
script: cat compiled.txt | gzip > packaged.gz
needs: ["test"]
artifacts:
paths:
- packaged.gz
pack-iso:
stage: package
before_script:
- echo "ipv6" >> /etc/modules
- apk update
- apk add xorriso
script:
- mkisofs -o ./packaged.iso ./compiled.txt
needs: ["test"]
artifacts:
paths:
- packaged.iso
パイプラインが作成できました!ステージは 3 つの連続したステージで、ジョブ pack-gz
と pack-iso
が、package
ステージ内で並列実行されています。
高度なパイプラインの構築
ここからは、高度なパイプラインを構築する方法を説明します。
CIパイプラインに自動テストを実装
DevOps において、ソフトウェア開発戦略の重要なルールは、素晴らしいユーザーエクスペリエンスを備えた優れたアプリを作成する、というものです。ここでは、CI パイプラインにいくつかのテストを追加し、プロセス全体の早い段階でバグを検出、修正しましょう。この方法なら、問題が大きくなる前や新しいプロジェクトに移る前に問題を修正できます。
GitLabにはさまざまな テスト 用にすぐ使えるテンプレートがあり、これらを使用することで作業が簡単になります。必要な手順は、CI の構成にテンプレートを追加するだけです。
この例では、アクセシビリティテスト (英語版) を追加します。
stages:
- accessibility
variables:
a11y_urls: "https://about.gitlab.com https://www.example.com"
include:
- template: "Verify/Accessibility.gitlab-ci.yml"
a11y_urls
変数をカスタム化し、Web ページの URL を挿入して、Pa11y と コード品質 のテストを行います。
include:
- template: Jobs/Code-Quality.gitlab-ci.yml
GitLab を使うと、マージリクエストのウィジェットエリア内でテストレポートを簡単に確認できます。コードレビュー、パイプラインステータス、テスト結果を一か所にまとめることで、あらゆることがよりスムーズに効率よくできるようになります。
マトリックスビルド
場合によっては、異なる構成、OS バージョン、プログラミング言語バージョンなどでアプリをテストする必要があります。そのような場合には、parallel:matrix (英語版) ビルドを使って、1 つのジョブ構成でさまざまな組み合わせでアプリケーションを並列にテストします。このブログでは、マトリックスキーワードを使用して、異なるバージョンの Python でコードのテストを行います。
python-req:
image: python:$VERSION
stage: lint
script:
- pip install -r requirements_dev.txt
- chmod +x ./build_cpp.sh
- ./build_cpp.sh
parallel:
matrix:
- VERSION: ['3.8', '3.9', '3.10', '3.11'] # https://hub.docker.com/_/python
パイプライン実行中、このジョブは 4 通りの方法で並列実行されます。それぞれ以下のように異なる Python イメージを使用します。
ユニットテスト
ユニットテストとは
ユニットテストとは、ソフトウェアの個々のコンポーネントや機能が期待通りに機能するを確認する、対象を絞った単体テストです。ユニットテストは、ソフトウェア開発プロセスの早い段階でバグを検出して修正し、コードのそれぞれの部分が、独立した状態でも正しく機能することを確認するために必須です。
例: 計算機アプリを開発しているとします。加算関数のユニットテストでは、2 + 2 が 4 になるかどうかを確認します。このテストに合格すれば、加算関数が正しく機能していることが確認されます。
ユニットテストのベストプラクティス
テストに失敗すると、パイプラインは失敗し、ユーザーに通知が送られます。デベロッパーはジョブのログ (通常何千行もある) を確認し、どこでテストに失敗したのかを特定し、修正します。このチェックには時間がかかり、効率がよくありません。
そこで、ユニットテストレポート (英語版) を使うようにジョブを構成することができます。GitLab はマージリクエストとパイプラインの詳細ページ上にレポートを表示することができるため、ログ全体を確認しなくてもエラーをより簡単に素早く特定できます。
JUnitテストレポート
以下はJUnit テストレポートの例です。
統合テストおよびエンドツーエンドテスト戦略
通常の開発ルーチンに加え、統合テストとエンドツーエンドテスト専用に指定したパイプラインをセットアップすることが非常に重要です。これらのテストでは、マイクロサービス (英語版)、UI テスト、それ他のコンポーネントを含んだ、コードの異なる部位の連携がスムーズかどうかをチェックします。
これらのテストは 毎晩 (英語版) 実行されます。テスト結果を自動的に指定のスラックチャンネルに送るよう (英語版) に設定することもできます。そうすることで、デベロッパーが翌日出社したときに迅速に問題を確認できます。こうした機能はすべて、問題を早期に特定して修正することを優先すべく設計されています。
テスト環境
テストの中には、アプリをしっかりテストするために、テスト環境を構築しなければならない場合があります。GitLab CI/CD を使用するとテスト環境のデプロイが自動化でき、時間を大幅に節約できます。本ブログは主に CI に着目したものとなっているため、詳細には触れません。アプリのデプロイとリリースについては、GitLab ドキュメントのこのセクション (英語版) を参照してください。
CI パイプラインへのセキュリティスキャンの実装
CI パイプラインにセキュリティスキャンを実装する方法は次のとおりです。
SASTおよびDASTインテグレーション
コードの安全性を守ることは非常に重要です。最新の変更に脆弱性がある場合は、その内容を直ちに把握したいと考えます。したがって、パイプラインにセキュリティスキャンを追加するのが最適な対応といえるでしょう。セキュリティスキャンは、コミットごとにコードをチェックし、リスクがあれば警告してくれます。CI パイプラインに静的アプリケーションセキュリティテスト (SAST (英語版)) や動的アプリケーションセキュリティテスト (DAST (英語版)) などの各種スキャンを追加する方法を説明する以下のナビゲーションをご確認ください。
ナビゲーションを開始するには、以下の画像をクリックしてください。
さらに、AI を活用すれば、脆弱性をさらに深く掘り下げ、修正方法の提案が得られます。詳しくは以下のナビゲーションをご確認ください。
ナビゲーションを開始するには、以下の画像をクリックしてください。
GitLab CI使い方まとめ
まだまだお伝えしたいことはありますが、ここで一度終わりにしましょう。説明に使用した例はすべて意図的に単純なものにしました。これは、慣れない技術スタックの説明が続くことで内容が頭に入らないといった状況を避け、GitLab CIのコンセプトを理解してもらうためです。では、ここまで学んできたことを次にまとめます。
- GitLab CI に作業をまかせるには、
.gitlab-ci.yml
内で 1 つ以上のジョブ (英語版)を定義する必要があります。 - ジョブには名前を付ける必要がありますが、その際は適切な名前を付けるようにしてください。
- それぞれのジョブには、指定のキーワード で定義された、 GitLab CI 用の一連のルールと指示が含まれます。
- ジョブは、順序どおりに、並列に、または DAG (英語版) を使って順不同で実行できます。
- ジョブ間ではファイルを渡してビルドアーティファクトに保存し、インターフェイスからダウンロードできるようにすることも可能です。
- CI パイプラインに テストとセキュリティスキャン (英語版) を追加して、開発中のアプリの品質とセキュリティを確保します。
本記事で使用した用語およびキーワードの説明と、関連するドキュメントのリンクを下表にまとめました。
キーワードの説明とドキュメント
キーワード/用語 | 説明 |
---|---|
.gitlab-ci.yml | プロジェクトのビルド方法の定義をすべて含むファイル |
script | 実行するシェルスクリプトを定義する |
before_script | (全) ジョブの前に実行するコマンドを定義するために使用する |
image | 使用するDocker イメージを定義する |
stages | パイプラインのステージを定義する (デフォルトは test) |
artifacts | ビルドアーティファクトのリストを定義する |
artifacts:expire_in | 指定時間後にアップロードしたアーティファクトを削除するために使用する |
needs | ジョブ間の依存関係を定義するときに使用し、ジョブを特定の順序で実行することを可能にする |
pipelines | いくつかのステージ (バッチ) で実行されるビルドのグループを指す |
※ドキュメントはすべて英語版です。
CI/CDについてさらに詳しく
- GitLab’s guide to CI/CD for beginners (GitLab のビギナーのための CI/CD ガイド (英語版))
- Get faster and more flexible pipelines with a Directed Acyclic Graph (有向非巡回グラフ(DAG)を使用して、より高速で柔軟なパイプラインを実現する(英語版))
- Decrease build time with custom Docker image (カスタム化した Docker イメージでビルド時間を短縮する (英語版))
- Introducing the GitLab CI/CD Catalog Beta (GitLab CI/CD カタログベータ版の紹介 (英語版))
よくある質問 (FAQ)
CIジョブを順次実行するか、または並列実行するか、どのように選択すればいいですか?
CI ジョブを順次実行するか、並列実行するかを選択する際は、ジョブの依存関係、リソースの利用可能性、実行時間、潜在的な干渉、テストスイートの構造、コスト面を考慮します。たとえば、デプロイジョブが始まるまでに終わらせなければならないビルドジョブがあったとします。その場合、これらのジョブを順次実行して、正しい実行順序を確かめます。一方、ユニットテストや統合テストなどのタスクは独立しており、それぞれの完了には依存しないため、並列実行できます。
GitLab CIの DAG(有向非巡回グラフ)とは何ですか?また、DAG はパイプラインの柔軟性をどのように向上させますか?
GitLab CI の有向非巡回グラフ (DAG) は、パイプラインステージの順序を並び替えます。DAG はジョブ間の依存関係を設定するため、前のステージにあるジョブが終わり次第、その後のステージのジョブが開始されます。これによりパイプライン全体の実行時間が短縮され、効率が向上し、一部のジョブは通常の順序より早く完了します。
GitLab の CI ジョブに最適なDockerイメージを選ぶ際に重要視しなければならないことは何ですか?
GitLab はジョブの実行の際に Docker イメージを使用します。デフォルトのイメージは ruby:3.1 です。ジョブの要件によって、最適なイメージを選ぶことがとても大切です。ジョブはまず指定された Docker イメージをダウンロードしますが、イメージに必要ではない追加パッケージが含まれていると、ダウンロードや実行に余分な時間がかかります。そのため、実行時に不必要な遅延が発生しないように、選択したイメージにはジョブに必要なパッケージだけが含まれていることを確認することが重要です。
次のステップ
ソフトウェア開発プラクティスで後れを取らないためにも、次のステップとしてCI/CDコンポーネントの標準化と再利用方法を理解することをお勧めします。GitLab CI/CD カタログ (英語版) もご確認ください。
継続的インテグレーションとデリバリー入門は、こちらをご覧ください。
監修:川瀬 洋平 @ykawase
(GitLab合同会社 カスタマーサクセス本部 シニアカスタマーサクセスマネージャー)