いまさら聞けない「CI/CD」の意義――GitHubとGitHub ActionsでCI/CDを試してみよう:GMOペパボに学ぶ「CI/CD」活用術(1)(2/2 ページ)
GMOペパボにおけるCI/CD活用事例を紹介する本連載。第1回は組織でCI/CDを導入する目的と意義を整理し、GitHub/GitHub Actionsを利用してCI/CDを実践する方法を紹介します。
GitHubとGitHub ActionでCI/CDを実践してみる
本項では、CI/CDの実践の例として小さなコマンドラインアプリケーションを作ってみます。プログラミング言語はGoを使用します。これは私が慣れているというのが一番の理由ですが、言語に最初からテスト実行のコマンド「go test」とテスティングパッケージ「testing」が組み込まれているというのも魅力です。筆者のGoの環境は以下のようになっています。
$ go version go version go1.16.5 darwin/amd64
共有リポジトリ環境にはGitHubを使用します。また、CIおよびCD環境にはGitHubに統合されているGitHub Actionsを使用します。
では、カレントディレクトリのファイルとディレクトリの一覧を表示するlsっぽいコマンドを作ってみましょう。名前は「最低限の機能を実装したls(Minimum ls)」ということでminilsとします。
1.共有リポジトリ環境の整備
それでは初めにminilsのGitリポジトリをGitHubに作成しましょう。リポジトリ名はminilsで作成します。私のGitHubアカウント名はk1LoWなので、リポジトリのURLはhttps://github.com/k1LoW/minilsとなります。アカウント名の部分は自身のアカウント名に置き換えてください。
これでGitHubにリポジトリができました(以下、リモートリポジトリと呼びます)。次に手元にGitリポジトリをチェックアウト(git clone)していきます(以下、ローカルリポジトリと呼びます)。
$ git clone git@github.com:k1LoW/minils.git Cloning into 'minils'... warning: You appear to have cloned an empty repository.
これで共有リポジトリ環境の整備は完了です。
2. CI環境の整備
続いて、ローカルリポジトリを確認します。
$ git status On branch main No commits yet nothing to commit (create/copy files and use "git add" to track)
何もコミットしていない空のリポジトリなので、最低限Goアプリケーションの体裁をとるためのコードをコミットしておきます。
$ go mod init github.com/k1LoW/minils go: creating new go.mod: module github.com/k1LoW/minils $ cat << EOL > main.go package main func main() {} EOL $ git add go.mod main.go $ git commit -m 'Initial commit'
この時点でビルトと(テストコードゼロの)テストの実行が可能になります。
$ go build -v ./... $ go test -v ./... ? github.com/k1LoW/minils [no test files]
それでは、CIの設定を追加していきます。
GitHub Actionsは、リポジトリ内の.github/workflowsというディレクトリに、ワークフローと呼ばれる自動化プロセスの設定をYAMLファイルに記述して設置し、リモートリポジトリにプッシュ(git push)するだけで設定完了となります。
今回作成するコマンドラインアプリケーションのCIの設定として、以下のようなYAMLファイルを「.github/workflows/ci.yml」として設置します。
name: CI on: push: # リモートリポジトリのコードがpushされた時にこのワークフローを実行する jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 # CI環境にリモートリポジトリのコードをチェックアウトする - name: Set up Go uses: actions/setup-go@v2 # CI環境にGoの環境をセットアップする with: go-version: 1.16 - name: Build # アプリケーションのビルドを実行する run: go build -v ./... - name: Test # アプリケーションのテストを実行する run: go test -v ./...
そして、YAMLファイルをコミットし、リモートリポジトリにプッシュします。
$ git add .github/workflows/ci.yml $ git commit -m'GitHub Actionsを利用してCI環境を整備' $ git push origin main
GitHub Actionsの稼働状況は、https://github.com/k1LoW/minils/actionsで確認できます。
テストコードはゼロですが、無事CI環境が動くようになりました。以降は、ローカルリポジトリの修正をプッシュするたびにビルドとテストが実行され、自動で「ビルドできること」「意図した通りに動くこと」が検証されるようになります。
さらに、以下のような設定を加えることで、コードの統合前の検証をさらに厳密に運用できるようになります。
- メインブランチには直接pushをせずブランチを作成した上でリモートリポジトリにプッシュし、CIのワークフローが成功してからマージする運用にする
- そもそもGitHubの機能を使ってメインブランチへのプッシュを禁止し、CIのワークフローが成功しないとマージできないようにする
これでCI環境の整備は完了です。
3. ソフトウェアのコードとテストコードを書く
では、コマンドラインアプリケーションのminilsを開発します。開発した実際のコードを以下に示します。
package main import ( "fmt" "io" "io/fs" "os" ) type osFS struct{} func (fsys *osFS) Open(name string) (fs.File, error) { f, err := os.Open(name) if f == nil { return nil, err } return f, err } func (fsys *osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) } func main() { if err := run(); err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { fsys := new(osFS) wd, err := os.Getwd() if err != nil { return err } return listDir(fsys, wd, os.Stdout) } func listDir(fsys fs.ReadDirFS, dir string, out io.Writer) error { entries, err := fsys.ReadDir(dir) if err != nil { return err } for _, e := range entries { _, err := out.Write([]byte(e.Name())) if err != nil { return err } } return nil }
続いて、こちらがテストコードです。
package main import ( "bytes" "io/fs" "testing" "testing/fstest" ) func TestListDir(t *testing.T) { tests := []struct { dir string want string }{ {"path", "to\n"}, {"path/to", "a.txt\nb.txt\ngo\n"}, {"path/to/go", "c.txt\n"}, } for _, tt := range tests { buf := new(bytes.Buffer) if err := listDir(testFS(), tt.dir, buf); err != nil { t.Fatal(err) } got := buf.String() if got != tt.want { t.Errorf("got %s\nwant %s", got, tt.want) } } } func testFS() fstest.MapFS { fsys := fstest.MapFS{ "path": &fstest.MapFile{Mode: fs.ModeDir}, "path/to": &fstest.MapFile{Mode: fs.ModeDir}, "path/to/a.txt": &fstest.MapFile{Data: []byte("test\n")}, "path/to/b.txt": &fstest.MapFile{Data: []byte("test\n")}, "path/to/go": &fstest.MapFile{Mode: fs.ModeDir}, "path/to/go/c.txt": &fstest.MapFile{Data: []byte("test\n")}, } return fsys }
早速、main.goとmain_test.goの2ファイルをローカルリポジトリに作成して、リモートリポジトリにコミットしましょう。
$ git add main.go main_test.go $ git commit -m 'lsの機能とテストを追加' $ git push origin main
さて、CIの結果を確認してみます。残念ながら失敗してしまいました。
ファイル/ディレクトリ名の出力時に改行が必要だと分かりました。以下のように修正します。
$ git diff diff --git a/main.go b/main.go index fbf3237..4b8c13a 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,7 @@ func listDir(fsys fs.ReadDirFS, dir string, out io.Writer) error { return err } for _, e := range entries { - _, err := out.Write([]byte(e.Name())) + _, err := out.Write([]byte(fmt.Sprintf("%s\n", e.Name()))) if err != nil { return err } $ git add main.go $ git commit -m 'ファイル/ディレクトリ名の出力時に改行が必要' $ git push origin main
無事CIでテストが成功しました。
ローカルリポジトリでビルドして動かしてみると以下のようにファイル/ディレクトリ一覧が表示されます。
$ go build $ ./minils .git .github go.mod main.go main_test.go minils
これでminilsの最初のバージョンが完成しました。
4. リリースやデプロイのコードを書く
作成したminilsコマンドを、ビルドしたものを他の人がダウンロードできるようにします。GitHubではGitのタグごとにリリースを作成でき、そのリリースにビルドしたバイナリファイルをリンクできます。リリースの前準備として、他の人が安心して利用できるようにminilsコマンドが依存しているライブラリのライセンス一覧(CREDITS)や、minils自体のライセンス(LICENSE)をリポジトリに追加します。
依存ライブラリのライセンス一覧を取得する方法は幾つかありますが、今回はgocreditsを使用します。
$ go install github.com/Songmu/gocredits/cmd/gocredits@latest $ gocredits -w . $ git add CREDITS LICENSE # LICENSEファイルはご自身で作成してください $ git commit -m '依存ライブラリのライセンス一覧とminilsのライセンスを追加'
次にリリースを実施するコードを書きます。といっても、今回はリリースにはGoReleaserを使用するため、書くコードは最小限になります。GoReleaserはGoで作成されたアプリケーションのさまざまなタイプのリリースを容易にするリリース自動化ツールです。
以下のようにGoReleaserをインストールし、GoReleaser用の設定ファイル(.goreleaser.yml)を設置します。
$ go install github.com/goreleaser/goreleaser@v0.171.0 $ goreleaser -v goreleaser version dev module version: v0.171.0, checksum: h1:aM9boGNCuwst7uHr3QxsxAwdjHEQvKu84GxMJhVtofk=
before: hooks: # ビルド前に go mod tidy を実行する - go mod tidy builds: # 環境変数 - env: - CGO_ENABLED=0 # クロスコンパイスをするOS goos: - linux - windows - darwin archives: # バイナリ以外に同梱するファイル - files: - CREDITS - LICENSE checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:'
GoReleaser用設定ファイルもコミットします。
$ git add .goreleaser.yml $ git commit -m 'GoReleaser用設定ファイルを追加'
GoReleaserはリモートリポジトリの最新のタグを使ってリリースを作成します。現在のminilsをv0.1.0としてリリースしてみましょう。GoReleaseはリリース時にGitにステージングをしていないファイルが存在するとリリースが失敗してしまうため、ステージングする必要がないファイルは.gitignoreに記述しましょう。
$ cat << EOL > .gitignore minils dist/ EOL $ git add .gitignore $ git commit -m '生成されるバイナリとGoReleaserのdist/ディレクトリを除外' $ git status # ステージングしていないファイルがないことを確認 On branch main nothing to commit, working tree clean
リリースするためにGitHubのPersonal access tokenが必要なので環境変数「GITHUB_TOKEN」にセットしておきます。必要な権限はrepoになります。
$ export GITHUB_TOKEN="あなたのPersonal access token"
v0.1.0のタグを作成しリモートリポジトリにプッシュします。
$ git tag v0.1.0 $ git push origin main --tag Enumerating objects: 4, done. Counting objects: 100% (4/4), done. Delta compression using up to 16 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 353 bytes | 353.00 KiB/s, done. Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (1/1), completed with 1 local object. To github.com:k1LoW/minils.git 5fe1fa2..c7e02a5 main -> main * [new tag] v0.1.0 -> v0.1.0
$ goreleaser release --rm-dist ・ releasing... ・ loading config file file=.goreleaser.yml ・ loading environment variables ・ getting and validating git state ・ releasing v0.1.0, commit c7e02a59712a23c46ec1ac98a09f8417048c3967 ・ parsing tag ・ running before hooks ・ running go mod tidy [・・・] ・ release succeeded after 7.61s
これでリリースができました。GitHubリポジトリの「Releases」欄にもv0.1.0のリリースが表示されています。以降も、手動で「タグを打ってgoreleaserコマンドを実行」すればリリースができるようになりました。
リリース作業を自動化します。アプリケーションのCDの設定として、以下のようなYAMLファイルを「.github/workflows/cd.yml」として設置します。
name: CD on: push: tags: - 'v*.*.*' jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.16 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
そして、YAMLファイルをコミットし、リモートリポジトリにプッシュします。
$ git add .github/workflows/cd.yml $ git commit -m'GitHub Actionsを利用してCD環境を整備' $ git push origin main
これで、Gitタグをリモートリポジトリにプッシュするだけでリリース作業が実行されるようになりました。では、試しにv0.1.1タグを作成して、リリースを実施してみましょう。
$ git tag v0.1.1 $ git push origin v0.1.1 Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:k1LoW/minils.git * [new tag] v0.1.1 -> v0.1.1
無事v0.1.1のリリースが自動実行されました。これでCD環境の整備は完了です。コマンドラインアプリケーションminilsにCI/CD環境を構築できました。
まとめ
第1回では、CI/CDの概要と重要性を紹介しました。また、CI/CDを実践するためのステップをGitHubやGitHub Actionsを使ったCI/CDの導入を通して紹介しました。CI/CDの実践のために重要な要素は、実はCI/CD環境ではなく「テストコード」や「デプロイやリリースのためのコード」といったソフトウェアデリバリーのパフォーマンスを向上させるコードです。
CI/CDを実践できている現場は、「テストコード」や「デプロイやリリースのためのコード」の質を上げることにも継続的に注力し、ソフトウェアデリバリーのパフォーマンスを向上させています。さらには、CI/CD実践の範囲をソフトウェア開発周辺や、ソフトウェア開発以外にも広げることで、直接的または間接的に組織全体のパフォーマンスを向上させています。第2回以降では、私たちが所属するGMOペパボの事例を中心に、CI/CDの実践例を紹介します。
筆者紹介
小山 健一郎
GMOペパボ所属。少し実用的で小さなOSSを書くのが趣味。
Copyright © ITmedia, Inc. All Rights Reserved.