This is my life.

There are many like it, but this one is mine.

Spring Boot WebアプリケーションをCI上でスモークテストする

機能も作り込んだ。テストもオールグリーン。さあ張り切ってみんなの前でデモするぞ...

$ ./gradlew bootRun

...

エラー

ってこと、ありませんか?
僕はありました。

できるだけ実行コストをかけないように、ユニットテストではSpringに依存しないようにしています。
もちろん結合テストなどでは @SpringBootTest を利用していますがアプリケーション起動時のエラーは確認できません。

そんな中、以前 FlywayWebアプリケーションを起動しないで 実行する方法を調べていたことを思いました。

dev.classmethod.jp

※上記の記事時点では Spring Boot 1.5 なので、若干変わっています。

現在(2018/11/23時点)だと以下になります。

$ ./gradlew bootRun --args='--spring.main.web-application-type=none'

はい、あとはもうおわかりですね。
上記のコマンドをお使いの CI上で実行するだけです。

Webサーバーの立ち上げをしないで Spring Boot アプリケーションを起動することができます。
このとき、起動に失敗すれば 0 以外の Exit Code を返してくれるので、 CI では失敗として通知してくれます。

Flyway の DDL を修正や追加したときも、失敗したらCIコケるので便利でした。

一見目的が違うけど、「あー、あれ使ったらこれ達成できるなー。」って瞬間が好き。

Golang で CI を作っている話

前回の記事 で CI サーバーを作った話をしました。

ペライチ300行だったコードはコミットを重ね、少しずつ大きくなってきています。

github.com

リポジトリの名前も minimal-ci から duci に変えました。命名はお世話になっている先輩です。

f:id:duck8823:20180901004830g:plain

コンセプト

CIは非常に便利です。僕も業務や趣味で CircleCI や Travis CI 、 Jenkins を利用しています。
ですが、これらの設定は非常に複雑でかつドメイン特異的になります。
CIのバージョンアップや移行で疲弊したこともあるのではないでしょうか。

そこでこのCIのコンセプトは DSL is Unnecessary For CI にしました。

  • タスクはタスクランナーで定義しましょう
  • タスクの実行に必要な環境は Dockefile で定義しましょう
  • duci はコンテナ内でタスクを実行するだけです

僕の中で常に意識していることとして、開発中のフィードバックをできるだけ早くするというのがあります。
CIで失敗したときにCIでしか再実行・確認ができないのは辛いです。
同じ定義でローカルでも実行できるようにしています。

タスクはタスクランナーで定義しましょう

CIに特異的な設定をしないでジョブの定義をしましょう。
ローカルマシンでの開発中、多くの言語ではタスクランナーを利用すると思います。
Java、Kotlin では maven や gradle、JavaScript では npm や yarn がそれに当たります。
いずれのツールもサブコマンドで、予め定めたタスクを実行します。
Golangの場合はそれ自体のサブコマンドとして taskbuild が用意されています。

Makefile でもタスクの定義が可能ですね。
https://github.com/duck8823/duci/blob/master/Makefile

make test

このタスクを CI サーバーでも実行するようにすれば、
ローカルでもCIでも同じタスクを実行することができます。

タスクの実行に必要な環境は Dockerfile で定義しましょう

ローカル開発時とCIで同じタスクが実行できるようになりました。
しかし、このままではローカルとサーバーで実行環境が異なります。

コンテナを利用しましょう。
Dockerfile を利用してタスクの実行環境を定義することで、開発に必要な依存も管理することができます

FROM golang:1.11-alpine

RUN apk --update add --no-cache alpine-sdk

WORKDIR /go/src/github.com/duck8823/duci

ADD . .

ENV CC=gcc
ENV GO111MODULE=on
...

Dockerfileをみるだけで必要な言語のバージョン、サーバーの依存(alpine-sdk)や環境変数がわかります。
これをコミットすることで必要な依存を変更したときも、どのコミットからその依存が必要になったか追えるようになります。

duci はコンテナ内でタスクを実行するだけです

コンテナ内ではタスクを実行するだけです。では、どのようにしてタスクを振り分けるのか。
Dockerfile 内の ENTRYPOINT にタスクランナーのコマンド、 CMD にデフォルトのサブコマンドを定義します。

そうすることで docker run 時にコマンドを渡さなければデフォルトのサブコマンド、コマンドを渡すことで特定のサブコマンドを実行することができます。

例えば Dockerfile を以下のように定義します。

...
ENTRYPOINT ["go"]
CMD ["test", "./..."]

このとき、

docker run <IMAGE_NAME>

で コンテナ内では

go test ./...

が実行されます。

docker run <IMAGE_NAME> build

go build

が実行されます。

duci では GitHub の Webhook からジョブを実行しますが、
push の場合はデフォルトのサブコマンド、issue_comment の場合はコメントで指定したサブコマンドを実行するようにしました。

Dockerfile があれば、ローカルでもコンテナを起動して実行することができます。
CIで失敗したときにローカルですぐに試せるので、原因の解明がとても楽です。
CIの設定を試すために何度もコミット&プッシュする必要はありません

パッケージ構成

コード量も増えたので、パッケージ構成も変更しました。
いわゆる階層アーキテクチャやクリーンアーキテクチャのような構成ではありませんが、自分なりに各パッケージに意味をもたせています。

  • インフラストラクチャパッケージ
    • 更に下位のパッケージに分けています。
      それぞれのパッケージはアプリケーションに依存しないものを集めており、使い回ししやすくしています。
  • アプリケーションパッケージ
    • アプリケーションの設定などの他、ランナー=メインロジック( Gitクローン -> Docker Run -> Commit Status ) を実現しています。
  • データモデルパッケージ
    • アプリケーションパッケージとプレゼンテーションパッケージで利用する構造体を定義しています。
  • プレゼンテーションパッケージ
    • ルーティングとハンドラーを定義しています。
      ハンドラーは Webhooks をアプリケーションパッケージに渡す形に変換する役割を担っています。

自分で考えたパッケージ構成にしておくことで、判断基準が自分にあるので悩まないですみます。
現段階のパッケージ構成なので、ソースが大きくなったりしたらまた変わるでしょう。

CIをもうちょっと高機能にする

必要最低限な機能は 前回の記事 で紹介しています。

実用的にするには、前回のペライチスクリプトではいくつかの機能が足りていません。

  • 非同期的に実行する
  • 並列数を制御する
  • ジョブのタイムアウトを設定する

Webhooks -> Git Clone -> Archive Tar Package -> Docker Image Build -> Docker container create & run -> Create Commit Status
この一連の流れを同期実行していました。レスポンスはすべての処理が終わったあとに返します。
CIを実行するのにはそれなりに時間がかかるので、GitHubのWebhookでTimeoutと表示されてしまっていました。
そのため、各ジョブは非同期で実行できるようにしました。

非同期で実行する場合、サーバーのリソースは限られているのでジョブの並列数を意識する必要があります。
また、ジョブの最大並列数を定義した場合、各ジョブのタイムアウトを設定しなければキューに入れられたジョブがずっと実行されないままになる危険があります。

これらは比較的簡単に実現できました。そう、 goroutine ならね。

var sem = make(chan struct{}, runtime.NumCPU()) // 最大並列数=サーバーのコア数
go func ... {  // goroutine で非同期に実行できるようにする
    ...
    errs := make(chan error, 1)

    timeout, cancel := context.WithTimeout(context.Background(), 30 * time.Second) // タイムアウトの設定
    defer cancel()

    go func() {
        sem <- struct{}{} // 実行できる並列数を一つ減らす
        errs <- runner.Run(timeout) // メインの処理... 終了時に `errs` に渡す
        <-sem // 実行できる並列数を一つ戻す
    }()

    select { // タイムアウト or エラーが入るまで(メイン処理が終わるまで)
    case <-timeout.Done():
        // タイムアウトの処理
        return timeout.Err()
    case err := <-errs:
        if err != nil {
            // ジョブが失敗したときの処理
            return err
        }
        // ジョブが成功したときの処理
        return nil
    }
}()

非同期にするのは関数呼び出し時に go をつけるだけです。
タイムアウトcontext.WithTimeout で実現できます。メインの処理は更に goroutine とし、 selectタイムアウトあるいはメインの処理実行まで待ちます。
並列数は最大実行可能数を渡した channel を作成しておき、メイン処理の前後で増やす・減らす処理をするだけで実現が可能です。

テストの話

テストについては golang.tokyo #17 で発表させて頂きました。

www.slideshare.net

まとめ

ペライチで作ったCIサーバーを育てていて、context とか goroutine の扱いを意識することができました。
MobyというDockerクライアントが含まれるライブラリも存在し、非同期処理や並列数も結構簡単に設定できるので、GolangはCIづくりに適してそうですね。
みなさんも自分好みのCIを作りましょう。

ペライチ300行弱のコードで簡易CIサーバーを作った

皆さんはゴールデンウィークいかがお過ごしでしょうか。
僕は暇すぎたので雑にCIサーバーのようなものを作ることにしました。

github.com

CIサーバーを運用する際の悩みとして、サーバーの環境構築が挙げられます。
色々なプロジェクトでは言語や依存するサーバー環境が異なります。これを解決するのは非常に面倒です。
そこで、 Docker を裏で利用してコンテナ内ですべて実行することにしました。

個人の意見として、ビルドやテストの実行環境はCIサーバーで担保するのではなく、プロジェクトで担保するべきだと思っています。
実行環境をプロジェクト毎に担保することによってCIサーバーのスケールや移行が楽になります。

コード内で Docker を利用する際には、

github.com

が非常に便利です。

これを利用するため、サーバーは Golang で記述することにしました。

また、ジョブ自体は Maven や Gradle、NPM、Fastlane などのタスクランナーを利用して実行することにしました。
サクッと利用方法を見たい場合は ここ から。

作る機能

CIの機能を分解すると、

  • トリガー
  • ジョブの実行
  • 結果の通知

に分けられると思います。

トリガー

ジョブを実行するタイミングです。
今回はサーバーとして起動し、GitHub の Webhooks を待ち受けることにしました。
GitHub の Webhooks は種類・情報が豊富です。

Webhooks | GitHub Developer Guide

これを利用することで任意のタイミングでジョブを実行できるようになります。

GitHub の Pull Request のコメントをトリガーフレーズとして実行できるようにしました。
Jenkins の Pull Request Builder Plugin の trigger phrases と同じような使い勝手です。

f:id:duck8823:20180506115916p:plain

ジョブの実行

前述のとおり、ジョブは Docker コンテナ内で実行します。
プロジェクト毎に Dockerfile を用意し、 ENTRYPOINT にタスクランナーのコマンドを設定しておくことで、トリガーレーズで任意のタスクを実行できるようになります。

gist.github.com

上記のような Dockerfile を用意していた場合、 ci test とコメントすると コンテナ内で mvn test が実行されます。

結果の通知

ジョブを実行したら、その結果を通知する必要があります。
リクエストに対して同期的にジョブを実行し、成功したら 200 を返すようにしました。 ※ジョブに時間がかかってしまうので、GitHub の Webhooks は Timeout となってしまいます。

また、トリガーを GitHub の Pull Request 依存にしたので、Commit Status として結果を返すことにしました。

f:id:duck8823:20180506121019p:plain

コード

前置きが長くなりましたが、 以下が300行弱で簡易CIを実現するコードです。

gist.github.com

ここからは、上から順に各ポイントを紹介したいと思います。

Webhooks のパース

net/http を利用して Webhooks を待ち受けるサーバーを実現しています。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    ...
})
http.ListenAndServe(":8080", nil)

リクエストは JSON文字列 として受け取ります。
そのままでは非常に扱いづらいので、構造体にマッピングしましょう。

ただし、 Payload の構造体を自分で記述するのは骨が折れます。
以下のプロジェクトから構造体を拝借しました。

github.com

body, _ := ioutil.ReadAll(r.Body)
...

event := &github.IssueCommentEvent{}
json.Unmarshal(body, event)

ここでは、 github.IssueCommentEvent という構造体にマッピングしています。

冒頭で記述したとおり、GitHub の Webhooks には非常に多くの種類(event type)があります。
その種類によって Payload が異なるので、正しく構造体にマッピングしなければなりません。

どの event type かは、リクエストのヘッダー X-GitHub-Event に記述されています。
Pull Request のコメントは issue_comment です。それ以外の場合は除外しましょう。

https://developer.github.com/v3/activity/events/types/#issuecommentevent

githubEvent := r.Header.Get("X-GitHub-Event")
if githubEvent != "issue_comment" {
        ...
    return
}

トリガーフレーズの取得

Pull Requestのコメント内容は Payload の comment.body から取得することができます。
正規表現/^ci\s+(?<phrase>.+)/ にマッチした場合にジョブを実行します。

if !regexp.MustCompile("^ci\\s+[^\\s]+").Match([]byte(event.Comment.GetBody())) {
    ...
    return
}
phrase := regexp.MustCompile("^ci\\s+").ReplaceAllString(event.Comment.GetBody(), "")

Pull Requestかどうかの判定

event type issue_comment は名前から分かる通り Issue のコメントでも発火してしまいます。
Payload の issue.number から得られた番号から、 Pull Request 情報を取得することができます。

pr, _, err := githubClient.PullRequests.Get(
    context.Background(),
    event.Repo.Owner.GetLogin(),
    event.Repo.GetName(),
    event.Issue.GetNumber(),
)

特定のブランチを Clone する

Gitリポジトリの Clone には、

github.com

を利用しました。
特定のブランチをクローンする場合は CloneOptions.ReferenceName を指定します。
refs/heads/<ブランチ名> で指定する必要があります。
Pull Request の Head ブランチは上記で取得した Pull Request 情報から取得することができます。

base := fmt.Sprintf("%v", time.Now().Unix())
root := fmt.Sprintf("/tmp/%s", base)
repo, err := git.PlainClone(root, false, &git.CloneOptions{
    URL:           event.Repo.GetCloneURL(),
    Progress:      os.Stdout,
    ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", pr.Head.GetRef())),
})

GitHub に Pending の commit status をつける

Commit Status は進行によって更新するので、共通の処理は CommitStatusService としてまとめておきました。

https://gist.github.com/duck8823/261fafdcea18b655ce6a49381499d9b5#file-main-go-L262-L294

git.PlainClone した際の戻り値として得られる git.Repository から、 HEAD の Hash を得ることができます。
Commit Status は特定の Commit Hash に対して設定することができます。
また、 Context はトリガーフレーズごとに異なるようにしました。これにより build は成功したけど test に失敗したなどの情報がわかるようになります。

ref, err := repo.Head()
...
statusService := &CommitStatusService{
    Context:      fmt.Sprintf("minimal_ci-%s", phrase),
    GithubClient: githubClient,
    Repo:         event.Repo,
    Hash:         ref.Hash(),
}
statusService.Create(PENDING)

Dockerイメージをビルドするための tarアーカイブ を作成する

Moby では Dockerfile を含むディレクトリを tar形式 でアーカイブする必要があります。
filepath.Walk を利用して、クローンしてきたディレクトリに対して再帰的に tarアーカイブに含めるようにしています。

filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
    if err != nil {
        return err
    }
    if info.IsDir() {
        return nil
    }
    file, _ := os.Open(path)
    defer file.Close()

    data, _ := ioutil.ReadAll(file)

    header := &tar.Header{
        Name: strings.Replace(file.Name(), root, "", -1),
        Mode: 0666,
        Size: info.Size(),
    }
    writer.WriteHeader(header)
    writer.Write(data)

    return nil
})

イメージのビルド

client.NewEnvClient()DOCKER_HOST などの環境変数を利用した Dockerクライアント を作成することができます。
Client.ImageBuild では非同期にDocker Imageのビルドが実行されます。
戻り値である types.ImageBuildResponseBodyEOF まで読むことでビルドの終了を同期的に待つことができます。

また、types.ImageBuildOptionsTags で タグを指定することができます( -t オプション相当 )。 タグにはコンテナの作成時と同じものを利用しましょう。今回は 実行ごとに UNIX時間 をつけているのですが、 リポジトリ名などでキャッシュを利用するようにすれば、ジョブの実行が高速化されるかもしれません。

cli, _ := client.NewEnvClient()
resp, _ := cli.ImageBuild(context.Background(), file, types.ImageBuildOptions{
    Tags: []string{base},
})
defer resp.Body.Close()

ioutil.ReadAll(resp.Body)
logger.Info("Image Build succeeded.")

コンテナの作成

コンテナを作成します。
Cmd に トリガーフレーズ 指定することで、任意のタスクの実行を実現できます。

con, _ := cli.ContainerCreate(context.Background(), &container.Config{
    Image: base,
    Cmd:   []string{phrase},
}, nil, nil, "")

コンテナの実行

いよいよコンテナの実行です。 コンテナ作成時に取得した コンテナID をスタートさせます。
コンテナの実行は同期的に行われます。
Client.ContainerWait で待つことができます。戻り値には コンテナ内で実行したコマンドの return code が含まれます。
return code0 以外のときはジョブに失敗したとみなし、 Commit Status を failure にしました。

また、ログは Client.ContainerLogs で取得できます。

cli.ContainerStart(context.Background(), con.ID, types.ContainerStartOptions{})
if code, err := cli.ContainerWait(context.Background(), con.ID); err != nil {
    ...
    return
} else if code != 0 {
    statusService.Create(FAILURE)

    http.Error(w, fmt.Sprintf("return code: %v", code), http.StatusInternalServerError)
    return
}

out, _ := cli.ContainerLogs(context.Background(), con.ID, types.ContainerLogsOptions{
    ShowStdout: true,
    ShowStderr: true,
})

成功の通知

最後までエラーが無ければ、 Commit Status を success にしましょう。
また、レスポンスにログを渡しています。

statusService.Create(SUCCESS)

buf := new(bytes.Buffer)
buf.ReadFrom(out)

respBody, err := json.Marshal(struct {
    Console string `json:"console"`
}{
    Console: buf.String(),
})

w.WriteHeader(http.StatusOK)
w.Write(respBody)

使い方

準備

Commit Status を作成するために、 GITHUB_API_TOKEN を設定する必要があります。
トークンは https://github.com/settings/tokens から取得することができます。

export GITHUB_API_TOKEN=<your api token>

サーバーを起動する

Go の環境がある場合

$GOPATH が設定されていて、 $GOPATH/bin に $PATH が通っている場合、は go get で取得できます。

go get -u github.com/duck8823/minimal-ci
minimal-ci

Docker Compose で起動する (コンテナからホストのDockerデーモンを利用する)

サーバー自体を Docker コンテナで起動する場合も、ホストの Docker デーモンを利用して実行するようにしましょう。
OS によって若干異なるのでそれぞれの compose ファイルを用意しました。

Windows の場合

Docker for Windows を利用します。

www.docker.com

あらかじめ Docker for Windows の Settings から、 Expose daemon on tcp://localhost:2375 without TLS を設定しておきましょう。

f:id:duck8823:20180506165017p:plain

DOCKER_HOST として tcp://docker.for.win.host.internal:2375 を設定することでコンテナ内からホストの Dockerデーモン を利用することができます。

docker-compose -f docker-compose.win.yml

Mac の場合

Docker for Mac を利用します。

www.docker.com

Docker for Mac には Expose daemon... の設定はありません。

bobrik/socat のイメージを利用することで実現できました。
https://hub.docker.com/r/bobrik/socat/

また、 DOCKER_HOSTtcp://docker.for.mac.host.internal:2375 です。

docker-compose -f docker-compose.mac.yml

Optional.of( ngrok の利用 )

Webhooks を受け取るために、GitHubからサーバーにアクセスできる必要があります。
ngrok でローカルのポートを外部に公開することができます。
試しにポートを公開したい場合は利用するといいかもしれません。
リポジトリの composeファイル では ngrok コンテナはコメントアウトしています。

ngrok.com

ngrok を起動したら http://localhost:4040/status にアクセスしてみましょう。

f:id:duck8823:20180506172407p:plain

ここで表示される URL にアクセスすることによって、 フォワーディングされます。

Webhooks の設定

GitHubでは、リポジトリ毎に Webhooks を設定します。
https://github.com/<owner>/<repo>/settings/hooks/new

Payload URL には 公開しているURL を設定します。
Which events would you like to trigger this webhook? には Issue comments を設定しましょう。

f:id:duck8823:20180506172812p:plain

まとめ

GitHub / Docker( Moby ) を使って簡易CIを作ってみました。
セキュリティがガバだったり、失敗時のログを後から見られなかったりとまだまだ機能は足りないですが、 このコードをベースにオリジナルのCIを作ってみると楽しいと思います。

Nginx の Webdav でデプロイ時のみパスワードが必要な Mavenリポジトリ を立てる

前回の記事で JitPack を使えば Mavenリポジトリ を立てる必要すらないっていう話を書いたのですが、 クローズドな環境では外部サービスの利用が難しい場合があります。

そこで、今回は Nginx を使ってサクッと Mavenリポジトリ を立ててみます。
さらにリポジトリは誰でも見られるようにして、デプロイ時はパスワード認証をかけます。

CentOS 7系でやってみましょう。
ここでは、使い捨てのDockerコンテナを立ち上げて試します。

docker run --rm -it -p 80:80 centos:7 bash 

Nginxのインストール

CentOS 7 にインストールするためには、yumリポジトリを追加する必要があります。

vi /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=0
enabled=1

リポジトリを追加したら yum コマンドでインストールしましょう。

yum install -y nginx

インストールをしたら起動します。 nginx  コマンドはバックグラウンドで実行されます。

nginx 

Dockerコンテナでなく実際のサーバーで実行する場合は

systemctl start nginx

で開始することができます。

起動した状態で、ブラウザで以下にアクセスしてみましょう。

http://localhost/

以下のような画面が表示されれば Nginx が起動しています。

f:id:duck8823:20171224095744p:plain

確認が済んだらプロセスを終了します。
コンテナで起動している場合は pid を取得して kill します。

ps x | grep [n]ginx | awk '{print $1}' | xargs kill

実際のサーバーの場合は以下で終了することができます。

systemctl stop nginx

Basic認証できるようにする

ライブラリの利用時は認証せず、デプロイ時にはユーザー名とパスワードで認証するようにします。
Nginx でBasic認証を利用する場合ために httpd-tools をインストールします。

yum install -y httpd-tools

Basic認証のユーザーファイルを作成しましょう。

htpasswd -c /etc/nginx/.htpasswd ユーザー名

画面の指示に従ってパスワードを入力します。

作成された /etc/nginx/.htpasswdBasic認証用のファイルとなります。

Mavenリポジトリの設定を追加する

Maven用の Webdav 設定を追加します。

vi /etc/nginx/conf.d/maven.conf
server {
  # localhost でアクセスすると default.conf が優先される。
  # 以下の設定で 127.0.0.1 にアクセスするとこちらが優先される。 
  listen 80 default;
  server_name _;

  # アクセスする際のパス 以下の場合 http://サーバー名/maven/
  location /maven/ {
    
    # この場合 /var + /mavan/ で Mavenリポジトリの root は /var/maven
    root /var;

    # デプロイ時の一時ディレクトリ
    client_body_temp_path /tmp/maven;
    # ディレクトリを作成するか
    create_full_put_path on;

    # ブラウザでディレクトリにアクセスした際に一覧が表示されるようにする
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;

    # webav を有効にする
    dav_access group:r all:r;
    dav_methods PUT DELETE MKCOL COPY MOVE;

    # GET 以外に制限(Basic認証)をかける
    limit_except GET {
      auth_basic "Maven Repo";
      auth_basic_user_file "/etc/nginx/.htpasswd";
    }
  }
}

設定を作成したら、必要なディレクトリを作成しましょう。
プロセスは nginx ユーザーで実行されるため、作成したディレクトリのオーナーを nginx に変更しておきます。

mkdir -p /var/maven /tmp/maven
chown nginx:nginx /var/maven /tmp/maven
nginx

/etc/nginx/conf.d/default.confserver_name localhost; が設定されているため、 http://localhost/maven/ にアクセスしても default.conf が適用されます。 よって、 404 エラーが返ってきてしまいます。

http://127.0.0.1/maven/ にアクセスすることで /etc/nginx/conf.d/macen.conf が適用され、 /var/maven/ 以下を参照することができます。

ライブラリをデプロイする

Maven

デプロイしたいプロジェクトの pom.xml を編集します。

distributionManagementリポジトリ情報を追記します。
Dockerホストからデプロイする場合、 http://127.0.0.1/maven/ です。
webdav を利用してアップロードするので、 dav:http://127.0.0.1/maven/ となります。

<project>
    ...
    <distributionManagement>
        <repository>
            <id>docker.maven</id>
            <name>Maven Repo</name>
            <url>dav:http://127.0.0.1/maven/</url>
        </repository>
    </distributionManagement>
    ...
    <build>
        <extensions>
            <extension>
                <groupId>org.apache.maven.wagon</groupId>
                <artifactId>wagon-webdav-jackrabbit</artifactId>
                <version>3.0.0</version>
            </extension>
        </extensions>
        ...
    </build>
    ...
</project>

webadv で GET 以外のメソッドにはBasic認証をかけているので、 この状態では ` 401 Unauthorized デとなりプロイできません。

認証情報は $HOME/.m2/settings.xml に記述します。

<settings>
    <servers>
        <server>
            <!-- pom.xml の distributionManagement/repositoryの id と一致させる -->
            <id>docker.maven</id>
            <username>ユーザー名</username>
            <password>パスワード</password>
        </server>
    </servers>
</settings>

下記のコマンドでデプロイすることができます。

mvn deploy

Gradle

build.gradle を編集します。

apply plugin: 'maven'

configurations {
    deployerJars
}

repositories {
    mavenCentral()
}

dependencies {
    deployerJars 'org.apache.maven.wagon:wagon-webdav-jackrabbit:3.0.0'
}

uploadArchives {
    repositories.mavenDeployer {
        configuration = configurations.deployerJars
        repository(url: "http://127.0.0.1/maven/") {
            authentication(userName: "ユーザー名", password: "パスワード")
        }
    }
}

コマンドは以下の通りです。

./gradlew uploadArchives

これによりJarファイル や pom.xml も自動的に生成され、サーバーにアップロードされます。

ライブラリの利用

デプロイ後にブラウザでアクセスしてみると、ディレクトリが増えていると思います。

f:id:duck8823:20171224100130p:plain

Mavenリポジトリは URL を指定するだけで追加できます。
リポジトリを追加したら、通常のライブラリと同じように dependency を記述します。

Maven

<project>
    <repositories>
        ...
        <repository>
            <id>maven.docker</id>
            <name>Maven Repo</name>
            <url>http://127.0.0.1/maven/</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>example-maven</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

Gradle

repositories {
    maven { url 'http://127.0.0.1/maven/' }
}
dependenciea {
    implementation 'com.example:example-maven:0.0.1-SNAPSHOT'
}

おわり

Nginx で webdav を利用して Mavenリポジトリ を立てて、Basic認証でデプロイ(アップロード)を制限する方法を記述しました。
社内ネットワークでライブラリを配布したいけど、webdavでなんでもデプロイされるのはちょっと...ていう場合には使えるかもしれないです。かなりニッチ...

お試しが終わったらコンテナを終了させましょう。
最初の docker コマンドで --rm オプションを指定しているので、コンテナから抜ければ自動的に削除されます。
まっさらな環境で試す場合に Docker 非常に便利ですね。

exit

Mavenライブラリ配布の自動化をやめた話

この記事は モバイル 自動化/自動テスト Advent Calendar 2017 の19日目です。

皆さんは、JavaやKotlinで作成したライブラリ、加えてGradleプラグインの配布について悩んだことはないでしょうか。
クロスプラットフォームな開発環境も増えましたが、Android開発といえば Java や Kotlin といった JVM言語が主流です。
もちろんライブラリもこれらの言語で作成されます。 

一般的に、JavaやKotlinで作成されたライブラリの配布はMavenリポジトリを介して行われます。

Maven公式のリポジトリMaven Central です。 その他、 Bintray jcenter も有名なリポジトリです。 よってライブラリを作成して公開したい場合は上記サーバーにデプロイすることになります。 しかし、Maven Central にライブラリを登録するまでにはアカウント作成など面倒な手順を踏むことになります。

今は21世紀なのでサクッとライブラリを作成して公開したいですね。

Mavenリポジトリを立てて公開する

Mavenリポジトリは、誰でも立てることができます。 http(s) でアクセスできるサーバーを立てるだけです。

自分はこれまで、GitHubソースコードを管理し、レンタルサーバーリポジトリを立てて公開していました。 また、ソースコードの更新からリポジトリへの公開は自動化していました。

自分の場合、MavenリポジトリとJenkinsサーバーを同一にしています。 GitHubで master ブランチが更新された場合、 Jenkinsが mvn deploy を実行するだけです。 このコマンドではローカルのMavenリポジトリに配置されますが、 HTTPサーバーの静的コンテンツを配置するディレクトリ以下にMavenリポジトリを指定すると、 サーバーを介してライブラリを利用することができます。

<distributionManagement>
    <repository>
        <id>local</id>
        <name>local</name>
        <url>file://localhost/path/to/DocumentRoot</url>
    </repository>
</distributionManagement>

この方法ではライブラリを作るたびにJenkinsのジョブを追加しなければなりません。 また、公開できるサーバーとドメインを所有している必要があります。

そこで、GitHubのみを利用して公開する方法を探していました。

GitHub Pagesを利用して公開する

GitHubの ユーザー名.github.io リポジトリあるいは特定のブランチを maven リポジトリとして公開しちゃう方法です。 「GitHub Maven リポジトリ」で検索とこの方法がヒットします。

今回は ユーザー名.github.io リポジトリを利用して公開する方法をご紹介します。

  1. GitHub上に、ユーザー名.github.io リポジトリを作成する

  2. public_repo と user:email の権限を付与した Personal Access Token を作成する

  3. ローカルマシンの settings.xml でサーバーを設定する
    通常は $HOME/.m2/settings.xml にあります。

<settings ...>
  <servers>
    <server>
     <id>github.com</id>
     <password>取得した Personal Access Token</password>
    </server>
  </servers>
</settings>
  1. プロジェクトの pom.xml を設定する
    今回利用する site-maven-pluginMaven Plugin として提供されているので、pom.xml で設定しています。
  <profiles>
    ...
    <profile>
      <id>github</id>
      <properties>
        <!-- settings.xml で記述したサーバーの id -->
        <github.global.server>github.com</github.global.server>
      </properties>
      <distributionManagement>
        <repository>
          <id>internal.repos</id>
          <name>Temporary Repository</name>
          <!-- mvn deploy コマンドで ビルドディレクトリ/mvn 以下にデプロイするようにする -->
          <url>file://${project.build.directory}/mvn</url>
        </repository>
      </distributionManagement>
      <build>
        <plugins>
          <plugin>
            <groupId>com.github.github</groupId>
            <artifactId>site-maven-plugin</artifactId>
            <version>0.12</version>
            <configuration>
              <message>Maven artifacts for ${project.version}</message>
              <!-- GitHubにプッシュするディレクトリを ビルドディレクトリ に指定する -->
              <outputDirectory>${project.build.directory></outputDirectory>
              <!-- ビルドディレクトリには classファイル なども作成されるのでディレクトリを指定する -->
              <includes>
                <!-- mvn deploy コマンドで作成されるローカルリポジトリ -->
                <include>mvn/**/*</include>
              </includes>
              <repositoryOwner>GitHubのユーザー名</repositoryOwner>
              <repositoryName>GitHubのユーザー名.github.io</repositoryName>
              <branch>refs/heads/master</branch>
              <!-- デフォルトだと最新版の artifact で上書きしてしまうので、マージする -->
              <merge>true</merge>
            </configuration>
            <executions>
              <execution>
                <goals>
                  <goal>site</goal>
                </goals>
                <!-- maven deploy 後に実行されるようにする -->
                <phase>deploy</phase>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
  1. 実行する
    実行時は上述の pom.xml で設定したプロファイルを指定します。
mvn deploy -P github
  1. 自動的に実行されるようにする
    あとはお好みの CI/CD で上述のコマンドが実行されるようにするだけです。

これによりライブラリの配布もGitHub上でできるようになりました。 ライブラリを利用する場合はリポジトリのURLとして https://ユーザー名.github.io/mvn/ を指定します。

maven-site-plugin を利用しているため、Gradleの場合は pom.xml の生成など手間が増えます。

JitPackを利用する

JitPack は少し変わったMavenリポジトリです。 Spockなどがこのリポジトリを介して配布されています。

JitPack を利用する場合、配布側でビルドする必要がありません。 JitPack上のライブラリをインポートしたタイミングで JitPack上でビルドされるのです。

GitHub上のライブラリを利用する

  1. Mavenリポジトリとして https://jitpack.io を指定する

  2. ライブラリの指定

    1. groupId を com.github.リポジトリオーナー名 にする
    2. artifactId を リポジトリ名 にする
    3. バージョンに リリースタグ または コミットハッシュ を指定する

この方法では特別ビルドを行う必要がありません。 しかし、JitPack が groupIdartifactId を書き換えてしまうので、混乱するかもしれません。 ライブラリの groupIdartifactId を それぞれ com.github.リポジトリオーナー名リポジトリ名 にしてしまうのが最も簡単でしょう。 しかし、groupId は独自のドメインを指定したい場合もあります。

JitPackでは DNS TXTレコード を利用してドメイン名を指定する方法を提供しています。 https://jitpack.io/docs/#custom-domain-name

まとめ

最近までレンタルサーバーMaven リポジトリを立てていたのですが、 できる限り管理するサーバーは減らしたいですよね。

Spockを利用する際に指定されていた Mavenリポジトリ が今まで利用したことがなかったものなので調べてみると、 とても便利だったので久しぶりにテンションがあがりました。

これまでGolangJavaScriptRubyなど比較して Javaのライブラリ配布は面倒な印象がありました。 しかし、JitPack を利用すれば配布するためのビルドする必要すらなくなりました。 また、GitHubにソースが公開されているけど配布されていないライブラリについても利用できる可能性があります。

Publicなライブラリの場合はこういったサービスを無料で利用することができて非常に便利ですね。