Perlのレガシーシステムを少し更新した話

こちらは [Perl Advent Calendar 2023](https://qiita.com/advent-calendar/2023/perl) の17日目の記事になります。

最近はあまりコードは触れていないのですが、
半年前くらいにやっていた、Perlのレガシーな某サービスを少しだけ更新した話があったのでそのあたりの話を軽くさせていただきます。

---

# 主なバージョンアップ内容

このシステムは、2015年に大幅に刷新したものでした。
当時、オンプレ内のVMで動作させていましたが、コンテナ化だけは行っていて、
その後オンプレからクラウド環境にシフトなどしながら最低限のところだけパッチ的に延命させてきました。

しかし、その後年月の経過により様々なほころびが出てきてしまっていたので、今回可能な範囲で刷新を行いました。

変更前後の主な内容は以下になります。

|  内容  |  変更前  |  変更後  |
| ---- | ---- | ---- |
|  コンテナベースイメージ  |  CentOS:7  | AlmaLinux:8  |
|  Perlバージョン  | Perl 5.20.1  | Perl 5.36.1  |
|  CPANモジュールバージョン  |  2015年前後がメイン  |  基本的に2023年最新版（例外あり）  |
|  絵文字対応  |  Unicode 6.3まで  |  Unicode 14まで  |
|  MySQLバージョン  | RDS for MySQL 5.7.41  | RDS for MySQL 8.0.33  |


それぞれのトピックを簡単に説明させていただきます。

---

# コンテナベースイメージ

元々CentOS 7をベースイメージとしていました。
2015年当時としては新しく、長年使わせてもらっていろいろありがたい感じだったのですが、
当時は Docker Compose なども無くいろいろと過渡期であったため
オンプレ時代の思想を大きく受けており、一つのコンテナに全部入りとなっていました。

例えば、Perlの公式イメージに変えるかどうかも考えたのですが、一部のミドルウェアに影響がありそうだったため、無難にRHEL系を続投する事にしました。

CentOS 8は一足先にEOLとなりましたが、
CentOS 7のEOLも 2024-06-30 と近づいており、
そろそろ乗り換えを検討しないとまずいと考え、悩んだのですが AlmaLinux 8 を選択しました。
と言ってもここについてはほとんど問題もなく、意識することは無く行けました。

---

# Perl バージョン
2015年当時の最新だったPerl 5.20.1ですが、
Perlは後方互換性などが非常に高いので、古いままでもさほど問題になっていなかったのですが、
Perl 5.26で、「`@INC`からカレントディレクトリが削除」というそこそこ大きな変更が発生したりしました。

詳しくは以下の記事を参照いただければと思います。

https://gihyo.jp/dev/serial/01/perl-hackers-hub/004601

また、以前はPerlのビルド時に特定のプロジェクトパスの `@INC` を埋め込んでいたりしたのですが、

https://songmu.jp/riji/entry/2014-02-19-carton.html

の記事を参考に、`env-exec` のラッパーシェル経由で起動させるようにして、プロジェクト毎に必要なパスを通すようにしました。

---

# CPANモジュールの刷新

CPANモジュールの更新ですが、

https://blog.sushi.money/entry/2020/07/28/221004

のようなオシャレな仕組みが構築できていればよかったのですが、
日々の運用やもろもろだけで手一杯で、現状はCPANモジュールはgit管理して必要なときだけの更新になっていました。

このため、約8年ぶりの大幅刷新となったため、なかなか相当数の更新が入りました。

## Image::Magick が入らない問題

<details><summary>Image::Magickインストールのエラーログ</summary>

```
[root@aef433e8b6e8 bl]# tail -30 /root/.cpanm/work/1684490119.32244/build.log
META.yml/json not found. Creating skeleton for it.
Configuring Image-Magick-7.1.0-0
Running Makefile.PL
Checking if your kit is complete...
Looks good
Warning (mostly harmless): No library found for -lMagickCore-7.Q16HDRI
Generating a Unix-style Makefile
Writing Makefile for Image::Magick
Writing MYMETA.yml and MYMETA.json
-> OK
Checking dependencies from MYMETA.json ...
Checking if you have parent 0 ... Yes (0.238)
Checking if you have ExtUtils::MakeMaker 0 ... Yes (7.64)
Building Image-Magick-v7.1.0
cp Magick.pm blib/lib/Image/Magick.pm
AutoSplitting blib/lib/Image/Magick.pm (blib/lib/auto/Image/Magick)
Running Mkbootstrap for Magick ()
chmod 644 "Magick.bs"
"/usr/local/perl-5.36/bin/perl" -MExtUtils::Command::MM -e 'cp_nonempty' -- Magick.bs blib/arch/auto/Image/Magick/Magick.bs 644
"/usr/local/perl-5.36/bin/perl" "/usr/local/perl-5.36/lib/5.36.1/ExtUtils/xsubpp"  -typemap '/usr/local/perl-5.36/lib/5.36.1/ExtUtils/typemap' -typemap '/root/.cpanm/work/1684490119.32244/Image-Magick-7.1.0/typemap'  Magick.xs > Magick.xsc
mv Magick.xsc Magick.c
cc -c  -I/usr/local/include/ImageMagick-7 -DMAGICKCORE_HDRI_ENABLE=1 -DMAGICKCORE_QUANTUM_DEPTH=16 -I/usr/include/libxml2 -I"/usr/include/ImageMagick-7" -fwrapv -fno-strict-aliasing -pipe -fstack-protector-strong -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -D_FORTIFY_SOURCE=2 -I/usr/include/freetype2 -g -O2 -Wall -pthread -DMAGICKCORE_HDRI_ENABLE=1 -DMAGICKCORE_QUANTUM_DEPTH=16 -O2   -DVERSION=\"7.1.0\" -DXS_VERSION=\"7.1.0\" -fPIC "-I/usr/local/perl-5.36/lib/5.36.1/x86_64-linux/CORE"  -D_LARGE_FILES=1 -DHAVE_CONFIG_H Magick.c
Magick.xs:56:10: fatal error: MagickCore/MagickCore.h: No such file or directory
 #include <MagickCore/MagickCore.h>
          ^~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:350: Magick.o] Error 1
```
</details>

ググってみると、

https://foswiki.org/Support.Question2081

の方と同じ問題のようでした。

この際 `Image::Magick` は廃止して、`Imager`, `Image::Imlib2` などの乗り換えなどの方法もあるのかもしれませんが、結構な範囲で使用していたのでそこまでの工数はかけられない、、、
ということでもう少し追ってみると、
`Image::Magick` のバージョンの最新版は7.1.0で、
AlmaLinux 8のdnfでインストールされているImageMagickは 6.9.12.86 だったようです。。

このためcartonで入れるバージョンを

```
requires 'Image::Magick', '== 6.9.12';
```

にして一旦進める事にしました。。

## Crypt::CBC をアップグレードした影響の問題

`Image::Magick` の他はさほど大きな問題も出なかったのですが、
`Crypt::CBC` で v2.33 -> v3.04 に上げたところ、一つ問題が発生しました。

`Crypt::CBC` はサーバ間で暗号化・復号化を行うものですが、このシステムとは別の基盤部分で連携していた別サーバ間との復号化部分で問題が発生していました。

別基盤のサーバの `Crypt::CBC` はまだ v2.33 を使っていたため、
アップグレードするならタイミングをあわせて行う必要があったようです。。

---

# Unicode 6.3以降の絵文字が使えない問題

こちらが今回の刷新の直接のきっかけとなった問題です。
絵文字の仕様は2015年当時に設計されていたものだったので、
当時最新のUnicode 6まで使うことだけ想定されていました。

具体的には範囲外の文字は許容しないようにしていました。

```
$_ =~ s/\p{Cn}//g;
```

https://perldoc.jp/docs/perl/5.10.1/perlunicode.pod

そのPerlのバージョンでUnicodeとして割当られていない文字は除去されるため、
Perl 5.20.1の場合は Unicode 6.3 までとなっていました。

このため、例えば以下の絵文字は入力しても許容されないようになっていました。

- Unicode 8 🤔 :thinking face:
- Unicode 13 🥲 :smiling_face_with_tear:
- Unicode 14 🫠 :melting_face:

これを、Perl 5.36.1 に上げることによって Unicode 14の絵文字まで使えるようになりました。

なお、本日時点で最新のPerl 5.38系ではUnicode 15まで使えるようになっているようです。

https://perldoc.jp/docs/perl/5.38.0/perl5380delta.pod#Unicode3215.032is32supported

https://www.unicode.org/versions/Unicode15.0.0/

このため、こちらについては後日 Perl 5.38系に上げてみようと考えています。

---

ここからはPerlのお話ではありませんが、ついでということで同時期におこなったMySQLのアップグレードのお話をｗ

# MySQL 5.7 -> 8.0 アップグレードについて
Aurora MySQLとは違い、
RDS for MySQL 5.7 は2023年10月でサポート終了になっていました。

https://www.sunnycloud.jp/column/20221118-02/

MySQL 5.7と8.0の違いは以下のあたりを見ていただくといいかとおもうのですが、

https://dev.mysql.com/doc/refman/8.0/ja/upgrading-from-previous-series.html

MySQL 8.0 への移行が完了しました 〜さようなら全ての MySQL 5.7〜

https://blog.cybozu.io/entry/2021/05/24/175000


特に、以下のあたりを読んでみると、みなさん以下の問題の対応に苦労されているようでした。

MySQLのデフォルトcollationの注意点

https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0157

MySQL 8.0 でも utf8mb4_general_ci を使い続けたい僕らは

https://mita2db.hateblo.jp/entry/2020/12/07/000000

## MySQL 5.7と8.0の主な違いについて：collationの問題

「A、a 」「びょういん、びよういん」「はは、ハハ 」「はは、ぱぱ 」「🍣、🍺」などが区別されるかどうかという問題が最大の問題でした。

当初はなるべく標準に準拠すべきかなと思い

- 5.7 `utf8mb4_general_ci`
　↓
- 8.0 `utf8mb4_0900_ai_ci`

のMySQL標準の変更を受け入れても良いんじゃないか、ということで検証環境で試してみました。

が、実際に検証してみたところ、一部機能で大きく挙動が変わってしまう箇所があり、
結局 utf8mb4_general_ci を維持するのが最も無難ということになりました。

## utf8mb4_general_ci を維持させるには

https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0157

https://blog.smartbank.co.jp/entry/2023/07/04/120000

などを読むと、 `default_collation_for_utf8mb4` というパラメータを使うと良さそうなのですが、
RDSではこれに対応していないという問題が判明しました。

https://yoneyore.hatenablog.com/entry/2020/11/26/232128

> [AmazonRDS for MySQLの場合]
> パラメータグループにdefault_collation_for_utf8mb4は存在するが、
> RDSが再起動かかるとutf8mb4_0900_ai_ciに変わってしまっている...。
> そのため、現状各テーブル、カラムに照合順序を付けてDDLを流すしかないと思われる。

再起動すると確かに変更前の値に戻ってしまっていました。。

また、素のMySQLであれば `SET PERSIST構文` というのがあるのですが、

https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0094

RDSではこれも使えない…
ということで、 `default_collation_for_utf8mb4` での対応は諦めて別の方法を探りまし
た。
DB全体での collation 対応が難しいのであれば、テーブル単位で明示的に指定するのがよさそうということで、
検証環境やデータ量が小さい場合は以下のようにテーブル単位で指定して変更、これが王道のやり方だと思われます。

```sql
mysql> ALTER TABLE xxxx CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
```

しかし、本番DBだとデータ量が大きいことと、メンテナンス時間が限られるため、
`CONVERT TO CHARACTER SET`
は現実的には使えなそうでした。

いろいろ試したところ、文字コード自体の変更には時間がかかるが、COLLATEのみの変更は高速ということが分かりました。
既に utf8mb4 になっているテーブルであれば、COLLATEのみの変更で良かったようです。
（utf8mb4化については、以前 MySQL 5.1 -> 5.7化の時にやっていました）

```sql
mysql> ALTER TABLE xxxx COLLATE utf8mb4_general_ci;
```

【2023-12-18追記】

すみません、ちょっと意図が伝わりにくい事を書いてしまっており、以前 yoku さんにアドバイスをいたいておりましたので追記させていただきます。

https://twitter.com/yoku0825/status/1697217634952192290

> ALTER TABLE table_name COLLATE =..だと、「今後COLLATIONを指定しなかった時にデフォルトで割り当てられるCOLLATION」を変えて、「既にあるカラムのCOLLATIONは変わらない」

となります。
自分の意図は、今後追加されるカラムのデフォルト値を `utf8mb4_general_ci` にしたいという事でした。
既にあるカラムのCOLLATIONは変わらないので注意が必要です。
今回の場合は、事前の検証で `utf8mb4_general_ci` がアプリケーションに影響するテーブルとカラム単位までの絞り込みが出来ていたため、それ以外の本件を忘れた頃に今後誰かがカラムを追加した際に、COLLATION指定を意図せず `utf8mb4_0900_ai_ci` になってしまわないことを行いたかったものとなります。
`default_collation_for_utf8mb4` が使えなくても、システム全体としてCOLLATIONの一貫性を保ちたかったものとなっております。

## 昔のテーブルに 0000-00-00 が入っている問題

MySQL 8.0から、DATETIME型のZERO_DATEの扱いが更に厳格になりました。
(5.7ではWARNINGのみだったものが、エラーになってしまう。 `sql_mode` で変えることも出来ますが、できるだけ標準に合わせておきたい）

大昔に作成されたテーブルには、 `0000-00-00` などが結構入っていたので、
DATETIME型の最小値である `1000-01-01` に更新することでひとまず対応しました。

`0000-00-00` のレコードが入っているテーブルであっても、COLLATE変更だけであれば変更が可能となっていますが、

```sql
mysql> select count(*) from xxxx where createstamp < '0000-00-00';
ERROR 1525 (HY000): Incorrect DATETIME value: '0000-00-00'
mysql> select count(*) from xxxx where createstamp < '0000-01-01';
+----------+
| count(*) |
+----------+
| 12345678 |
+----------+
1 row in set (0.00 sec)

mysql> alter table xxxx COLLATE=utf8mb4_general_ci;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
```

忘れた頃（将来的）にALTER TABLEしようとすると ZERO_DATE で弾かれるようになるため、
`0000-00-00` のレコードはこのタイミングで修正して撲滅するようにしました。

---

# まとめ
駆け足になってしまいましたが、
8年ぶりに老朽化していたPerlのレガシーシステムを若干ですが刷新しました。

やはり放置期間が長くなるとその分影響範囲も大きくなってくるので、（工数が取れるのであれば）3〜4年に一回くらいは式年遷宮的なものは行ったほうが良さそうです。

Perlのバージョンについては、さすがの後方互換性というか、8年ぶりのメジャーアップグレードであってもほとんど大きな問題はなく、内部のコードはほとんどいじらずとも最低限だけでいい感じになってくれました。
当初は意識していなかった、省メモリ化・パフォーマンスの向上などのメリットもあり、とりあえずなんとかなってよかったなぁ、と。。

もし同じような境遇の方がいらっしゃいましたら、どのように進めていったか、どんな問題が発生したかなどを教えていただけると助かります🙇‍♂️

---

【2024-01-14追記】
この記事の追記を

https://qiita.com/teckl/items/821c1e4b4fb4d4519950

に書きました。
もしご興味のある方いらっしゃいましたらご参照ください。