Gitサーバーを Amazon Linux 2023 へ移行した話

目次

こんにちは。ヌーラボの伊藤です。

私たち Backlog Git チームは、Amazon Linux 2(以下 AL2)の EoL に際し、Git サーバーを Amazon Linux 2023(以下 AL2023)へ移行しました。移行を進める上で意識したこととハマったことをまとめたいと思います。

移行対象

移行対象の Git サーバーはリポジトリという「状態」を EBS ボリュームに抱えています。そのため、Web サーバーのように「新しいインスタンスを立てて向き先を切り替える」だけでは済まず、状態も含めて移行した上で通信を切り替える必要があります。

また、Git サーバーはプライマリとレプリカの冗長構成をとっており、複数の環境で運用されているため、全体では十数台のサーバー移行作業となります。計画時点から、長期にわたる作業になることが予想されていました。

移行手順

検討を重ね、次の流れで移行を進めることにしました。

  1. 新サーバー(AL2023 インスタンス)を立ち上げる(ミドルウェアとアプリケーションが動作する環境を用意する)
  2. レプリカへの通信とレプリケートを止める
  3. EBS を移行する
    1. 現行サーバーの EBS スナップショットを作成する
    2. スナップショットの Fast Snapshot Restore を有効化する
    3. スナップショットからボリュームを復元する
    4. 復元したボリュームを新サーバーへマウントする
  4. レプリカの向き先を新サーバーに切り替える
  5. レプリカへの通信とレプリケートを再開する
  6. レプリカの動作確認をする
  7. レプリカとプライマリを入れ替える
  8. プライマリの動作確認をする

意識したポイント

手順をつくる上で意識したのは、できるだけシンプルにすることです。ポイントは次の三つです。

問題が起きたときの戻り先がすぐにわかる

手順を考える上で、問題を見つけたときにどこへ戻ればよいかがすぐにわかることを意識しました。今回の手順でいうと、「6. レプリカの動作確認」で問題を見つけた場合は、「5. レプリカへの通信とレプリケートを再開する」の前まで戻せば、問題を抱えたまま稼働することを避けられます。

手順内に「この場合は、これをする」という分岐が多くなると手順が複雑になり、いざ問題と直面したときに判断が遅れる懸念がありました。「何かあったら状態を一つ前に戻せばいい」というシンプルな考え方を基本方針にすることで、作業者のメモリを空けた状態にしておくことが結果的に安全な作業につながると考えました。

手順書にマジックナンバーを残さない

手順書へ落とし込む際には、ID や IP アドレスといったマジックナンバーを記載しないようにしました。例えば「4. レプリカの向き先を新サーバーに切り替える」では、次の aws コマンドを実行しています。

aws route53 change-resource-record-sets \
  --hosted-zone-id "$zone_id" \
  --change-batch "{
    \"Changes\": [{
      \"Action\": \"UPSERT\",
      \"ResourceRecordSet\": {
        \"Name\": \"${fqdn}\",
        \"Type\": \"A\",
        \"ResourceRecords\": [{\"Value\": \"${new_ip}\"}]
      }
    }]
  }"

これをそのまま手順書に載せるのではなく、ドメイン名やサーバー名を引数に取るスクリプトにし、ID や IP アドレスはスクリプトの中で解決する形にしました。

update-dns-record.sh <fqdn> <server-name>

他の手順も同様に、対象の環境やサーバー名を指定してスクリプトやジョブを実行すれば済む形になっています。スクリプトやジョブの名称からどんな操作をするのか、与えるパラメーターから何を対象にしているかが明示的になり、仮に手順書に間違いがあった場合に気づきやすくなります。また、別環境の手順書を作る際にも、移行完了した環境の手順書を複製し環境名と移行元/移行先のサーバー名を置換するだけで済むので、手間が減ります。

一連の作業を一つのアクションにまとめる

作業が細切れにならないように、一連で実行できる作業はできるだけ一つのアクションで実施できるようにしました。例えば「3. EBS を移行する」のうち、スナップショットの作成からボリュームの復元までは、次の三つのコマンドを実行する必要があります。

  • aws ec2 create-snapshot --volume-id $volume_id
  • aws ec2 enable-fast-snapshot-restores --availability-zones $az --source-snapshot-ids $snapshot_id
  • aws ec2 create-volume --availability-zone $az --snapshot-id $snapshot_id

これらのコマンドは完了までにそれぞれ数時間かかるため、三つをスクリプトにまとめ、一連で流せるようにしました。このようにした意図は、一つ一つの作業に待ちが長くなると再開時にどこまで進めていたかがわからなくなるリスクを避けるためです。結果的に、夜間にスクリプトを流して翌日に次の作業へとりかかる、という形で効率化にも繋がりました。

実際に進めてみてどうだったか

検討を重ねたおかげで、大きな問題を発生させることなく移行を進めることができました。また、作業者が進めやすいように手順をできるだけシンプルになるよう工夫したことが、数ヵ月にわたる移行の後半フェーズで特に効きました。手順が正確であることはもちろん、作業者にとって読みやすく扱いやすい状態となっていることもまた重要だと感じました。

想定できていなかったこと

手順そのものは想定通りに進めることができましたが、いざやってみると細かなところで想定外がありました。私がハマった落とし穴をいくつかピックアップしてお話します。

足りなくなっていた権限

スナップショットからボリュームを作成しようとすると次のエラーで失敗しました。

aws: [ERROR]: An error occurred (UnauthorizedOperation) when calling the CreateVolume operation: You are not authorized to perform this operation.

権限がないとのことですが、この操作は過去の移行手順でも実施されているものでした。「ここでエラーになるはずないのにな…」と思って調べると、原因は AWS のアップデートにありました。

以前は CreateVolume アクションの許可があればスナップショットから EBS ボリュームを作成できましたが、アップデートによりスナップショットのリソースステートメントも明示的に指定する必要が生じていました。IAMポリシーの Resources へ arn:aws:ec2:*::snapshot/* を追加することで権限エラーは発生しなくなりました。

これ自体はすぐに解決できるものでした。ただ、この一件で「過去にできていたから、今回もできるはず」という思い込みを自覚するとともに、この考え方は危険だと感じました。移行作業は頻繁に行うものではないので、前にやったことがあるというのは数年前の話というのも珍しくありません。「しばらく実施していない操作だからこそ、今回は大丈夫だろうか?」という視点をもつべきでした。

忘れ去られた設定

EBSボリュームを再マウントするために一度サーバーを再起動したところ、直前まで普通にできていた SSH 接続が、突然できなくなりました。

原因は、Ansible に残っていた「sshd のポートを 10022 番に変更する」という設定でした。厄介だったのは発動のタイミングです。Ansible でプロビジョニングした直後はこの設定がまだ有効になっておらず、サーバーには接続できていました。ところがサーバー再起動で sshd が再起動され、それを引き金に設定が発動し、SSH 接続できなくなりました。

そもそもこの設定は何のためにあるのでしょうか。チームメンバーに聞いても、はっきりしたことはわかりません。git のコミットメッセージを元に歴史を辿ると、理由が見えてきました。

  • かつて 22 番ポートは別サービスのために使われていた
  • そのため、運用者が使う管理用の sshd を、22 番から 10022 番へ「逃がして」いた
  • 現在は別サービスが分離されているため 22 番のままで問題ない

結論として、かつての運用の名残で、現状では意味のある設定ではありませんでした。対処としては設定を消すだけです。ただ、この設定が入ったのが何年も前だったこともあり、「消しても大丈夫」という確信を得るまでに時間がかかりました。

不要になった時点で設定を消すことを徹底できれば理想ですが、長く続いているプロジェクトではこうした取りこぼしは発生してしまうものだと思います。移行時にまとめて見つかることがないように、日々のプロジェクト活動の中で、形骸化した設定をクリーンアップする機会を持てるとよいと感じました。

思わぬ依存関係

新サーバーへの移行後、問題が起きることなく日中の稼働が続き、安心した矢先のことです。夜間に定期実行しているメンテナンス用のスクリプトがエラーを吐きました。

調べてみると、原因は OS 移行に伴う Perl のバージョンアップでした。システム標準の Perl が 5.16 から 5.32 系に上がったことで、autoderef が動作しなくなっていたのです。autoderef は、リファレンスを明示的にデリファレンスせずに each や push へ渡せる書き方で、Perl 5.24 で削除されています。長く 5.16 で動いてきたメンテナンススクリプトはこれに依存していたため、次のように書き換える必要がありました。

# Before(autoderef、5.24 で削除済み)
each($ref);
push(${$results}{key}, $item);

# After(明示的なデリファレンス)
each(%$ref);
push(@{${$results}{key}}, $item);

こんなところにも移行の影響が出るのかと、あらためて作業の影響の大きさを再認識しました。

移行後に溢れ出した swap

もう一つ、思わぬところに影響が現れました。移行後のサーバーで swap が溢れ、アラートが頻繁に発生するようになったのです。何らかの移行ミスでアプリの挙動を変えてしまったのかと焦りましたが、そうではありませんでした。

原因は、カーネルのバージョンアップによるメモリ回収の挙動が変わったことによる影響でした。今回の移行でkernel のバージョンは 4.14 から 6.1 へ上がり、この間の kernel 5.8 で、メモリ回収のバランスを決めるロジックが変更されました(mm: balance LRU lists based on relative thrashing)。それにより、swap をほぼ使わない想定で swappiness=1 に設定したままでも、長くアクセスされていないヒープ領域が swap へ退避されるようになり、小さめに確保していた swap 領域が数日で満杯になるという状況に陥っていました。swapのサイズを実態に合わせて変更するとともに、アラートの閾値を見直すことで解決しました。

OS 移行ではパッケージやミドルウェアの互換性ばかりに目が行きがちですが、こうしたカーネルのポリシー変更も一緒についてきます。監視の閾値は旧環境の挙動を前提にしていることが多いため、移行の動作確認にはアラート定義の見直しまで含めておくべきだった、というのがこの一件の反省です。

おわりに

このブログではトピックを絞りましたが、他にも小さなつまずきがたくさんありました。ざっと挙げるだけでも、こんな具合です。

  • qmail が新しい OpenSSL と非互換でビルドできず、Postfix へ移行した
  • ログ収集エージェント(td-agent 系)が AL2023 非対応で、Fluentd / Fluent Bit へ載せ替えた
  • AL2 の AMI に同梱されていた start_server が素の AL2023 には存在せず、サービスが起動しなかった
  • カーネルの更新で nobarrier マウントオプションが使えなくなっていた
  • 構成管理に書かれたバージョン指定が実態と乖離しており、再構築を機に古いバイナリがデプロイされてワーカーが動かなくなった
  • ホスト単位で必要な SQS キューの追加や、公開している Webhook 送信元 IP リストの更新とアナウンスといった、新ホストを迎えるための足回りの作業

今回計画をできるだけ詰めて移行を進めましたが、それでも想定外はありました。直面した問題は、インフラ起因のもの・アプリ起因のもの・設定のズレなど様々で、いま振り返っても事前にすべてを想定しておくのは難しいと感じます。想定外はあるものと許容したうえで、リスクを最小限に留められるように進めていくのが大切であり、現実的なゴールではないかと思います。

以上、最後まで読んでいただきありがとうございました。

「ブログリレー」の関連記事

ブログ一覧へ