見出し画像

CI/CDを漠然と理解していたPdMが、GitHub ActionsでCI/CD環境を構築してみた

概要

こんにちは。株式会社Mobility Technologiesで、法人向けタクシーソリューション『GO BUSINESS』のプロダクトマネージャー(PdM)を担当しております、Tannyです。

この記事では、実務でのプログラミング経験がないPdMが、CI/CD環境を構築してみた学習記録をまとめています。CI/CDという考え方はなんとなく理解しているけど、具体的にはどのような仕組みなのか、よくわかっていないという方におすすめの内容です。(技術的な内容はかなり省略しています。ご了承ください🙏)

きっかけ

CI/CDとの出会い

まずは今回の記事でCI/CDについて取り上げようと思った理由をご紹介しておきます。

私の以前の職場では、ウォーターフォール型でシステムを開発しており、新機能のリリースは以下のようなプロセス実施していました。この方法では一定の品質を担保できるものの、リリースには相応の工数がかかります。リリース中はサービスも停止する必要がありました。

1. 外部ベンダがリリース手順のバーンダウンチャートを準備する
     ↓
2. 外部ベンダからリリース手順の説明を受け、承認する
     ↓
3. 最大1時間程度、サービスを停止し、手順に従ってリリースする

以前の職場におけるリリース手順

対して、Mobility Technologiesでのリリース手順はどうだったか?私からリリースをお願いすると、開発者からは「プルリク」や「マージ」などの用語が聞こえてきて、気づいたらサクッとリリースが完了しています。しかもリリースは自動で行われていて、待っている間は別の作業もしているようです。これはすごい🤔

どうやらこれが、噂に聞くCI/CDと呼ばれるもののようです。これまで概念図レベルでは知っていたのですが、実際にどう動いているかは把握していませんでした。テキパキとリリースされていく様子にあまりにも感動したので、一体どんな技術が使われているのか気になりました🤔 というわけで、実際に自分でCI/CD環境を構築してみることにしました。

CI/CDとは

CI/CDとはContinuous Integration / Continuous Delivery(継続的な統合 / 継続的な配信)の略で、複数の開発者によるソースコードを継続的に統合し、その結果を継続的に配信するための手法・考え方を意味しています。この記事では、開発者が「ポチッと」するだけでソースコードの変更が自動的に取り込まれて、WEBへの配信まで完了する環境のことをCI/CD環境と呼ぶことにします。

CI/CDのイメージ(「エンジニアのためのCI/CD再入門」より引用)

このCI/CD環境を実際に構築してみて、どのような技術で実現されているのか、またこの環境を運用することがどのようなメリットを生むのかを調査していきます。

Webアプリを作る

CI/CDは、なんらかのシステムの更新作業を効率化するための技術です。なので、まずはCI/CD環境の適用対象となるWebアプリを作ります。

仕様を決める

今回は、ユーザーが入力した文字列を画面に表示するだけのシンプルなWebアプリを作ってみます。掲示板などでユーザー名を入力する部分をイメージしました。後の作業を考慮して、仕様を丁寧に書いておきます。

ユーザーは、テキストボックスに文字列(1文字~10文字)を入力する。「登録」ボタンを押すと、「ユーザー名」が画面上部に表示される。
  ・文字列が0文字の場合、ユーザー名は「名無し」を表示する。
  ・文字列が11文字以上の場合、ユーザー名は「ERROR!!」を表示する。
  ・それ以外の場合、文字列をユーザー名として表示する。

Webアプリの仕様 ver1

基本的にはユーザーが入力した文字列をユーザー名として表示し、例外がある場合は「名無し」「ERROR!!」などに置き換えて出力します。(後でテストコードを書くことを優先して、ユーザー目線では不便な作りになっています。)

Webアプリを実装する

今回は「React」でWebアプリを実装していきます。Reactを選んだのは、私の担当サービスであるGO BUSINESSがReactで実装されているからです(担当システムの勉強もできて一石二鳥ですし😁)

今回のアプリは、「React.js&Next.js超入門 第2版」のサンプルコードを参考にさせていただきました。

サンプルコードを少し変更して出来上がったのがこちら!

完成したWEBアプリ。テキストエリアに入力した文字列が表示される

文字列を入力するとそのまま出力されることの他に、0文字と11文字以上の時の出力が仕様通りであることも確認しました🙆🏻‍♂️  ひとまず基本のWebアプリは完成です。このソースコードは「GitHub」で管理していきます。

CIの準備|テストを書く

このWebアプリをもう変更しないのであれば、Webにアップロードして終了!ですが、我々が業務で通常扱うWebアプリは、ここから複数の開発者が手を加えて機能をどんどんアップデートしていきます。その時、元からあった機能も正しく動くことを保証するために、テストをやり直す必要があります(リグレッションテスト)。ただし毎回手作業でテストし直すのは地味に面倒ですし、やり忘れるリスクもあります。

そこで、「テストを実行してくれるプログラム」を書いておいて、ソースコードを改修するたびに、そのプログラムを実行して自動でテストするようにします。今回はJavasScriptのテストフレームワークである「Jest」を利用してテストコードを書いていきます。

テストコードの細かい仕組みは割愛します🙇‍♂️ まずは完成したテストコードをご覧ください。基本仕様の「入力した文字列をそのまま出力する」テストがこちらです。

test('1文字以上10文字以内の時はそのまま出力する', () => { //テストのタイトル
  render(<App />);  //①アプリを描画(表示)する

  fireEvent.change(screen.getByRole('textbox'), {
    target: { value: 'テストネーム' }, //②テキストボックスに「テストネームと入力する」
  });
  fireEvent.click(screen.getByText('Click')) //③「Click」ボタンを押す

  expect(screen.getByText(/テストネーム/)).toBeInTheDocument() 
  //④「テストネーム」という文字列が表示されているか検索する。
});

このように、ユーザーの操作と期待結果をそのまま記載しています。④で「テストネーム」が出力されていればテスト成功となります。

11文字以上を入力した時のテストも書いてみます。ここでは、11文字以上を入力したときに「ERROR!!」が出力されるという仕様をそのまま書いています。

test('11文字以上の時は「ERROR!!」を出力する', () => {
  render(<App />);

  fireEvent.change(screen.getByRole('textbox'), {
    target: { value: '12345123456' }, // 11文字以上を入力
  });
  fireEvent.click(screen.getByText('Click'))

  expect(screen.getByText(/ERROR!!/)).toBeInTheDocument()
    //「ERROR!!」という文字列が表示されているか検索する。
});

同じようにして、空白の時のテストも実装します。この状態で、「npm test」コマンドでテストを実行できます。3つのテストに成功したことが確認できました🙆‍♂️  テストを書くのに手間はかかりますが、何回もテストを実行する場合はこちらの方が早くて確実ですね。

テストの実行結果。3つのテストにPASSしたことと、所要時間が表示される

テストコードが書けたので、Webアプリの方のソースコードを改修してみましょう。このアプリでは「MAX_LENGTH」という定数で最大文字数(10文字)を定義しています。これを他の入力欄でも共通して使っていて、他のエンジニアがうっかり変更してしまったケース(例えば20文字など)を考えてみます。

const MAX_LENGTH = 11const MAX_LENGTH = 20

この状態でテストを実行した結果は以下の通りです。

11文字以上の時にエラーにならない場合のテスト結果。テストに失敗したことが表示される

テストがこけました!(一度言ってみたかったセリフ)元の仕様を満たさなくなったので、テストが通らなくなりました。どこで失敗したのかも分かりやすく表示してくれます。素晴らしい。これでエンジニアは、別の定数を使うなどの方法を検討するでしょう。

このように、ソースコードを修正するたびにテストを実行すれば、テストが書かれている範囲で、元の機能も正しく動いていることを保証できます。

CDの準備|デプロイする

Webアプリが完成したので、次はサーバーにデプロイ(配信)していきます。今回は「Amazon S3」にファイルをアップロードし、WEBアプリを公開します。

WebアプリをS3にアップロードして公開する

まずは「npm run build」コマンドでReactアプリをビルドします。ビルドが完了すると「build」ディレクトリが作成されます。この中身をS3にアップロードするだけでWebアプリを公開できます。非常に簡単ですね。

ただしS3へのアップロードの前には色々と設定が必要になるので、以下の記事を参考にして設定を行いました。

必要な設定が完了したら、あとはファイルのドラッグ&ドロップだけでWEBアプリの公開が完了します。

「build」ディレクトリの中身をアップロードする
Webブラウザからアクセスできるようになった🎉 

S3のコンソールを毎回開いてドラッグ&ドロップするのも少し面倒なので、コマンド入力(CLI)でアップロードできるようにします。以下のようなコマンドで実行できます。

npm run build && aws s3 sync build/ s3://[バケット名]

Webアプリの完成

これで、最初のバージョンのWEBアプリをデプロイするところまで完了しました。このWEBアプリをアップデートする場合は、以下のような手順を実行することになります。

1.  新機能とテストコードを実装する
2. テストが成功することを確認する(npm test)
3. ビルドしてS3にアップロードする(s3 sync build/ s3:// )

開発を1人で行う場合は、上記の手順を毎回実行する方法を続けても問題なさそうです。しかし複数人で同時に開発し、かつ頻繁にアップデートする場合、2.のテストを実施し忘れたり、3.の手順を間違えたりといった問題が起こりそうです。また、各自のPCにS3のアップロード設定を登録するのも手間がかかりそうです。

そこで、CI/CD環境を構築し、これらの手順を自動化します。

GitHub ActionsでCI/CD環境を構築する

GitHub Actions」は、GitHubが提供しているCI/CDサービスです。ソースコードの変更をマージしたタイミングなどを基点として、さまざまな処理を自動的に実行できます。

ここまではローカルPC上でコマンドを実行して、テストやデプロイを行いました。GitHub Actionsでは仮想マシン上で、これらのコマンドを自動的に実行できるようになります。開発者はソースコードを書くことに集中して、後工程のテストやデプロイはGitHub Actionsにおまかせというわけです。GitHub Actionsの概要に関しては以下の記事が参考になります。

CI/CDをサポートするサービスとしては他にも「CircleCI」や「Jenkins」といったツールがあるようです。GitHub Actionsは、Githubを利用していればアカウント発行などの追加の手続き不要で利用でき、しかも基本無料(!)ということで今回はこちらを採用しました。

最終的に構築するフローは以下のようになります。ここからは、構築と実行の手順で特に参考になった箇所をピックアップしてご紹介します。

CI/CD環境のイメージ図。これまでは手動で実行していたコマンドをGitHub Actionsで自動化する。


CI環境を構築する

まずはGitHub ActionsでCI環境を構築します。ソースコードに加えた変更を自動的にテストして、リポジトリに「統合」できるようにします。

GitHub Actionsの設定は、GitHubのリポジトリの「Actions」タブから行えます。ここでは、ワークフローのテンプレートを検索することができます。「react」で検索すると、テストに使えそうなワークフローが見つかりました。今回はこれをコピーして使ってみます。

「Actions」メニューからActionのテンプレートを検索する

ここではActionsを実行するタイミングやコマンドの内容を.yml形式で記述していきます。正直なところ、全部は理解できていませんが、読み解いてみた内容をコメントに入れました。

name: Test

on:
  push:         # (1-1) masterブランチにPushしたときに、このワークフローを実行
    branches: [ master ]
  pull_request: # (1-2) Pull Requestの作成時にこのワークフローを実行
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest #(2)ubuntuを利用する

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - run: npm run build --if-present # (3)Reactアプリをビルドする
    - run: npm test # (4)テストを実行する

(1-1)ではこのアクションを実行するタイミングを記述します。masterにpushした時、つまりメインのソースコードを更新するタイミングで実行します。※ (1-2)については後で説明します。

(2)では、アクションの実行にubuntuを利用することを指定します。GitHub Actionsが提供している仮想PCを一時的に間借りして、そこでコマンドを実行するイメージですね。(windowsやmacも選べるようです。)

その後、(3)でアプリをビルドして、(4)でテストを実行しています。先ほどまで自分のPC上で実行していたテストコマンドを、仮想PC上で順に実行します。

これでWebアプリ本体を更新した時に自動的にテストが実行されます。テストの実行状況は「Actions」タブからリアルタイムに確認できます。以下は、テストが成功した時のキャプチャです。

自動テストの実行結果。テストにPASSしていることがわかる

テストコードが実行されて、全てPASSしていますね。ちなみに、この画面は後からいつでも参照できるので、テスト実行のログを手作業で保存しておく必要がありません。これは便利!(もし別の場所に保存する必要がある場合、そういうコマンドを実行すればよさそうです)

これでCI環境が整いました。テストが通ったコードを頻繁にリポジトリに取り込むことができます。

CD環境を構築する

次にCD環境を構築しましょう。やることはシンプルで、S3にアップロードするためのコマンドをymlファイルに記載するだけです。今回は以下のサイトを参考にて作成しました。

完成したymlファイルはこんな感じです。

name: Deploy

on:
  push:
      branches:
        - master # (1)masterブランチにPushした時に、このワークフローを実行
jobs:
  build:
    runs-on: ubuntu-latest # (2)最新のUbuntu上でコマンドを実行する
    steps:
      - name: Checkout
        uses: actions/checkout@master

      - name: Install Dependencies
        run: npm install

      - name: Build
        run: npm run build  # (3)Reactアプリをビルドする

      - name: Deploy
        env: #(4) AWSにアクセスするためのキーを設定する
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: | #(5)S3のバケットにデプロイする 
          aws s3 cp --recursive --region ap-northeast-1 build s3://[bucket-name]

CIの時と同様に、(3)でコマンドの実行環境を準備した後、(4)でS3へのアップロードを行うだけです。これがmasterブランチへのpushのタイミングで実行されます。

これで、masterブランチの内容が更新されると同時に、Webにも配信される(=一般公開される)ようになります。なお、通常の開発現場では、開発環境で動作確認した上で、本番環境に反映させることが一般的です。ここでは省略して本番環境のみ用意しています。

ちなみに、外部からS3にアクセスするためにAWSのIDとアクセスキーが必要になりますが、(4)の箇所でymlファイルに直接書かずに別の場所(secrets)から参照するようにしています。このymlファイルは誰でも見れてしまうからですね。

CDの実行状況も「Actions」メニューから確認できます。配信担当者は、ファイルのアップロードが無事に終わるのを待つだけです。

自動デプロイの実行結果。ファイルがアップロードされるログが閲覧できる

これで、必要なタイミングでTestとDeployのアクションが自動的に実行されるようになりました!

WEBアプリを更新してみる

CI/CD環境が完成したので、WEBアプリを更新してみましょう。以下のような仕様を追加することにします。

入力中の文字数をテキストエリアの上に表示する

Webアプリの仕様 ver2

ソースコードを変更する場合は、masterブランチをいきなり修正せずに、別のブランチを切って修正することが一般的です。(参考資料:ブランチとは)今回は「develop」ブランチを切って追加機能を実装します。

機能の実装が終わったら、この仕様に対応するテストコードも追加して、テストが通ることも確認しておきます。

追加機能を実装し、テストコードを実行した状態

この変更をpushすると、Github上のソースコードは以下のような状態になっています。

masterブランチ:ver1.0 初期リリースの状態
developブランチ:ver2.0 文字数の表示機能を追加した状態

ここで、developブランチをmasterブランチにmergeすると、追加した機能が本番環境に反映されます。今回は、Githubのpull requestという機能を利用して、コードをレビューしてからmergeするようにしてみます。

Githubのメニューからpull requestを選び、「developブランチにcommitした内容をmasterブランチにmergeしたい」というリクエストを作ります。ここでは他のメンバーにリクエストのレビューを依頼することができます。(今回は自分でレビューすることにします。)

作成したPull request。変更内容の説明を記載して、他の開発者にレビューを依頼する

なお、pull requestを作成したタイミングで、先ほど用意したtest.ymlのアクションが実行され、テストにパスしたかどうかが表示されます。もし開発者がローカルでテストするのを忘れていたとしても、ここで必ずチェックされるので、バグを含んだソースコードが混入するのを防ぐことができます。

pull requestでは新旧ソースコードを比較したり、コメントを入れたりすることができます。今回は(自分で書いたコードですし)特にコメントすることもないので、そのままmergeを実行することにします。

テストが通っていることを確認して、「Marge pull request」を実行する

masterブランチにmergeされると、それを起点にdeploy.ymlのアクションが実行され、新機能を追加したWebアプリがS3にアップロードされます。このCD環境を構築したことによって、ワンクリックで配信まで完了することができました。

デプロイの結果。レビュアーがmargeを実行しただけで、配信まで自動的に実行される

CI/CD環境を構築したことで、テストとデプロイの作業を自動化することができました。ここで注目するべきは、開発者はソースコードを最新化するための、必要最小限の作業しか実施していないということです!コーディングの作業に注力することで、継続的に新機能を開発し、頻繁にリリースできるようになるということですね。

気づいたこと

ここまでは、CI/CD環境を実際に構築して運用する手順を紹介しました。実用には耐えないシンプルな物だとは思いますが、今後のPdM業務に取り組む上で、いろいろな気づきがありました。

テストコードを書くのは割と面倒

今回、テストコードというものを初めて書いてみたのですが、最初に思ったのは「面倒だなぁ…」ということです😅  今回はかなりシンプルなテストコードでしたが、どう実装するべきか調べる手間が結構かかりました。(もうちょっと効率的な書き方がありそうだとも思っています)しかも、テストコードを完璧に書いたとしても、何か新しい機能が増えるというわけではなく、ソースコードの正しさを証明できるだけです。テストコードを書くのは結構地味で時間のかかる作業でした…

とはいえ、テストコードを書くのは理想的なCI環境を維持するためには必要不可欠です。テストコードを後回しにしていると、将来的な不具合の発生につながり、新機能のリリースにも影響が出てしまうでしょう。もし開発者が「テスト書く時間がないな…」と呟いていたら、今後は開発期間に余裕を持たせることを検討しなきゃと思いました。

テストコードは万能ではない

テストコードを書いておけば、他のテストは不要か?というとそんなことはないです。FrontendとBackendなど、複数のシステムが連携している場合はテストコードで表すのは難しいですし、そもそもテストコードの実装が漏れていることもあり得ます。また、本番環境で実データを扱った時のみ発生する不具合などもあります。やはり、全てのシステムを結合して実施するEnd to Endのテストが必須であると改めて実感しました。

PdMとしては、CI/CD環境には頼りすぎず、本番リリース後に問題が発生していないことを、自分の目で確かめることを忘れないようにしたいと思います。

CI/CDのおかげでPdMが実力を発揮できる

自分が担当しているプロダクトにCI/CD環境が整備されていなかったらどうなるでしょうか?新機能のリリース作業に大きな工数がかかるため、なるべくまとめてリリースする、その結果リリースの頻度が落ちるといったことが起こるでしょう。(私が前職場で担当していたシステムでは、リリース頻度は多くて月に1回が限界でした)

CI/CD環境が整備されていれば、リリースに関する工数をほとんど気にする必要がなくなります。この環境に慣れるまでは、「こんなに頻繁にリリースして開発チームの負担にならないのかな?」と心配していました。しかし変更を加えたソースコードは、いつかはリリースしなければならないものです。であれば、細かく、早くリリースした方がメリットが大きいことに気づきました。今では、リリースの頻度を高めるにはどういう機能構成にすべきかを考慮するようになりました。

ただし、リリース後に不具合や機能不足などが見つかって手戻りになるのは明らかな無駄です。なので、ただ淡々とリリースするだけでなく、機能のテストや関係者への説明など、PdMとしてできる限りの事前準備は入念に行なっています。

CI/CD環境があることで、新機能を短期間・高頻度でリリースして検証を繰り返すという、PdMとしての実力を発揮できる環境が整うことになります。この環境を整備しているSREチーム、テストコードを書いてくれる開発チームに感謝しつつ、今後も新機能や改善をどんどんリリースしていきたいと思います!

おわりに

なんとなーく概念だけ理解していたCI/CDという考え方。今回はその環境を実際に構築してみることで、CI/CDの仕組みやそのメリット、構築における難しさなどをより深く理解することができました。

CI/CDは初期導入コストはかかりますが、実現できた時の効果は絶大です。今後はCI/CDの特性をよく理解して、そのメリットを十分に活かした開発ができるPdMになっていきたいと思います💪


この記事が気に入ったらサポートをしてみませんか?