検索
連載

いまさら聞けない「CI/CD」の意義――GitHubとGitHub ActionsでCI/CDを試してみようGMOペパボに学ぶ「CI/CD」活用術(1)(2/2 ページ)

GMOペパボにおけるCI/CD活用事例を紹介する本連載。第1回は組織でCI/CDを導入する目的と意義を整理し、GitHub/GitHub Actionsを利用してCI/CDを実践する方法を紹介します。

Share
Tweet
LINE
Hatena
前のページへ |       

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 ./...
ci.ymlのソースコード

 そして、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で確認できます。

GitHub Actionsの管理画面
GitHub 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
}
main.goのソースコード

 続いて、こちらがテストコードです。

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_test.goのソースコード

 早速、main.goとmain_test.goの2ファイルをローカルリポジトリに作成して、リモートリポジトリにコミットしましょう。

$ git add main.go main_test.go
$ git commit -m 'lsの機能とテストを追加'
$ git push origin main

 さて、CIの結果を確認してみます。残念ながら失敗してしまいました。

CIの実行結果
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.ymlのソースコード

 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コマンドを実行」すればリリースができるようになりました。

GitHub リポジトリのリリース画面
GitHub リポジトリのリリース画面

 リリース作業を自動化します。アプリケーションの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 }}
cd.ymlのソースコード

 そして、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環境を構築できました。

v0.1.1のリリース画面
v0.1.1のリリース画面

まとめ

 第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.

前のページへ |       
[an error occurred while processing this directive]
ページトップに戻る