ローカルネットワークのサーバを置き換えるんだけど、古いRailsアプリはどうすりゃいいんだ……

置き換え用にと新しく買ったサーバ用の PC が明日届く、とメールがきた。まあ、届いたからって平日なのですぐに作業できるわけでもないんだけど……

それはさておき。現状でローカルネットワークで運用してるサーバ3台を順繰りに押し出すように置き換えていって、最後に押し出される一番古い PC は万が一のときの予備にまわす。この際だからサーバは全部 Ubuntu Server 20.04 に統一しようと、すでにインストールメディアの準備は済んでいる。

で、さて。その一番古いサーバでもいくつかの web アプリを動かしているんだけど、そのひとつがだいぶ前に Ruby on Rails で作ったものなんだ。なんと Rails 4.1.4。ちなみに OS やなんかのバージョンは次の通り:

  • Ubuntu 16.04
  • Ruby 2.3.1
  • Ruby on Rails 4.1.4
  • unicorn 5.4.1

これを最新の Ubuntu 20.04 の環境に置き換えようとしてるんだけど……

とにかく、開発用マシン(Ubuntu 20.04,Ruby 2.7.1)で動かせるかどうか試してみたら、案の定ダメだった。アプリを動かすどころか bundle install の途中でコケる。どうも json とか therubyracer の gem をインストールするところでうまく行かないみたいだ。なら DockerHub にある Ubuntu 公式イメージの一番古いバージョン 16.04 の上でならどうか、と試してみたけどやっぱりダメ。

新しい OS のサーバに移行するのに合わせてアプリもバージョンアップしなきゃな、とは思ってたんだけど、開発環境で動作させられないんじゃそれも難しい。どうすりゃいいんだか…… 

USBメモリにUbuntuのインストールメディアを作る

CentOS 8 の開発終了のニュースを受けて、ローカルネットワークのサーバ OS を置き換える計画をしてる。Ubuntu をインストールして使ってるサーバもバージョンが 16.04 で EOL が今年の4月と迫っている。そういうわけなので、この際まとめて Ubuntu 20.04 LTS に置き換えようと考えた。

今日はその準備としてインストール用メディアを作る。これまではダウンロードした ISO イメージを DVD-R に焼いてたんだけど、サーバにしてる PC は DVD ドライブついてないし(インストール時には外付けドライブを繋いだ)、せっかくなので USB メモリのインストールメディアを作ってみることにした。

作業環境は次の通り:

  • 作業用マシン:Windows 10
  • インストール用USBメモリ作成:Rufus

Rufus (ルーファス) ってのは起動可能な USB フラッシュドライブを作るソフト。↓ここからダウンロードした。

同様のソフトはほかにもあるようだけど、下調べした感じではこれが一番使いやすそうだった。メニューも日本語だし。バージョンは 3.13。

USB メモリは余ってた 16GB のもの。インストール用メディアとしてはもっと容量が小さくてもよさそうだけど、余ってるんだからそれを使う。

あと、肝心の OS は、Ubuntu Server 20.04.1 LTS。https://jp.ubuntu.com/download からダウンロードした ISO イメージを用意した。

さあ、始めよう。Rufus はインストールの必要はなく、ダウンロードしたファイルをダブルクリックして実行すればいい。

書き込み先(デバイス)と書き込む ISO イメージ(ブートの種類)の設定をしたところがこの画面。デバイスが「回復(G:) [16 GB]」ってなってるのは Windows の回復ドライブに使ってた USB メモリだから。ISO イメージは「選択」ボタンをクリックしてファイルを選択した。他はデフォルトのまま。

これで「スタート」をクリックすると

と表示されて追加のファイルをダウンロードするらしい。「はい」をクリック。

これもこのまま「OK」。

最終確認。問題なければ「OK」をクリックして書き込み開始。2分ほどで終わって↓の画面になる。

状態が「準備完了」になってるけど、これで書き込み終了してる。

試しに作ったばかりの USB メディアから PC を起動してみたら、ちゃんと Ubuntu のインストーラが起動した。

というわけで今日の任務完了。

Docker上のMediaWikiにファイルをアップロードする

公式の Docker イメージを使って立てた MediaWiki だけど、ファイルをアップロードするにはやっぱりひと手間必要だった。なので、そのメモ。

MediaWiki にはファイルをアップロードする機能があるけど、デフォルトでは無効になっている。有効にするには MediaWiki と PHP 自体の設定ファイルを修正する必要がある。

  • LocalSettings.php – MediaWiki の設定ファイル
  • php.ini – PHP 自体の設定ファイル

どこをどう修正すればいいかはマニュアルに書いてある。

LocalSettings.php

LocalSettings.php ファイルは、MediaWiki をセットアップしたときにホストにダウンロードして、Docker コンテナには volume としてマウントしてあるので、ホスト側のファイルを編集すればいい。

$wgEnableUploads = true;

$wgEnableUploads に true を設定。

php.ini

で、問題はこっち。php.ini ファイルは MediaWiki の公式 Docker イメージに含まれてるものをそのまま使ったので、ホスト側にはない。なので、まずはコンテナの中で編集して、動作が変わるかどうか確認することにした。

takatoh@wplj $ docker exec -it wiki bash
root@bd8268684983:/var/www/html# php -i | grep php.ini
Configuration File (php.ini) Path => /usr/local/etc/php

php.ini ファイルの場所は php -i コマンドで確認できる(と MediaWiki のマニュアルに書いてある)。このコマンドの出力は結構な量を吐くので grep で php.ini にヒットする行だけ抜き出した。ともあれ、/usr/local/etc/php にあることがわかった。

ところが、そこに php.ini ファイルはなかった。

root@bd8268684983:/var/www/html# cd /usr/local/etc/php
root@bd8268684983:/usr/local/etc/php# ls
conf.d  php.ini-development  php.ini-production

そういうものなのかと疑問に思いながらも、ひとまずは php.ini-production ファイルの中身を見てみようとしても less コマンドがない。

root@bd8268684983:/usr/local/etc/php# less php.ini-production
bash: less: command not found

当然のように vim もない。cat はあったので中身は見れたけど編集はできない。

といわけで、方針を変更してファイルをホスト側にコピーして編集し、LocalSettings.php ファイルと同様にコンテナにマウントすることにした。ファイルをホスト側にコピーするのは次の通り:

takatoh@wplj $ docker exec -it wiki cat /usr/local/etc/php/php.ini-production > php.ini

cat コマンドをコンテナ側で実行して、ホスト側の php.ini ファイルへリダイレクトしている。

で、その php.ini ファイルを編集。3行続けて載せたけど、ファイル中では別々のところにある。

file_uploads = On
post_max_size = 16M
upload_max_filesize = 8M

file_uploads は元から On になってた。post_max_size と upload_max_filesize はそれぞれ 8M2M だったのを大きくした。

docker-compose.yml

編集した php.ini ファイルを LocalSettings.php ファイルと同じディレクトリに配置したら、コンテナにマウントすべく docker-compose.yml を修正。

  wiki:
    image: mediawiki:1.35.0
    container_name: wiki
    restart: always
    depends_on:
      - mysql
    volumes:
      - /home/takatoh/var/wiki/images:/var/www/html/images
      - /home/takatoh/var/wiki/LocalSettings.php:/var/www/html/LocalSettings.php
      - /home/takatoh/var/wiki/php.ini:/usr/local/etc/php/php.ini
    ports:
      - 9090:80

これでOK。

最後にコンテナを起動しなおしたら、無事、ファイルをアップロードできるようになった。

JavaScript: アロー関数で再帰

あけおめ、ことよろ。

正月早々、奇妙なものを見つけた。元記事は去年(というか昨日)のもので、タイトルの通りなんだけど、こんなのだった。

> const fibonacci = ((fb = n => n > 1 ? n * fb(n - 1) : 1) => fb)();

どう見ても階乗を求める関数なのに名前が fibonacci っておかしいだろ、というのは置いといて。確かに期待通りに動作する。

> fibonacci(5);
120 

元記事の説明によると:

  • アロー関数 n => n > 1 ? n * fb(n - 1) : 1 を変数 fb に代入し
  • その代入した関数を即時関数によって関数 fb として返すように
  • 代入(fibonacci)する

となってて、まあその通りではある。でも、その即時関数っていうのが単に引数をそのまま返してるだけなんだから、それ、いらねんじゃね?

というわけで即時関数なしでやってみると、やっぱり、ちゃんと期待通りに動作するじゃん。

> const fact = n => n > 1 ? n * fact(n - 1) : 1;
undefined
> fact(5);
120 

と、ここまで書いて気がついた。上では即時関数が云々と書いたけど、要するにその即時関数の引数であるアロー関数を変数 fb に代入してそれを呼び出してるだけだ。即時関数は何の関係もない。元記事の筆者がどこでネタを仕入れたのか知らないけど、不必要にトリッキーなだけだ。それとも以前のバージョンの JavaScript ではこう書かなきゃできなかったのかな。

ちなみに確認したのは Node v14.15.3。

takatoh@montana: takatoh > node -v
v14.15.3

というわけで、元記事から得たものといえば、関数呼び出しの引数部分に代入式が書けることか。使うかな、これ。

あ、あと、ブログのネタにはなった。

ReactのuseState、useEffectにハマる

……いや、React というより Fetch API のせいなのかもしれない。

ここのところで React で web アプリを作ってる。例によって自分で使うためだけのもの。

JSON を返す API を2つバックエンドに置いて、両方から取ってきたデータを合わせて表示する Collections コンポーネントを書いてたんだけど、いい書き方がわからずにハマった。

Collections コンポーネントは次のように動作する(のを期待している)。

  • 一方のAPIからコレクションのリストを取得する
  • リスト中のコレクションそれぞれについて、もう一方のAPIから詳細を取得する
  • 2つのAPIから取得したデータを合わせて、コレクションのリストとしてテーブル表示する

コレクションというのは、プログラミングにおけるデータ構造のことじゃなくて買い集めたコレクション(CDとかDVDみたいな)のこと。renderCollections 関数でレンダリングしているテーブルの行のうち、collection.id と collection.buy_date が一方のAPIから取得したデータ、collection.title と collection.brand がもう一方のAPIから取得したデータだ。

import React, { useState, useEffect } from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';


const Collections = (props) => {
  const perPage = 10;
  const [collections, setCollections] = useState([]);
  const [currentPage, setPage] = useState(0);

  const page = props.query.page ? parseInt(props.query.page) : 1;
  useEffect(
    () => {
      setPage(page);
      const offset = perPage * (page - 1);
      let colls = [];
      const fetchProduct = async (coll) => {
        const result = await fetch(`${props.api2Endpoint}/products/${coll.product_id}`)
          .then(response => response.json())
          .then(data => data["products"][0]);
        colls = [...colls, {...coll, title: result.title, brand: result.brand.name}];
        setCollections(colls);
      };
      const fetchData = async () => {
        const result = await fetch(`${props.api1Endpoint}/collections?limit=${perPage}&offset=${offset}`, { mode: "cors" })
          .then(response => response.json())
          .then(data => data["collections"]);
        for (const c of result) {
          fetchProduct(c);
        }
      };
      fetchData();
    },
    []
  );

  return (
    <div>
      <h2>Collections</h2>
      <TableContainer component={ Paper }>
        <Table aria-label="collection table">
          <TableHead>
            <TableRow>
              <TableCell>ID</TableCell>
              <TableCell>Title</TableCell>
              <TableCell>Brand</TableCell>
              <TableCell>Buy date</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            { renderCollections(collections) }
          </TableBody>
        </Table>
      </TableContainer>
    </div>
  );
}

const renderCollections = (collections) => {
  return (
    collections.map(collection => (
      <TableRow key={ collection.id }>
        <TableCell>{ collection.id }</TableCell>
        <TableCell>{ collection.title }</TableCell>
        <TableCell>{ collection.brand }</TableCell>
        <TableCell>{ collection.buy_date }</TableCell>
      </TableRow>
    ))
  );
}


export default Collections;

いろいろググりながらなんとか書いた上のコードで一応動くようにはなった。けど、表示されるコレクションの順番が不定になってしまう。APIからリストを取得した段階では ID 順に並んでいるのは確認したので、たぶん、個々の詳細データを取得する fetchProduct 関数内でリスト(変数 collections)を更新するタイミングのせいだ。つまり、ループの中で使ってる fetch 関数が非同期なせいだ(たぶん)。

あぁ、JavaScript ってこういうところがなんかやりづらいんだよなぁ。

なんかいい書き方はないものか。あとからソートすればいいのはわかるんだけど。

CentOS8終了

今頃気づいたんだけど、CentOS 8 が開発を終了するんだと。今月8日付のブログで発表されてる。

日本語のニュースやブログ:

今後、プロジェクトは CentOS Stream の開発に注力し、CentOS 8 は2021年末でサポートを終了とのこと。

ウチのローカルネットでは CentOS 8 のサーバを1台運用してるけど、まだ1年ちょっとしか経ってない。CentOS Stream は Red Hat Enterprise Linux(RHEL)と Fedora の中間に位置する、RHEL の開発ブランチという位置づけなので、今までの CentOS のかわりというわけにはいかない。さくらインターネットで借りてる VPS が CentOS なので慣れるためもあって運用してたけど、もう Ubuntu に統一するかな。

なお、CentOS 7 のサポートは2024年6月30日まで。

aptあるいはDockerの怪

apt コマンドのせいなのか Docker のせいなのかわからないけど、とにかく不可解な現象に遭遇したので記録しておく。

TL;DR

  • 先月から自宅のサーバで動かしている web サービスを少しずつ Docker 上に移行する作業をしている。
  • 開発用のマシン(Ubuntu 20.04 LTS)で期待通り動作する設定(Dockerfile と docker-compose.yml)ができたのでサーバ(Ubuntu 18.04 LTS)に持っていったら Docker イメージのビルドでコケた。
  • Dockerfile の中の apt update を apt-get update に変えたら通った。

開発用のマシンにて

開発用の環境は次の通り:

  • Ubuntu 20.04.1 LTS
  • Docker version 19.03.8
  • docker-compose version 1.25.0

で、期待通り動作するように書き上げた Dockerfile はこう:

FROM ubuntu:20.04

LABEL maintainer="takatoh"

RUN apt update
RUN apt install -y \
    ruby \
    ruby-dev \
    gcc \
    g++ \
    make \
&& rm -rf /var/lib/apt/lists/*

ENV GEM_HOME /usr/local/bundle
ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH
RUN gem install bundler unicorn

ADD ./files/lcstorage-2.1.0.tar.gz /usr/
WORKDIR /usr/lcstorage
RUN bundle install

CMD [ "unicorn", "-c", "/var/lcstorage/unicorn.conf" ]

Ruby で書いた web アプリを unicorn で動かしている。この Dockerfile でイメージをビルドして、ちゃんと期待通りに動作するのを確認した。

サーバにて

サーバの環境は次の通り:

  • Ubuntu 18.04.5 LTS
  • Docker version 19.03.6
  • docker-compose version 1.17.1

開発用の環境よりもバージョンが旧いといえばそのとおりだけど、OS はともかく Docker や docker-compose はそんなに旧いわけではない。実際、別の web アプリを同じようにサーバで動かしていて、それをビルドしたときには何の問題もなかった。

ところが、今回この Dockerfile をもとにサーバでイメージをビルドすると途中でエラーが発生した。どうも apt コマンドでパッケージをインストールしている途中でコケるようだ。

仕方がないので手順をひとつずつ手動でやってみることにした。ubuntu:20.04 のイメージからコンテナを起動して、apt update し、パッケージを順にひとつずつ apt install した。が、なんの問題もなくすべてのパッケージのインストールができてしまった。

どういうこと?

Dockerfile を使ってビルドしたときにはパッケージのインストールのところでエラーが出てたんだから、手動でひとつずつインストールすればどのパッケージのインストールでエラーが出るのか判断できる、と考えてた。けど、手動でやったらエラーが出ずに終わってしまった。これじゃ手がかりがない。

唯一の手がかりは Dockerfile でビルドしたときのエラーログだ。次のように出ていた。

E: Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/l/linux/linux-libc-dev_5.4.0-56.62_amd64.deb  404  Not Found [IP: 91.189.88.152 80]
 E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

http://security.ubuntu.com/ubuntu/pool/main/l/linux/linux-libc-dev_5.4.0-56.62_amd64.deb が見つからない、と言ってるけど、ブラウザで見てみると確かにこの URL が示すファイルがない。

でも、じゃあなんで開発用のマシンでは問題なかったんだ?

いろいろググってはみたものの、有力な手掛かりは見つからなかった。

と、もういちどエラーメッセージを見ると、apt-get update を実行しろみたいなことが書いてある。apt じゃなくて apt-get だ。apt コマンドは apt-get コマンドの置き換えなんだからそんなの関係あるか?と思いながら、他に手掛かりがないので Dockerfile を修正してみた。こうだ:

- RUN apt update
+ RUN apt-get update

すると、どういうわけかエラーなくビルドできてしまった。

はぁ?

結論

何が起きたのかよくわからん。いや、さっぱりわかんない。

でも、とにかく動くようになったのでひとまずは良しとする。が、やっぱり釈然としない。原因を追求してみたいけど、手に負えるかなぁ……

docker-composeでMediaWikiを動かす

ローカルネットワークで wiki を運用してるんだけど、Docker 上に移行すべく、今日はそのテスト。

環境

  • Ubuntu 20.04
  • Docker 19.03.8
  • docker-compose 1.25.0

Dockerイメージ

MediaWiki も MariaDB も Docker Hub に公式イメージが有るのでそれを使わせてもらう。データベースは、はじめは MySQL を試したんだけどうまく動かなかった(原因不明)ので MariaDB に変えた。

  • mediawiki:1.35.0
  • mariadb:10.5.6-focal

ディレクトリ構成とファイル

用意した構成はこんなの:

takatoh@apostrophe:testwiki$ tree .
.
├── docker-compose.yml
├── mysql
│   └── db
└── wiki
    └── images

docker-compose.yml はこう:

version: '3'

services:
  testwiki:
    container_name: testwiki
    image: mediawiki:1.35.0
    restart: always
    ports:
      - 8888:80
    volumes:
      - ./wiki/images:/var/www/html/images
#      - ./wiki/LocalSettings.php:/var/www/html/LocalSettings.php

  mysql:
    container_name: db
    image: mariadb:10.5.6-focal
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpasswd
      MYSQL_DATABASE: testwiki
      MYSQL_USER: mysqluser
      MYSQL_PASSWORD: mypassword
    ports:
      - 3306:3306
    volumes:
      - ./mysql/db:/var/lib/mysql

コメントアウトしてある行は MediaWiki の設定ファイル。これはインストールが済んでから使う。

コンテナの起動とMediaWikiのインストール

docker-compose コマンドでコンテナを起動する。

takatoh@apostrophe:testwiki$ docker-compose up -d

ブラウザで http://localhost:8888/ にアクセスして MediaWiki のインストール(というかセットアップというか)をする。その際、データベース関係は docker-compose.yml の記述に合わせる。

  • データベースのホスト:db
  • データベース名:testwiki
  • インストールで使用する利用者アカウント→データベースのユーザ名:mysqluser
  • インストールで使用する利用者アカウント→データベースのパスワード:mypassword

その他はそれなりに設定すればいい。最後に LocalSettings.php ファイル(設定ファイル)をダウンロードしてインストールは終わり。

設定の反映

いったんコンテナを止める。

takatoh@apostrophe:testwiki$ docker-compose down

ダウンロードした設定ファイルを配置。

takatoh@apostrophe:testwiki$ cp ~/Downloads/LocalSettings.php wiki

docker-compose.yml のコメントをはずす(該当行だけ示す)。

      - ./wiki/LocalSettings.php:/var/www/html/LocalSettings.php

コンテナを再起動。

takatoh@apostrophe:testwiki$ docker-compose up -d

これで無事起動した。

参考にしたページ

[追記:2020/11/5] データベースの移行

旧い wiki からデータを移行する手順。

  • データは wiki.sql ファイルにダンプしてあるものとする
  • 旧いほうの MediaWiki のバージョンは 1.27.1

wiki.sql ファイルを Docker コンテナと共有しているディレクトリにコピーする。

takatoh@apostrophe:testwiki$ cp wiki.sql ./mysql/db

./mysql/db ディレクトリは、データベースの Docker コンテナ(コンテナ名は db)からは /var/lib/mysql として認識されている(前述の docker-compose.yml ファイルを参照)。なのでデータベースのコンテナに接続して、データを流し込む。

takatoh@apostrophe:testwiki$ docker exec -it db bash
root@007d71dfcb37:/# cd /var/lib/mysql
root@007d71dfcb37:/var/lib/mysql# ls *.sql
wiki.sql
root@007d71dfcb37:/var/lib/mysql# mysql -u mysqluser -p testwiki < wiki.sql
Enter password:
root@007d71dfcb37:/var/lib/mysql# exit
exit

これでデータベース側での作業は終了。ただ、このままだと MediaWiki でエラーになる。バージョンが上がっているので MediaWiki の使用するデータベーススキーマとかも変わっているからだ。

そこで、MediaWiki のコンテナに接続して更新スクリプトを実行する。更新スクリプトは /var/www/html/maintenance/update.php だ。

takatoh@apostrophe:testwiki$ docker exec -it testwiki bash
root@f92dd50471e5:/var/www/html# cd maintenance
root@f92dd50471e5:/var/www/html/maintenance# php update.php

これで完了。

[追記]

11/7、本番環境も無事 Docker 上に移行した。