Windows ラップトップをメイン環境に (MacBook Pro はサーバーに)

最近仕事で使うようになった Kubernetes 自宅でも触ってます。AWS や GCP 以外の検証環境としては発売から4年以上経つ MacBook Pro 13 2015 (16GB RAM / Core i7) が唯一戦えるマシンになっていて Minikube (Virtual Box を利用) や Docker for Desktop の Kubernetes を切り替えてやってます。

ちょっと大きめのアプリを動かすとメモリ 16GB ではかつかつで GUI アプリを使う作業は厳しくなり、サブ機の Let's note CF-SZ6 (8GB RAM / Core i5) をメインにしてみました。

macOS の共有設定で、リモートログインを有効にして SSH で入るようにしました。

f:id:kondoumh:20190609012744p:plain:w300

画面がオフになってもスリープしない設定も入れておきます。

f:id:kondoumh:20190615093128p:plain

Mac には Git for Windows の mintty で接続しています。Windows 10 は 徐々に UI 改善されているので Emacs ライクなキーバインドがないことを除けばけっこう快適です。もうすぐ Windows Terminal も リニューアルされるし、VS Code で Remote Development も使えるようになったし Mac じゃなくてもいい作業は増えていくことでしょう。

marketplace.visualstudio.com

メモリ32GB以上のパワフルマシンを調達したい気持ちもありますが、しばらく手持ちのマシン + クラウドで凌ごうかと。

Netlify と PWA で JAMstack を目指す

静的サイトをホスティングできる Netlify 流行ってます。Git リポジトリを指定するだけでビルド・デプロイでき、CDN でリソースをキャッシュしてくれるので、PWA のホスティングにも向いています。

PWA を作る方法は色々とありますが Vue CLI で簡単にプロジェクト生成できるのでちょっと試してみました。

プロジェクト作成時にマニュアル選択にします。

f:id:kondoumh:20190526084153p:plain:w330

フィーチャー選択で PWA Support を選択します。

f:id:kondoumh:20190526084212p:plain

あとは好みで選択してプロジェクトを生成。

PWA アプリは前に作ったトイ・ダイアグラムエディタを流用し GitHub にリポジトリを作りました。

blog.kondoumh.com

Netlify に GitHub アカウントでサインアップして GitHub のリポジトリをピックアップ。

f:id:kondoumh:20190526094434p:plain

ビルド設定をして Deploy site ボタンを押してしばらく待つとデプロイが完了します。

f:id:kondoumh:20190526094317p:plain

あっけなくデプロイが終わりました。Deploy log も Web UI で確認できるので、エラー原因の特定も簡単です。

f:id:kondoumh:20190526095201p:plain

デプロイされたサイトにアクセスして、PWA アプリをインストールしてみました。

f:id:kondoumh:20190526095431p:plain

ちゃんと動いてます。

iPad の Safari や Android の Chrome からも PWA としてインストールできました。

このサンプルはクライアントサイドだけで完結するので、Tumbr API を叩いて簡単な画面描画をするアプリを PWA にしてデプロイしてみました。

blog.kondoumh.com

こちらも問題なく動作。

外部 API を叩いてちょっとしたタスクをこなすための PWA は手軽に作れるようになりました。

主にクライアント側の JavaScript と API で動的な処理を行い、HTML や CSS はデプロイ時に pre build するというアプリケーションアーキテクチャを JAMstack というそうです。

jamstack.org

Netlify ではデプロイ時にプロジェクトのビルドスクリプトを実行して成果物をデプロイしてくれます。

  • 静的サイトジェネレータでサイトをビルド -> 静的サイト公開
  • Vue CLI などのアプリビルド -> PWA 公開
  • ビルド時にJSON 生成 -> JSON データ公開

のように。他の API やサイトのスクレイピング結果などをアグリゲートするような用途にマッチします。

さらに、Functions を使えば AWS Lambda をラップしたサーバーレスな API も作ることができます。

www.netlify.com

JAMstack なアプリ開発を支援する機能どんどん実装されている感じがします。

以前 Web IDE である CodeSandbox を使ってみましたが、CodeSandbox に Netlify へのデプロイ機能が統合された模様です。

blog.kondoumh.com

www.netlify.com

サーバーレスで、ブラウザだけでアプリの開発からデプロイまで完結してしまう・・。そんな時代になりました。

Electron WebView のデバッグで DevTools を使う

Electron での開発で自分で作ったアセット (HTML / JavaScript / CSS) を WebView で表示することはあまりないと思いますが、アプリの構造上 WebView を使う必要がある場合、困るのは Main Wndow 用の DevTools では WebView コンテンツの JavaScript デバッグとか DOM のインスペクトができないことです。

WebView には openDevTools メソッドがあり専用の DevTools を起動できます。WebView のdom-ready イベントで使うと別ウインドウで上がってきます。

const webview = document.querySelector('webview');
webview.addEventListener('dom-ready', () => {
  webview.openDevTools();
});

開発時 (Electron アプリがパッケージングされていない時) だけ DevTools を起動したい場合、App モジュールの isPackaged を使って判定ができます。DEBUG モードのように使えるわけです。App モジュールは main process で使うもので renderer process では require しても取れませんが Remote モジュール経由で使えます。

const {app} = require("electron").remote;

const webview = document.querySelector('webview');
webview.addEventListener('dom-ready', () => {
  if (!app.isPackaged) {
    webview.openDevTools();
  }
});

f:id:kondoumh:20190517204717p:plain

これで DevTools が使えるようになりましたが、毎回オープンするのはウザいので開発時だけ表示するメニューを用意して、そこから起動できるようにします。

main process では メニュー構築用のテンプレートにパッケージングされていない場合のみ Debug メニューを追加します。このメニューには強制リロードと Main Window 用のサブメニューも role で追加しておくと便利です。

  const template = [
    {
      // snip
    }
  ];

  if (!app.isPackaged) {
    template.unshift({
      label: "Debug",
      submenu: [
        { role: "forceReload"},
        { role: "toggledevtools" },
        { 
          label: "open devTools for WebView",
          click () {
            mainWindow.webContents.send("openDevTools");
          }
        }
      ]
    });
  }

f:id:kondoumh:20190517204817p:plain

renderer process では コマンドに応じて WebView の openDevTools を呼びます。

ipcRenderer.on("openDevTools", () => {
  const webview = document.querySelector('webview');
  webview.openDevTools();
});

これで必要時だけ DevTools を起動できて Remote モジュール使わなくてよくなりました。

Vuetify の Data tables を Electron アプリに組み込む

Vue の UI Component 集 Vuetify の Data tables Component を Electron アプリで使ってみたいと思い、組み込み方法を検討していました。

vuetifyjs.com

以前 electron-vue で Electron プロジェクトを生成して Vue 使う話を書きました。

blog.kondoumh.com

このようなボイラープレートを使うのはプロジェクト全体を Vue で構築する場合はいいのですが、Vue 使っていない既存プロジェクトに部分適用したい場合には適しません。

Vue のセールストークとしては、プロジェクトに部分適用可能というのがあります。

他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は少しずつ適用していけるように設計されています。中核となるライブラリは view 層だけに焦点を当てています。そのため、使い始めるのも、他のライブラリや既存のプロジェクトに統合するのも、とても簡単です。

はじめに — Vue.js より

そこで、アプリ画面を構成する html ファイルで で Vue と Vuetify を読み込んで、画面単位で利用することにしました。

  <head>
    <meta charset="UTF-8">
    <title>Vuetify Data tables component sample</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/vuetify@1.5.14/dist/vuetify.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons">
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

Scrapbox の API でページリストを取得して表示する画面を作成します。

template は vue ファイルを使わず、html に記述。v-data-table コンポーネント標準のページング部品は拡張性がない感じなので v-pagination コンポーネントをを連結して使っています。v-data-table の hide-actions 指定で標準ページング部品を隠せました。v-pagination のページングイベント(next, prev, ページの数字ボタンクリック) ハンドラのメソッドは @input 属性で指定できます。

  <body>
    <div id="app">
      <v-app>
        <div>
          <v-data-table
          :headers="headers"
          :items="items"
          hide-actions
          :pagination.sync="pagination"
          :total-items="pagination.totalItems"
          class="elevation-1"
          >
            <template v-slot:items="props">
              <td class="text-xs-left">{{ props.item.pin != 0 ? "&#x2714;" : "" }}</td>
              <td class="text-xs-left">{{ props.item.views }}</td>
              <td class="text-xs-left">{{ props.item.linked }}</td>
              <td class="text-xs-left">{{ formatDate(props.item.updated) }}</td>
              <td class="text-xs-left">
                <a
                  :href="'https://scrapbox.io/'+ projectName + '/' + props.item.title"
                  target="_blank"
                >{{ props.item.title }}</a>
              </td>
              <td><img :src="props.item.image" style="width: auto; height: 25px"></td>
            </template>
          </v-data-table>
          <div class="text-xs-center pt-2">
            <v-pagination
              v-model="pagination.page"
              :length="page"
              @input="input"
            ></v-pagination>
          </div>
        </div>
      </v-app>
    <script>
      // You can also require other files to run in this process
      require('./renderer.js')
    </script>
  </body>

次に Vue の処理本体。html で指定している render.js に書いています。

API 叩いてデータを取得する関数 fetchData ではクエリパラメータでページング時のスキップ数やソートキーを指定。この関数を mounted、 v-data-table の pagination イベント、v-pagination の input イベントで呼んでます。

v-data-table のイベントはソート可能な列ヘッダーをクリックした時に発生します (ちょっと分かりづらい) 。data - headers では API でソートキーを指定できない列は sortable を無効化しています。Scrapbox の データの並び順(昇順・降順)は指定できないので descending は常に false で打ち消しています。

const app = new Vue({
  el: '#app',
  async mounted () {
    this.fetchData()
  },
  methods: {
    async fetchData () {
      const skip = (this.pagination.page - 1) * this.pagination.rowsPerPage
      let url = `https://scrapbox.io/api/pages/kondoumh?skip=${skip}&limit=${this.pagination.rowsPerPage}&sort=${this.pagination.sortBy}`
      const res = await axios.get(url)
      this.items = await res.data.pages
      this.pagination.totalItems = res.data.count
    },
    formatDate (timestamp) {
      // snip
    },
    input (page) {
      this.fetchData()
    }
  },
  computed: {
    page () {
      if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null ) return 0
      return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
    }
  },
  watch: {
    pagination: {
      handler () {
        this.pagination.descending = false
        this.fetchData()
      }
    }
  },
  data: () => ({
    items: [],
    pagination: {
      sortBy: 'updated',
      rowsPerPage: 20,
      totalItems: 0
    },
    headers: [
      { text: 'pin', value: 'pin', sortable: false, width: '5%' },
      { text: 'views', value: 'views', width: '10%' },
      { text: 'linked', value: 'linked', width: '10%' },
      { text: 'updated', value: 'updated', width: '25%' },
      { text: 'title', value: 'title', sortable: false, width: '30%'},
      { text: 'image', value: 'image', sortable: false, width: '25%' }
    ]
  })
})

無事データを取得して表示できました。

f:id:kondoumh:20190519153623p:plain

比較的簡単に1画面だけ Vue コンポーネントを使うことができました。今回の例では Electron 特有の実装は何もないですが、コンポーネント外の UI 部品と連携する場合は、 mounted で IpcRenderer のリスナーを定義することになります。

CLI で自在に操作可能な CI ツール Concourse を使う

CI ツールシリーズ第3弾。Concourse は Pivotal が開発した CI ツールです。概要についてはこのスライドが参考になります。

backpaper0.github.io

チュートリアルも日本語に翻訳されています。

concoursetutorial-ja.cfapps.io

macOS / Windows では Docker Desktop で簡単に導入できます*1。GitHub から concourse-docker をクローンまたはダウンロードして、docker-compose up -d するだけ。

github.com

EC2 などのサーバで試すには、docker-compose.yml の 環境変数 CONCOURSE_EXTERNAL_URL を環境変数から取得するようにしておくとよいでしょう。

  concourse-web:
    image: concourse/concourse
       :
    environment:
       :
    - CONCOURSE_EXTERNAL_URL=${CONCOURSE_EXTERNAL_URL}

起動前に環境変数を設定しておけば、ローカルのブラウザからパイプラインの状況を監視できます。

$  export CONCOURSE_EXTERNAL_URL=http://$(curl -s ifconfig.me):8080

Concourse の特徴である fly コマンドは Go で書かれたクロスプラットフォームな CLI ツールで*2、セットアップされたサイトのトップページからダウンロードできます。/usr/local/bin など PATH の通ったディレクトリに配置して起動できるようにします。

sudo mkdir -p /usr/local/bin
sudo mv ~/Downloads/fly /usr/local/bin
sudo chmod 0755 /usr/local/bin/fly

fly コマンドで作業するには Concourse API を叩くためのトークンを得るためにログインが必要です。

$ fly --target tutorial login --concourse-url http://127.0.0.1:8080

ここでは、tutorial という名前でセッションを作っています。ログイン用の URL が表示されますので、ブラウザで開いてログインを完了させます。Concourse を localhost で起動している場合は URL を叩くだけで完了します。リモートの場合は、画面に表示されるコピーボタンを押してクリップボードにトークンをコピーし、ターミナルのプロンプトに貼り付けます。

f:id:kondoumh:20190424232805p:plain

今回も Spring Boot サンプルアプリをターゲットにパイプラインを作ってみます。

github.com

Concourse では Git のリポジトリや Docker レジストリなどを外部のリソースとして使用することができます。今回作成するパイプラインは、ビルド対象のプロダクトとは別の Git リポジトリで管理するようにしました。こうすることでパイプラインの変更とビルド対象のコードの変更を分離できます。

前回の GitLab CI はプロダクトのリポジトリに CI の定義を配置するため、パイプラインの修正のたびにコミットが発生します。一発で成功することが分かっている場合を除き、フィーチャーブランチで作業して成功したら MR を作成するという開発スタイルになるでしょう。Concourse CI ではパイプライン専用のリポジトリを作ることができます。

今回作成したパイプラインのディレクトリ構造は以下のようにしています。

└── workspace
    ├── pipeline.yml
    └── tasks
        ├── build.sh
        ├── build.yml
        ├── deploy.sh
        └── deploy.yml

pypeline.yml がリソース・ジョブのオーケストレーションを記述するメインのファイルです。

今回定義した pipeline.yml は次のようになっています。ジョブは build-sb-sample という名前の1つだけで、ジョブ内でビルドとデプロイは別タスクに分けています。デプロイではコンテナをビルドして Docker in Docker 構成で起動するため privileged: true を指定しています。

resources:
  # プロダクトのリポジトリ
  - name: sb-sample-service
    type: git
    source:
      uri: https://github.com/kondoumh/sb-sample-service.git
      branch: master
  # パイプラインのリポジトリ
  - name: pipelines
    type: git
    source:
      uri: https://github.com/kondoumh/pipelines.git
      branch: master

jobs:
- name: build-sb-sample
  public: true
  plan:
  # 2つのリポジトリを取得
  - get: sb-sample-service
  - get: pipelines
  # プロダクトのビルドタスク
  - task: Build project
    file: pipelines/concourse/workspace/tasks/build.yml
  # プロダクトのデプロイタスク
  - task: Deploy service
    privileged: true
    file: pipelines/concourse/workspace/tasks/deploy.yml

タスクの定義は専用の YAML に切り出しています。

build.yml の定義。Java:8 のコンテナを使って、ビルド用のシェルを実行します。inputs としてパイプラインのプロダクトの両リポジトリを使うため2つとも指定。プロダクトのディレクトリ内(の target ディレクトリ) にビルド成果物である JAR ファイルを出力するため、outputs にもプロダクトのリポジトリを指定しています。Maven Central リポジトリから多くの依存ライブラリをダウンロードするため、Maven のローカルリポジトリ (.m2) をキャッシュに指定しました。

platform: linux

image_resource:
  type: docker-image
  source: {repository: java, tag: 8}

inputs:
  - name: pipelines
  - name: sb-sample-service

outputs:
  - name: sb-sample-service

caches:
  - path: .m2/

run:
  path: "pipelines/concourse/workspace/tasks/build.sh"

build.sh では、取得したリポジトリ内の mvnw コマンドでビルド・単体テスト・パッケージングを行います。

#!/bin/sh -xe

cd sb-sample-service
./mvnw package

deploy.yml の定義。inputs として、前のタスクの outputs である (JAR が生成された) プロダクトリポジトリとパイプラインリポジトリを指定しています。

platform: linux

image_resource:
  type: docker-image
  source: {repository: quay.io/cosee-concourse/dind, tag: latest}

inputs:
  - name: sb-sample-service
  - name: pipelines

run:
  path: "pipelines/concourse/workspace/tasks/deploy.sh"

デプロイで使用する docker イメージは Docker in Docker での docker-compose に対応した dind のイメージを使いました。

github.com

deploy.sh では、Docker サービスを起動して、プロダクトのリポジトリ内の docker-compose.yml を使ってコンテナをビルド・起動し、curl で API を叩いて動作確認しています。

#!/bin/sh -xe
source /docker-lib.sh
start_docker

cd sb-sample-service
docker-compose up -d
sleep 30
docker ps
curl -X POST "http://localhost:18888/api/user/" -H "accept: */*" -H "Content-Type: application/json" -d "{ \"id\": 1, \"name\": \"Mike\"}"
curl -X GET "http://localhost:18888/api/usr/1" -H "accept: */*"
docker-compose down

このようにして作成したパイプラインを fly コマンドを使って Concourse に設定 (set-pipeline)、ポーズ解除 (unpause-pipeline)、ジョブ起動 (trigger-job) します。

$ fly -t tutorial set-pipeline -c pipeline.yml -p build-sb-sample
$ fly -t tutorial unpause-pipeline -p build-sb-sample
$ fly -t tutorial trigger-job -j build-sb-sample/build-sb-service -w

set-pipelinespunpause-pipelineup のように短縮名もありますので打鍵量を減らすことができます。

trigger-job で watch オプション (-w) を付けることで、そのままターミナルに実行ログが流れていきます。

f:id:kondoumh:20190424224856p:plain

もちろん Web UI でもパイプラインを起動して実行の様子を眺めることができます。右側の build-sb-sample が実行中になっています。

f:id:kondoumh:20190424012624p:plain

実行されているジョブの様子。2つのリソースがジョブに繋がって実行中であることがわかります。

f:id:kondoumh:20190424012643p:plain

成功して完了した画面。

f:id:kondoumh:20190424012809p:plain

Web UI でも実行ログを確認できます。タスクごとにセクションが分かれています。

f:id:kondoumh:20190424012838p:plain

Elm で書かれたという UI はシンプルでリアクティブにアニメーションします。

fly watch でターミナルを見ているだけでも実行状態を監視できるので Web UI は触らなくても作業が進みます。チュートリアルにもこうあります。

fly watch コマンドは、ラップトップPCのバッテリーの節約になります。実は、Concourse Web UI で実行されているJobを見ていると、ターミナルでfly watchを実行するよりもバッテリー消費量が多いことが分かりました。あなたのPCでは、状態が異なる場合があるかもしれませんが。

実際、今回は外出先の待ち時間でパイプラインの動作確認をしていたのですが、EC2 インスタンスで起動した Concourse に fly コマンドを叩いて実行を確認、YAML ファイルとシェルを編集し Git リポジトリに push という作業フローだったので、Pixel 3 の Termux だけで完結してしまいました。

blog.kondoumh.com

以上のように Concourse はターミナル操作がメインになるので、マニアックというか地味ですが、Jenkins と比べると覚えることが少なくシンプルな CI ツールに仕上がっています。

Maven や NPM のような各プログラミング言語専用のビルドツールの枠を超えて、開発者の手元で簡単に流せる CI ツールとして採用するのもよいでしょう。

ジョブ、タスクのような単位でビルドを管理できるため、多数のモジュールの依存関係を管理する複雑で巨大なパイプラインにもスケールアウト可能になっていると思います。

*1:いつの間にか Docker for Mac / Windows から名前変わったんですね

*2:Concourse 本体も Go で開発されています。

GitLab のコンテナネイティブなパイプライン機能を使ってみる

オンプレミスで自前のサーバーを建て GitLab をセルフホストしている現場をよく見るようになってきました。GitLab 曰く、セルフホスト市場で 2/3 のシェアを獲っているそうです。

今や GitLab はソースコード管理に留まらず DevOps 含めたワンストップのサービスになろうとしているようです。

docs.gitlab.com

CI/CD が標準装備で、月2000分まで無償枠です。GitLab にアカウントを作成して、CI を試してみました。

1つのリポジトリに対して1つの CI/CD パイプラインを定義できます。パイプラインは .gitlab-ci.yml という名前の YAML ファイルで記述します。

build1:
  stage: build
  script:
    - echo build

test1:
  stage: test
  script:
    - echo "run a test suite"

test2:
  stage: test
  script:
    - echo "run a lint test"

deploy1:
  stage: deploy
  script:
    - echo "Do your deploy here"

git clone がないのは対象リポジトリが固定だからです。デフォルトの stage 名と実行順は buildtestdeploy で、ステージ名が同じ場合は並行実行になります*1。上記のパイプラインは以下のように、test1test2 がパラレルに実行されます。

f:id:kondoumh:20190414060306p:plain

前回の Jenkins Build Pipeline と同様に Spring Boot アプリのサンプルを使います。

github.com

リポジトリは GitHub から簡単にインポートできます。プロジェクトのルートに .gitlab-ci.yml を配置すれば CI Runner がコミットの度にパイプラインを実行してくれます。

f:id:kondoumh:20190414013930p:plain

Runner は GitLab で稼働していますが、Jenkins の JNLP エージェントのようにローカル環境にインストールして実行することも可能です。

docs.gitlab.com

今回作ったパイプライン定義です。BuildTest の2ステージ構成で、それぞれ Java 8 と Docker in Docker のコンテナイメージを使います。Build ステージで Spring Boot アプリの JAR を作成 (ビルドと単体テスト実行、アーカイブが流れます)。artifacts で生成した JAR のパスを書いて、Test ステージの docker-compose から呼ばれる Dockerfile でコンテナに JAR を COPY しています。

Build:
  image: java:8
  stage: build
  script: ./mvnw package
  artifacts:
    paths:
      - target/sb-sample-service.jar

Test:
  image: docker:latest
  services:
    - docker:dind
  stage: test
  script:
    - apk add --no-cache py-pip python-dev libffi-dev openssl-dev gcc libc-dev make curl
    - pip install docker-compose
    - docker-compose up -d
    - sleep 30
    - 'curl -X POST "http://docker:8080/api/user/" -H "accept: */*" -H "Content-Type: application/json" -d "{ \"id\": 1, \"name\": \"Mike\"}"'
    - 'curl -X GET "http://docker:8080/api/usr/1" -H "accept: */*"'
    - docker-compose down

Test ステージではコンテナ内でアプリのコンテナを起動して curl で REST API を叩きます。docker-compose をインストールするために Python をインストールして pip install でインストールしています。curl もインストール。

docker-compose up -d でアプリと Mongo DB のコンテナを起動、curl で REST API を叩きます。この時アプリのホスト名は docker を使います*2

パイプライン実行結果グラフのノードをクリックすると実行ログを確認できます。

f:id:kondoumh:20190414071031p:plain

f:id:kondoumh:20190414011906p:plain

今回は使っていませんが、ステージ毎に only セクションでビルドの種類を指定でき、MR (Merge Request) 単位のビルドや Master ビルドを定義して GitLab フローを回すのに最適です。

build:
  stage: build
  script: ./build
  only:
  - master

test:
  stage: test
  script: ./test
  only:
  - merge_requests

deploy:
  stage: deploy
  script: ./deploy
  only:
  - master

docs.gitlab.com

Jenkins のように複数のリポジトリを取得して複合的なパイプラインを作るということはできません*3が、今回見たようにコンテナがネイティブにサポートされているので、クリーン環境でテストできますし、慣れれば環境構築も楽ですね。

Microsoft にも Azure DevOps があるので GitHub とインテグレートしたりするのかな?

azure.microsoft.com

*1:stages セクションを定義してカスタマイズ可能です。

*2:これが分からずちょっとハマりました。

*3:将来的に計画はされてるようです。

Jenkins でコンテナアプリの CI

プロジェクトでは Jenkins でビルド職人をやることが結構あります。Jenkins のジョブは Web UI でポチポチ設定するのが伝統でしたが、最近はビルドパイプラインを DSL で書けるようになって Infrastructure as Code 化が進んでいます*1

wiki.jenkins.io

ここ数年は Docker コンテナでアプリをリリースすることが増えており、コンテナイメージのビルド・破棄というステップがパイプラインに入ってきます。

以前作った Spring Boot の REST API のコンテナ化アプリを題材にパイプラインを構築してみました。

blog.kondoumh.com

github.com

このサンプルでは、docker-compose で Spring Boot の jar ファイルをコンテナに COPY して起動し Mongo DB のコンテナと連携させています。

パイプラインは以下のようなステップで流します。

  • ソースコードのチェックアウト (git clone)
  • パッケージング (maven package)
    • ビルド
    • 単体テスト
    • jar ファイル生成
  • コンテナビルドと起動 (docker-compose up)
  • 結合テストの実行
  • コンテナの停止と破棄 (docker-compose down)

アプリの単体テストはコンテナビルド前に実行し、結合テストはコンテナを起動してエンドポイントに対して E2E のテストを実行します。

pipeline {
    agent { node {label 'jnlp_agent'} }
    stages {
        stage('checkout') {
            // git clone
            steps {
                checkout([$class: 'GitSCM', branches: [[name: '*/master']],
     userRemoteConfigs: [[url: 'https://github.com/kondoumh/sb-sample-service.git']]])
            }
        }
        stage('package') {
            steps {
                // build, unit test, package
                sh './mvnw package'
            }
        }
        stage('build and run container') {
            steps {
                // build and run container
                sh 'docker-compose up -d'
                sleep 60
            }
        }
        stage('container integration test') {
            steps {
                // e2e test with curl
                echo 'conteiner integration test'
                sh 'curl -X POST "http://localhost:18888/api/user/" -H "accept: */*" -H "Content-Type: application/json" -d "{ \"id\": 1, \"name\": \"Mike\"}"'
                sh 'curl -X GET "http://localhost:18888/api/usr/1" -H "accept: */*"'
            }
        }
    }
    post {
        always {
            // shutdown container
            sh 'docker-compose down'
        }
    }
}

DSL なので Groovy 知らなくても大体読めると思います。agent {} 宣言で実行する Jenkins agent を指定します*2stages 内の stage が順次実行されます。post { always{} } に書かれた処理は stages の途中でエラーが発生しても必ず実行されます。stage は並列実行などもできます。

docker-compose でコンテナをビルド実行すると Spring アプリケーションの起動に時間がかかるため、sleep を入れてます。

結合テストは Java で書いてちゃんと assert すべきですが、簡単な動作確認を curl で書いて済ませました。

実行は Blue Ocean のモダンな画面でモニタリングできます*3

f:id:kondoumh:20190401211954p:plain

jenkins.io

Blue Ocean の画面でパイプラインを編集可能なプラグインもあるようです。

今回のように Docker コマンドを shell で叩いてもいいのですが Pipeline Plugin の DSL で書けるようにしてくれるプラグインもあります。

wiki.jenkins.io

イメージの取得ビルド、コンテナ内のプログラム実行、ジョブ終了後のコンテナ破棄をやってくれます。ただし、docker-compose には対応していないようです。残念。

Jenkins のエージェント自体もコンテナで動かすことができて、公式のイメージを利用できます。

https://hub.docker.com/r/jenkinsci/slave

Jenkins ではプラグインを使う必要がありますが、最近の CI ツールはネイティブにコンテナイメージを扱えるものが主流になっています。 Jenkins も Jenkins X というプロジェクトで、クラウドネイティブ化を目指しています。

jenkins-x.io

*1:Jenkinsfile という Groovy の DSL でコードとして管理できます。

*2:以前は slave と呼ばれてましたがポリコレ的に改名されました。

*3:Blue Ocean は必須ではありません。