Perl CGI on Docker and Testing it on CircleCI


はじめに

自分は長らく仕事で、PerlのWebアプリケーションのおもりをしています。
ですが、そのアプリケーションにはテストがなくて、何をするにもドキドキです…。
そもそも作り始められた当時はそのような文化はメジャーではなかったのです。

時間が経ち、世の中には便利なものがたくさん登場してきました。
今であれば、そのアプリケーションに自動テストを動かす環境を整備することもできなくはないのではないか?
そう考えました。

シンプルなPerl CGIのアプリケーションをDocker上で動かし、
CircleCIを使って、E2Eテストを動かすようにしてみます。

動かすまでが目的なので、そんなに凝ったことはしません。

ソース

この記事で使っているソースは↓です。
https://github.com/ken1flan/perl_test_on_circleci

Perl CGIのサンプルアプリケーション

サンプルのCGIは、データベースを使った簡単なものにしました。
/cgi-bin/index.cgi にアクセスすると、「ただいまのラッキーにゃんこは〜です☆」と表示します。
〜の部分にはデータベースCatsDBのCatColorsに保存されているレコードからランダムにひとつ取得したものを表示します。

構成

CGIはapache httpdのmod_cgidによって動いています。
また、データベースはMariaDBを使っています。

Dockerに載せる

Dockerfileに必要なパッケージを入れたり、設定をしたりする手順を書き、それをを元にマシンのイメージを作成します。

Perl Dockerイメージ

perl cgiを動かすコンテナのDockerイメージを作ります。
配布されている公式CentOSのイメージに、CGIを動かしていたサーバにインストールしていたパッケージを追加したり、設定ファイルをコピーしたりしています。
実際のファイルはこちらをみてください。

systemd

Webサーバを通常のサーバと同様に、systemdを使って立ち上げることにします。ですが、公式イメージをそのまま使うだけでは権限が足りずにできません。Docker-hubのCentOSのDescriptionの中程にしたがって、設定をしていきます。

ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ "/sys/fs/cgroup" ]

Dockerfileの最後で起動するようにします。

CMD ["/usr/sbin/init"]

httpd

yumパッケージのhttpdを入れ、systemdで扱えるように登録します。

RUN yum install -y httpd httpd-devel
RUN systemctl enable httpd.service

CGIのファイルをコンテナにコピーします。

COPY httpd/conf.d/01-cgi.conf /etc/httpd/conf.d

EXPOSEで、80番ポートをコンテナがリッスンできるようにします。

EXPOSE 80

Dockerイメージの作成

Dockerfileが出来上がったら、Dockerイメージを作成します。

$ docker build -t ken1flan/perl_test_on_circleci .

MariaDB

準備するのが億劫だったので、CircleCIのパッケージを利用してしまいます。
dockerhubの公式パッケージでもよさそう。

Docker Compose

複数のコンテナに分けているので、素のDockerだと起動が面倒なので、
開発時はdocker-composeで管理を簡単にできるようにします。
実際のファイルはこちらをみてください。

perlコンテナ

Dockerイメージは先の手順で作ったものを使います。

  perl:
    image: ken1flan/perl_test_on_circleci:latest

ホストの8080番へのアクセスをコンテナの80に接続します。

    ports:
      - "8080:80"

ホストのカレントディレクトリをコンテナの/codeにマウントします。
ホストのappディレクトリをコンテナの/var/wwwにマウントします。

    volumes:
      - .:/code
      - ./app:/var/www

mariadbと名前で通信できるようにしたり、mariadbコンテナから起動してからperlコンテナが起動するようにします。

    depends_on:
      - mariadb

systemctlが使えるようにします。

    privileged: true

mariadbコンテナ

Dockerイメージだけ指定しています。他の設定はしていません。

  mariadb:
    image: circleci/mariadb:10.3

起動

$ docker-compose up -d
WARNING: Found orphan containers (perl_test_on_circleci_db_1, perl_test_on_circleci_mysql_1) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up.
Starting perl_test_on_circleci_mariadb_1 ... done
Starting perl_test_on_circleci_perl_1    ... done
$
$ docker-compose ps
         Name                     Command           State          Ports
--------------------------------------------------------------------------------
perl_test_on_circleci_m   docker-entrypoint.sh      Up      3306/tcp
ariadb_1                  mysqld
perl_test_on_circleci_p   /usr/sbin/init            Up      0.0.0.0:8080->80/tcp
erl_1
$

セットアップ

Dockerイメージからコンテナを立ち上げただけでは何もデータが入っていないので、入れていきます。

Perlコンテナでbashを実行し…

$ docker-compose exec perl bash

mysqlコマンドでデータベース作成、テーブル作成、初期データ投入を行います。

# cd /code
# mysql -u root -h mariadb < createtables.sql

動作確認

下記にアクセスして無事にページが表示されればOKです。
http://localhost:8080/cgi-bin/index.cgi

E2Eテスト

ブラウザを使ってCGIの動作をテストします。
Seleniumを使えば、クロスブラウザでも可能ですが、今回はChromeを使いました。

Chromeとlibpngのインストール

PerlコンテナにChromeとスクリーンショットを取るために使うlibpngのパッケージを入れます。

RUN yum install -y libpng libpng-devel

COPY yum.repos.d/google-chrome.repo /etc/yum.repos.d
RUN yum install -y google-chrome-stable

テストスクリプト

WWW::Mechanize::Chromeを使って、http://127.0.0.1/cgi-bin/index.cgiにアクセスし、表示されている文言を確認します。
実際のファイルはこちらをみてください。

E2Eテストはブラウザを使って行いますが、WWW::Mechanize::Chromeを使っています。
このモジュールはSeleniumと違い、直接Chromeを操作するので、手軽でよいと思い、選択しました。

use Log::Log4perl qw(:easy);
use WWW::Mechanize::Chrome;

実際のページを取得する前に、起動しています。
このときに画面を使わないheadlessを指定しています。今回作ったDockerイメージはWindowマネージャのようなものが一切入れていないので、この指定がないと動きません。

my $mech = WWW::Mechanize::Chrome->new(headless=> 1);

起動したら、getにURLを指定するだけで、HTMLが取得できます。
その内容をチェックすることでテストできます。

$mech->get('http://127.0.0.1/cgi-bin/index.cgi');
my $content = $mech->content;

ok( $content =~ /ただいまのラッキーにゃんこは(茶トラ|サバトラ|キジトラ|ヒョウ柄|三毛|ブチ|サビ|シャム|黒|白|灰)です☆/ );

テストコードには入れませんでしたが、content_as_pngでスクリーンショットを保存することもできます。

my $png = $mech->content_as_png();

CircleCI

ここまでで、Docker上でCGIのE2Eテストができましたので、CircleCI上に持っていきたいと思います。

実際のファイルはこちらをみてください。

Dockerイメージの指定

CircleCIの公式Dockerイメージでは当然動かないので、自分で設定したDockerイメージを使う必要があります。
CircleCIではDocker HubにあるDockerイメージであれば利用することができるので、自前のDockerイメージをDocker Hubに登録し、それを指定します。

executors:
  my-executor:
    docker:
      - image: ken1flan/perl_test_on_circleci:latest
      - image: mariadb:10.3
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes

作業ディレクトリの指定

プロジェクトのファイルは指定しないと、/root/projectに配置されますが、その位置だとサーバの設定をいろいろ変える必要がでてきてしまいます。
開発環境と同じように/code以下にファイルを置くように.circleci/config.ymlで設定しています。

    working_directory: /code

mariadbの起動待ち

データベースの準備が整わないうちにテストが始まると、失敗してしまいます。
ですので、MariaDBの接続用ポートを監視して、通信できることを

Dockerfile中でncatをインストールしておいて…

RUN yum install -y nmap-ncat

.circleci/config.ymlで、ncatで3306ポートを監視することで、mariadbの起動を待つようにしています。

      - run:
         name: waiting for mariadb to be ready
         command: |
          for i in `seq 10`; do
            nc -z 127.0.0.1 3306 && echo Success && exit 0
            echo -n .
            sleep 1
          done
          echo Failed waiging for mariadb to be ready && exit 1

httpdの起動

      - run:
         name: setup httpd
         command: |
           rm -rf /var/www
           ln -s /code/app /var/www
           APP_ENV=test httpd

テスト

      - run:
         name: e2e_test
         command: APP_ENV=test carton exec perl test/e2e_test.pl

できた!

32bitは?

古いアプリケーションだと32bitのOS上で動いていることが多いと思います。
このことについてはまだ考察が足りていません。

最後に

なんだかんだと、ちゃんとDocker上でPerl CGIを動かし、CircleCI上でE2Eテストもできるようになりました。
これで、古いアプリケーションのメンテナンスを頼まれても、ちゃんとテストを書きながらやれますね!