White Box技術部

WEB開発のあれこれ(と何か)

CircleCIでDockerfileのあるプロダクトをテストする

既存のアプリケーションをコンテナ化したら、CircleCIで回していたテストがコケていたので、その修正の記録です。

CircleCIの設定を書く

CircleCIの設定をいじるのは久しぶりだったので、以下のようなことも調べつつやっていたのですが、結局ハマってだいぶ時間をとられてしまいました。

  • dockerキー配下のimageは、先頭のコンテナイメージが、stepsのコマンド実行に使われる
  • コンテナから別コンテナを呼ぶ際の名前を変えたければ、nameキーを使う
  • ポートフォワードの指定はconfig.ymlからできない
  • dockerhubのdockerはalpineだったので、ソフトウェアのインストールはapk add

テスト用イメージの作成(ハマったとこ)

何にハマっていたのかと言うと、大きくは2つで

  1. コンテナから他のコンテナに繋げない
  2. コンテナの状態を育てていけない

というところでした。

1. コンテナから他のコンテナに繋げない

こういうことをしたかった、という実行環境のイメージは以下のような感じでした。

f:id:seri_wb:20200126004427j:plain:w600

各種stepを実行するためのコンテナイメージ(docker)がCircleCI上で動作していて、 そのコンテナの中で、アプリケーション(python)用のコンテナがビルドされ、起動後にテストが実行される・・・みたいな。

つまり、テストアプリケーションのコンテナを、Docker in Dockerの状態にしたかったのです。

そして、アプリケーションのコンテナから参照するテスト用のコンテナ(MySQL、Azurite)は、実行用コンテナと同列で起動させれば、ローカルのDocker Composeと同じ状態にできるなって思ったのですが、 結論から言うとこれはできないそうです。

f:id:seri_wb:20200315195206j:plain:w600

そうとは知らず、色々試してはみたので、結果だけは残しておこうかなって・・・

privilegedオプションでできること・できないこと

まずは、stepsの実行イメージをdockerベースのコンテナにし、そのコンテナでビルドしたイメージをprivilegedオプションを付けて起動させて、やりたいことができるか確認しました。

  build:
    docker:
      - image: docker:git
      - image: arafato/azurite:latest
        name: azurite
...
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Build image
          command: docker build -t hoge/hoge-app .
      - run:
          name: Run image
          command: docker run -td --privileged --name hoge-app hoge/hoge-app bash

この状態で、それぞれのコンテナからできたことと、できなかったことは以下になりました。

dockerイメージのコンテナを実行コンテナ、その上でビルドしたイメージをアプリケーションとして書いています。

できた

  • 実行コンテナからAzuriteのイメージ名でcurl接続

できなかった

  • アプリケーションからAzuriteへのcurl接続(ホスト名解決できない)
  • 実行コンテナからlocalhostでAzuriteにcurl接続(ホスト名をazuriteにするとできる)

ちなみにAzuriteのホスト名解決を行うためには、dockerキーのvalue値でnameの指定も必要です(image名と同じだとしても)。

2. コンテナの状態を育てていけない

次に、アプリケーションのコンテナでテストを実行できるようにするには、 Dockerfileで実行される処理以外にも処理を実行する必要があったので、コンテナの状態を育てていく必要がありました。

そこで、docker run時にコマンドとしてpipenv sync --devを渡していたわけですが、これがなぜか実行されなかったため、 run時はDockerfile上のCMDを動作させないためにbashを渡し、別のステップでdocker execで実施するようにするしかありませんでした。

また、docker execでpipenv sync --devを実行したステップの後、pipenv run lintなどをexecに渡して実行すると

「mypyなんぞない」

と言われたので、、、execにコマンドを渡す際はbash -cを使って渡すようにしました。

      - run:
          name: Install dependencies
          command: docker exec hoge-app bash -c 'pipenv sync --dev'
      - run:
          name: Lint code
          command: docker exec hoge-app bash -c 'pipenv run lint 2>&1' | tee lint.log
      - run:
          name: Test hoge-app
          command: docker exec hoge-app bash -c 'pipenv run test 2>&1' | tee test.log

      - store_artifacts:
          path: lint.log
      - store_artifacts:
          path: test.log

とりあえずこうすると、Docker in DockerでもLintとTestの実行はできたのですが、名前解決がうまく行かないことなどから肝心のテストがコケるので、サポート問い合わせした結果、やりたいような構成はできないという回答をもらったわけです。

以下にも直接通信ができないと書いてあるので、そのとおりだと言う話でもあるのですが。。。

最終的なconfig.yml

というわけで、最終的には実行コンテナをDockerfileのベースイメージにし、Dockerfileに書いている処理をstepsに記載するという、普通のやり方で対応しました。

以下は最終的なconfig.ymlを適当に抜粋したものになります。

version: 2

jobs:
  build:
    docker:
      - image: circleci/python:3.6.6
        name: hoge-app
        environment:
          DB_HOST: test-db
      - image: circleci/mysql:5.7-ram
        name: test-db
        environment:
          MYSQL_USER: root
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_TCP_PORT: 3306
      - image: redis
        name: redis
      - image: arafato/azurite:latest
        name: azurite
    working_directory: ~/hoge-app
    environment:
      LANG: ja_JP.UTF-8
      LC_ALL: ja_JP.UTF-8
      LANGUAGE: ja_JP:ja
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Set locales
          command: sudo localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
      - run:
          name: Update submodule
          command: git submodule update --recursive --init
      - run:
          name: Install dependencies
          command: |
            sudo apt-get update && \
            sudo apt-get install -y curl build-essential \
            mecab mecab-ipadic libmecab-dev
      - run:
          name: Install dependencies
          command: pipenv sync --dev
      - run:
          name: Setup azurite filedata
          command: curl -v -X PUT -H 'x-ms-version:2017-11-09' http://azurite:10000/azureacount/filedata?restype=container

      - run:
          name: Lint code
          command: pipenv run lint 2>&1 | tee lint.log
      - run:
          name: Test hoge-app
          command: pipenv run test 2>&1 | tee test.log

      - store_artifacts:
          path: lint.log
      - store_artifacts:
          path: test.log

TypeScriptでwindowにプロパティを追加するいくつかの方法

DjangoのテンプレートにReactを埋め込むとかいう、妙なことをしているときに、とある理由からwindowにReactやらを追加しておく必要ができたのですが、TypeScriptだとそのまま突っ込むと型の関係で怒られました。

調べてみるとほぼこのタイトルの記事があったのですが、他にもいくつか解決策が出たので残しておきます。

ちなみに、怒られたのはuseStateがReact自身の同一性を見ているからでした。 https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react

1. windowを作り直す

最初は上記の記事にあるように、window.tsファイルを作成して実装しました。

たしかこんな感じで書いたらいけるはずです。

  • window.ts
interface MyWindow extends Window {
  HogeForm: Comment
}
declare var window: MyWindow;
export default window;
  • index.ts
import React from 'react';
import { render } from 'react-dom';
import HogeForm from '../containers/HogeForm';
import window from './window';

render(<HogeForm />, document.getElementById('hoge_container'));

これで、hoge_containerタグの場所の表示を置き換えて表示しました。HogeFormの型はCommentでいけます。

この場合、表示するクラスやらが増える度にwindow.tsを修正することになります。

2. as anyを使う

管理ファイルが増えるのも煩わしいなぁとぼやいていたら、同僚にanyを使ったらやり方を教えてもらいました。

import React from 'react';
import ReactDOM from 'react-dom';
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;

import { HogeForm } from '../containers/HogeForm';
(window as any).HogeForm = HogeForm; 

これだとファイルも増えないので、わかりやすさは上がった気がします。 でもTypeScript使っててanyって明示するのもなぁと、同僚と唸ることに。

3. expose-loaderを使う

といったわけで、最終的にはexpose-loaderを使って解消しました。

import 'expose-loader?React!react';
import 'expose-loader?ReactDOM!react-dom';

import 'expose-loader?Containers!./containers/'; 

ライブラリを追加する必要はありますが、コード自体はだいぶスッキリしたので、対価には見合っているかなと。 ただ、直接関数をロードすると、呼び出す側が正しく認識できなかったので、いったんObjectにするようにしています(ここではContainers)。

これによって使う側がどう変わるかと言うと、django-react-templatetagsでの書き方が、

  • expose-loader導入前
{% load react %}
{% react_render component="HogeForm" props=hoge_data %}
  • expose-loader導入後
{% load react %}
{% react_render component="Containers.HogeForm" props=hoge_data %}

のようになります。


といったのを9月くらいにやっていたのですが、気付けばもう年も変わっているという、、、

今年は忘れないうちにアウトプットしていきたいところです。

【主にスクラム向け】プロダクトバックログフォーマットと運用フローについて

プロダクトバックログのテンプレートが欲しい

「プロダクトバックログなんて、そんな何個も作るものじゃないでしょ」

とか思っていたときもあったのですが、ここ数年、公私合わせて、年2、3個はプロダクトバックログ(PBL)を作り、その都度フォーマットを作っていたので、 いい加減(自分のためにも)PBLのフォーマットを決め、テンプレートを用意するようにしました。

これが「さいきょうのPBLだ!」

最強なのかはともかく、現時点で一番わかりやすく運用しやすいPBLのフォーマットは、以下のようになりました。

f:id:seri_wb:20190610002920j:plain

テンプレートにはGoogleスプレッドシートを利用しています。

なぜかというと、現状ではこれが一番全体の把握がしやすく、多くの人が特に追加費用無しでアクセス可能であり、独自カスタマイズを許容できるツールになるからです。
セルに記述するというのも、アイテムを追加する人の心理的障壁を下げていますし、途中を許容しつつ議論ができるのも利点だと思うので、 特に強いこだわりがないのであれば、現状はスプレッドシート一択で良いのではないでしょうか。

PBL項目

スプレッドシートの各項目は以下のような用途を想定しています。

項目 用途
No 通番。優先度で前後させても通番を振り直しはしない
エリア 開発範囲を示すときに使う。LeSS Hugeのエリアに相当
ステータス プロダクトバックログアイテムの状態を表す(詳細は後述)
誰が 誰のための価値になるものを作るのかを明示する。ユーザストーリのWho
何をしたい どういったことを実現するのか。ユーザストーリのWhat
それはなぜか(価値) どういう価値が提供されるのか(リリース後に検証する内容)を書く。ユーザストーリのWhy
ポイント ストーリポイントを書く
レビューチェック内容 このアイテムでどういったことを実現するのかを具体的に書く。スプリントレビューの受け入れ基準に相当(How)
備考 SKIPにした理由や、アイテム分割した際の情報などを書く

PBLに携わる人数が多ければ、起票者の項目もあると便利です

スクラムでのPBL運用フロー

ではこのPBLをスクラムのイベントに沿って運用するフローを提示してみたいと思います。

1. プロダクトバックログリファインメントを実施

  1. 各自がプロダクトバックログにTODOでアイテムを追加する(事前記入だったり、時間を取ってやったりする)
  2. プロダクトバックログリファインメントを実施し、アイテムの上の方から内容を確認していく
    1. TODOを追加した人から内容の説明を受け、チーム内のゴール認識を合わせる(レビューチェック内容を決める)
    2. アイテムのストーリーポイントを見積もる
    3. アイテムの状況に合わせて、ステータスを変更する
  3. アイテムの粒度が大きすぎたり、不明瞭な場合は、実現可能な粒度にアイテムを分割して、再度内容を精査する

ステータスは以下のような意味合いで利用します。

ステータス 意味
TODO アイテムを起票した状態で、まだチームへの展開が行われていない
READY 展開された内容が、現時点で着手可能
NOT READY 展開された内容が、まだ着手不可能
DOING スプリントで実施中
DONE アイテムが完了している
WAIT 現状議論する段階にない(議論できない)
PENDING どうするかを保留した
SKIP 一旦実施しないこととした

2. スプリントプランニングを実施

PBL上位のアイテムでステータスがREADYとなっているものからスプリントで実施するアイテムを決め、そのアイテムのステータスをDOINGにする。

選んだアイテムはスプリントバックログ(ホワイトボードを使うのがお奨め)に移動し、タスクを詳細化・細分化する。

3. スプリントレビューを実施

スプリントで作成した成果(プロダクトインクリメント)を、PBLのレビューチェック内容と照らし合わせながらレビューする。

実現できていればステータスはDONEにし、追加で実現したいことが見つかったならPBLに新しいアイテムをTODOで追加する。

4. 繰り返し

これらを繰り返していく。

まとめ

個人的には、価値を生み出す作業にどれだけ注力できるかが、アジャイル開発の目指すところかなと思っているので、それ以外の部分については可能な限り力をかけずに済ませるようにしたいです。

ちょっとでもPBLのフォーマットに迷うくらいであれば、とりあえずこのフォーマットに書き殴ってみて、後で適当なものに切り替えてみるとかはどうでしょうか。

【MySQL 8向け】MyBatis Migrationsのコンテナ化

JVM系のマイグレーションツールの導入

新しくSpring Bootの開発環境をコンテナ上で作る上で、DBマイグレーションツールのMyBatis Migrationsをコンテナで使えるようしたので、その手順をまとめておきます。

f:id:seri_wb:20190605223604p:plain

Docker Composeの構成

作っていた開発環境のマイグレーション関連部分に絞ってディレクトリ構成を表すと、以下のようになっています。

$ tree -d
.
├── sample
│   └── migration
│       ├── drivers
│       └── sample
│           ├── environments
│           └── scripts
└── mysql
    ├── conf.d
    ├── data
    ├── log
    ├── sql
    └── tmp
        └── data

プロジェクトルートにdocker-compose.ymlを置き、それ起因でコンテナを運用するようにしています。

マイグレーション自体はdocker composeを持つリポジトリとは別で管理しているので、 アプリケーション名のディレクトリ(ここではsample)配下に、migrationディレクトリとしてgit cloneして利用するイメージです。

  • docker-compose.yml
version: '3.7'
services:
  migration:
    build:
      context: ./sample/migration/
    image: sample-migration
    container_name: sample_migration
    command: /bin/bash
    tty: true
    working_dir: "/sample/migration"
    volumes:
      - ./sample/migration:/sample/migration:cached
    depends_on:
      - db

  db:
    image: mysql:8
    container_name: sample_db
    environment:
      MYSQL_ROOT_PASSWORD: "root"
      #MYSQL_DATABASE: "sample"
      MYSQL_USER: "admin"
      MYSQL_PASSWORD: "admin"
    volumes:
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - ./mysql/conf.d:/etc/mysql/conf.d
      - ./mysql/data:/var/lib/mysql
      - ./mysql/tmp/data:/tmp/data
      - ./mysql/log:/var/log/mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"

このdocker-compose.ymlでは、MySQL 8のコンテナを立ち上げ、その後マイグレーション用のコンテナを立ち上げています。

MySQLのコンテナイメージ

MySQLのコンテナイメージでは、MYSQL_DATABASEを指定することで、予めDBを作成することもできるのですが、 DBは複数利用することを考え、コンテナイメージ作成時に実行されるinit.sqlで作成するようにしています。

CREATE DATABASE sample CHARACTER SET utf8mb4;
GRANT ALL ON *.* TO 'admin'@'%' WITH GRANT OPTION;
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'admin';
CREATE USER 'admin'@'127.0.0.1' IDENTIFIED BY 'admin';
FLUSH PRIVILEGES;
MyBatis Migrationsのコンテナイメージ

本題のMyBatis Migrationsのコンテナイメージは、OpenJDKのイメージをベースに作成します。

  • sample/migration/Dockerfile
FROM openjdk:11-slim

RUN apt-get update && apt-get install -y \
        wget \
        unzip \
    && apt-get clean \
    && rm -rf /var/lib/apt

RUN wget https://github.com/mybatis/migrations/releases/download/mybatis-migrations-3.3.5/mybatis-migrations-3.3.5-bundle.zip \
    && unzip mybatis-migrations-3.3.5-bundle.zip -d /usr/local \
    && rm mybatis-migrations-3.3.5-bundle.zip

ENV MYBATIS_MIGRATIONS_HOME /usr/local/mybatis-migrations-3.3.5
ENV PATH $PATH:$MYBATIS_MIGRATIONS_HOME/bin

WORKDIR /sample/migration

COPY . .
CMD ["/bin/bash"]

docker-compose up -d後(コンテナイメージ起動後)、コンテナに入ってマイグレーションスクリプトのテンプレートを作成します。

mkdir sample
cd sample
migrate init

この際、コマンド実行ディレクトリにdriversディレクトリが作成されますが、複数DBのスクリプト管理をすることを考え、WORKDIR配下にdriversを移動させます。

実際に利用するMySQLJDBCコネクタは、以下からダウンロードし、先ほどのディレクトリに配置してください。

その後、プロパティの以下を生成された値から変更します。

  • time_zoneをAsia/Tokyoに
  • script_char_setをアンコメント
  • JDBCに必要な設定を追加(driverのクラスに注意)
  • send_full_scriptをアンコメントしてfalseに
  • driver_pathをJDBCコネクタを配置したコンテナ内の絶対パス

  • sample/migration/sample/environments/development.properties

## Base time zone to ensure times are consistent across machines
time_zone=Asia/Tokyo

## The character set that scripts are encoded with
script_char_set=UTF-8

## JDBC connection properties.
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://db:3306/sample
username=admin
password=admin

#
# A NOTE ON STORED PROCEDURES AND DELIMITERS
#
# Stored procedures and functions commonly have nested delimiters
# that conflict with the schema migration parsing.  If you tend
# to use procs, functions, triggers or anything that could create
# this situation, then you may want to experiment with
# send_full_script=true (preferred), or if you can't use
# send_full_script, then you may have to resort to a full
# line delimiter such as "GO" or "/" or "!RUN!".
#
# Also play with the autocommit settings, as some drivers
# or databases don't support creating procs, functions or
# even tables in a transaction, and others require it.
#

# This ignores the line delimiters and
# simply sends the entire script at once.
# Use with JDBC drivers that can accept large
# blocks of delimited text at once.
send_full_script=false

# This controls how statements are delimited.
# By default statements are delimited by an
# end of line semicolon.  Some databases may
# (e.g. MS SQL Server) may require a full line
# delimiter such as GO.
# These are ignored if send_full_script is true.
delimiter=;
full_line_delimiter=false

# If set to true, each statement is isolated
# in its own transaction.  Otherwise the entire
# script is executed in one transaction.
# Few databases should need this set to true,
# but some do.
auto_commit=false

# If set to false, warnings from the database will interrupt migrations.
ignore_warnings=true

# Custom driver path to allow you to centralize your driver files
# Default requires the drivers to be in the drivers directory of your
# initialized migration directory (created with "migrate init")
driver_path=/sample/migration/drivers

# Name of the table that tracks changes to the database
changelog=CHANGELOG

# Migrations support variable substitutions in the form of ${variable}
# in the migration scripts.  All of the above properties will be ignored though,
# with the exception of changelog.
# Example: The following would be referenced in a migration file as ${ip_address}
# ip_address=192.168.0.1

これで準備完了です。

使い方

実際に利用する際は、initしたときのように起動後にコンテナ内に入ってmigrate upを実行でもいいですが、以下のように直接実行することもできます。

$ docker-compose up -d
$ docker-compose exec migration bash -c "cd sample && migrate up"

初回の起動スクリプトなどを組む場合は、こちらの使い方が便利です。

手順は以上です。よいコンテナライフを!