MakefileとCI

さて、今日はMakefileとCIについて見ていきたいと思います。

昨日の時点でMakefileは以下のような状態になりました。

```makefile
SERVICE := qiita-advent-calendar-2019

GIT_HASH := $(shell git rev-parse HEAD)
LINKFLAGS := -X main.gitHash=$(GIT_HASH)

.PHONY: install
install:
	go get -v ./...

$(SERVICE):
	go build -ldflags '$(LINKFLAGS)' .	

.PHONY: clean
clean:
	@rm -f $(SERVICE)
```

## lint

リンターは [golangci/golangci-lint](https://github.com/golangci/golangci-lint) を使用します。
リンターのダウンロードとリンターを走らせるタスクをそれぞれ定義します。

```makefile
LINTER_EXE := golangci-lint
LINTER := $(GOPATH)/bin/$(LINTER_EXE)

$(LINTER):
	go get -u github.com/golangci/golangci-lint/cmd/golangci-lint

LINT_FLAGS :=--enable golint,unconvert,unparam,gofmt

.PHONY: lint
lint: $(LINTER)
	$(LINTER) run $(LINT_FLAGS)
```

実行してみましょう。

```sh
$ make lint
/<GOPATH>/bin/golangci-lint run --enable golint,unconvert,unparam,gofmt
```

何も表示されなければオッケーです。

## test

まだテストは書いていませんが、テストを走らせるタスクを定義しておきましょう。

```makefile
TEST_FLAGS := -v -cover -timeout 30s

.PHONY: test
test:
	go test $(TEST_FLAGS) ./...
```

```sh
$ make test
go test -v -cover -timeout 30s ./...
?       github.com/KentaKudo/qiita-advent-calendar-2019 [no test files]
```

## all

バイナリを作成する際に実行しておきたいタスクを簡単のためにallとしてまとめておきましょう。CIで実行する際に便利です。

```makefile
.PHONY: build
build: $(SERVICE)

.PHONY: all
all: install lint test clean build
```

```sh
$ make all
go get -v ./...
/<GOPATH>/bin/golangci-lint run --enable golint,unconvert,unparam,gofmt
go test -v -cover -timeout 30s ./...
?       github.com/KentaKudo/qiita-advent-calendar-2019 [no test files]
go build -ldflags '-X main.gitHash=61374325f9c4b3a6258bd480618ac295a6ebf55b' .
```

## docker-image

昨日Dockerのイメージをビルドしましたが、これもMakefileに書いておきましょう。

```makefile
DOCKER_REGISTRY=docker.io
DOCKER_REPOSITORY_NAMESPACE=kentakudo
DOCKER_REPOSITORY_IMAGE=$(SERVICE)
DOCKER_REPOSITORY=$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY_NAMESPACE)/$(DOCKER_REPOSITORY_IMAGE)
DOCKER_IMAGE_TAG=$(GIT_HASH)

.PHONY: docker-image
docker-image:
	docker build -t $(DOCKER_REPOSITORY):$(DOCKER_IMAGE_TAG) . \
	  --build-arg SERVICE=$(SERVICE)
```

コマンドをみやすくするため変数をいくつか定義しています。
またタグとしてgithashを使いました。

## docker-image-push

ビルドだけでなくできたイメージをレジストリにプッシュするタスクも追加しておきましょう。
CI上でも実行したいので、レジストリにログインするタスクも用意しておきます。

```makefile
.PHONY: docker-auth
docker-auth:
	@docker login -u $(DOCKER_ID) -p $(DOCKER_PASSWORD) $(DOCKER_REGISTRY)

.PHONY: docker-build
docker-build: docker-image docker-auth
	docker tag $(DOCKER_REPOSITORY):$(DOCKER_IMAGE_TAG) $(DOCKER_REPOSITORY):latest
	docker push $(DOCKER_REPOSITORY)
```

ログインに必要な情報はコマンドラインから与えます。

```sh
$ DOCKER_ID=kentakudo DOCKER_PASSWORD=<redacted> make docker-build
    ...
The push refers to repository [docker.io/kentakudo/qiita-advent-calendar-2019]
ddfebc023a12: Pushed 
1b7f5ec5366c: Pushed 
f1b5933fe4b5: Layer already exists 
084b0a196fb9b3b96bb7b5b638b0087cfc4b2516: digest: sha256:1dd6b3aca580cd2cfa28d3ead40863d56494d4294275e9dafa866cbcf1e98592 size: 949
ddfebc023a12: Layer already exists 
1b7f5ec5366c: Layer already exists 
f1b5933fe4b5: Layer already exists 
latest: digest: sha256:1dd6b3aca580cd2cfa28d3ead40863d56494d4294275e9dafa866cbcf1e98592 size: 949
```

## CI

ここまでで必要なmakeタスクは揃いました。CIのおおまかな流れは以下の通りです。

- master以外のブランチ：`make all`を実行し、テストとリントが通過することをチェック。
- masterブランチ：再び`make all`を実行後、`make docker-image-push`でコンテナイメージの作成とレジストリへのプッシュ。

今回はCircleCIで動作を確認してみましょう。

```yaml
version: 2
jobs:
  build:
    working_directory: ~/qiita-advent-calendar-2019
    docker:
    - image: circleci/golang:1
    steps:
    - checkout
    - run: make all

  deploy:
    working_directory: ~/qiita-advent-calendar-2019
    docker:
    - image: circleci/golang:1
    steps:
    - checkout
    - run: make all
    - setup_remote_docker:
    - run: make docker-build

workflows:
  version: 2
  development:
    jobs:
    - build:
        filters:
          branches:
            ignore: master
  deployment:
    jobs:
    - deploy:
        filters:
          branches:
            only: master
```

実行時に必要な変数（Dockerレジストリへのログイン情報）をコンソール上で設定しておいてください。

## おまけ

masterブランチではさらにもう１つ、`make kubernetes-push`というタスクを実行します。これは作成したイメージをもとに開発環境のk8sクラスタ上のアプリケーションに対してパッチHTTPリクエストを投げるタスクです。PRのマージ時に自動的に開発環境へのデプロイもしています。

今回はminikubeを使用しているため実行はしませんが以下のような雰囲気です。

```makefile
K8S_NAMESPACE=qiita
K8S_DEPLOYMENT_NAME=$(SERVICE)
K8S_CONTAINER_NAME=$(SERVICE)
K8S_URL=https://<dev env>/apis/apps/v1/namespaces/$(K8S_NAMESPACE)/deployments/$(K8S_DEPLOYMENT_NAME)
K8S_PAYLOAD={"spec":{"template":{"spec":{"containers":[{"name":"$(K8S_CONTAINER_NAME)","image":"$(DOCKER_REPOSITORY):$(DOCKER_IMAGE_TAG)"}]}}}}

.PHONY: kubernetes-push
kubernetes-push:
	test "$(shell curl -o /dev/null -w '%{http_code}' -s -X PATCH -k -d '$(K8S_PAYLOAD)' -H 'Content-Type: application/strategic-merge-patch+json' -H 'Authorization: Bearer $(K8S_AUTH_TOKEN)' '$(K8S_URL)')" -eq "200"
```

本番環境へのデプロイの自動化はまた違った方法をとっていますがそれに関してはまた後日触れられたらなと思います。
今日はこんなところで。

---

明日はヘルスチェックのエンドポイントを実装していきたいと思います。