AWS SDK for PHP の SQS::ReceiveMessage でシグナルを受けたときに long pooling を中断して終了したい

<?php
pcntl_async_signals(true);

$sig_handler = function ($signo) use (&$term){
    echo "signal:$signo\n";
    $term = true;
};

pcntl_signal(SIGHUP, $sig_handler);
pcntl_signal(SIGINT, $sig_handler);
pcntl_signal(SIGQUIT, $sig_handler);
pcntl_signal(SIGTERM, $sig_handler);

$sqs = new SqsClient([
    'region'  => 'ap-northeast-1',
    'version' => 'latest',
]);

while (!$term) {
    $messages = $sqs->receiveMessage([
        'QueueUrl'        => $queue,
        'WaitTimeSeconds' => 20,
    ]);

    // 何か処理する、途中でシグナルを受けても死なない
}

SQS からメッセージを受信し、何かの処理が完了するまではシグナルを受けてもプロセスを終了しないようにするためにシグナルハンドラを仕込んでいます。しかしこのコードでは WaitTimeSeconds で指定した long polling 中にもシグナルで終了しなくなるため、終了させたいときにサッと終了しません。

非同期シグナルが有効になっていればシグナルハンドラでの割り込みは利くし AWS SDK for PHP(の中の Guzzle) の中で呼ばれている curl_multi_select もシグナルで中断されるものの AWS SDK for PHP(の中の Guzzle)が自動的に curl_multi_select を再試行するため sqs::receiveMessage から抜けてこなくて、きっちり WaitTimeSeconds まで待たされます。

sqs::receiveMessage で待っているときは実質何もしていないのでシグナルを受けたらすぐにサッと終了してほしいです。終了が最大で20秒も遅延するのはちょっと長すぎます。

いくつか改善案を考えてみました。

シグナルを処理しない

PHP でシグナルを処理しようとするから終了が遅れてしまうのであってシグナルを処理しなければ即死するので、シグナルを処理しないことにします。

<?php
$sqs = new SqsClient([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
]);

for (;;) {
    $messages = $sqs->receiveMessage([
        'QueueUrl'        => $queue,
        'WaitTimeSeconds' => 20,
    ]);

    // 何か処理する、途中でシグナルを受けて死んでもいいじゃない
}

いやそれで終わりならこんな記事書かんわ。

メッセージを受信した後、何か処理しているときにはできればシグナルでは即死せず、何かの処理が終わってから死んでほしいです。

処理中はシグナルをブロックする

シグナルハンドラは仕込まず、かつ、何か処理中はシグナルをブロックします。

<?php
$sqs = new SqsClient([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
]);

for (;;) {
    $messages = $sqs->receiveMessage([
        'QueueUrl'        => $queue,
        'WaitTimeSeconds' => 20,
    ]);

    pcntl_sigprocmask(SIG_BLOCK, [SIGHUP, SIGINT, SIGQUIT, SIGTERM]);
    try {

        // 何か処理する、途中でシグナルを受けても死なない

    } finally {
        pcntl_sigprocmask(SIG_UNBLOCK, [SIGHUP, SIGINT, SIGQUIT, SIGTERM]);
    }
}

シグナルハンドラで exit する

シグナルハンドラで exit すれば普通に終了します。 何か処理中は死んでほしくないのでフラグ変数とかで分岐させます。

<?php
pcntl_async_signals(true);

$processing = false;

$sig_handler = function ($signo) use (&$term, &$processing){
    echo "signal:$signo\n";
    if (!$processing) {
        exit;
    }
    $term = true;
};

pcntl_signal(SIGHUP, $sig_handler);
pcntl_signal(SIGINT, $sig_handler);
pcntl_signal(SIGQUIT, $sig_handler);
pcntl_signal(SIGTERM, $sig_handler);

$sqs = new SqsClient([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
]);

while (!$term) {
    $messages = $sqs->receiveMessage([
        'QueueUrl'        => $queue,
        'WaitTimeSeconds' => 20,
    ]);
    try {
        $processing = true;

        // 何か処理する、途中でシグナルを受けても死なない

    } finally {
        $processing = false;
    }
}

シグナルハンドラから例外を投げる

先ほどの例とあまり変わりませんが、シグナルハンドラから例外を投げれば無理やり SQS::ReceiveMessage を抜けることができます。 なお、飛んでくる例外は AWS SDK の例外クラスでラップされているので getPrevious で剥がす必要があります。

<?php
class SignalException extends RuntimeException {}

pcntl_async_signals(true);

$processing = false;

$sig_handler = function ($signo) use (&$term, &$processing){
    echo "signal:$signo\n";
    if (!$processing) {
        throw new SignalException("signal:$signo", $signo);
    }
    $term = true;
};

pcntl_signal(SIGHUP, $sig_handler);
pcntl_signal(SIGINT, $sig_handler);
pcntl_signal(SIGQUIT, $sig_handler);
pcntl_signal(SIGTERM, $sig_handler);

$sqs = new SqsClient([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
]);

while (!$term) {
    try {
        $messages = $sqs->receiveMessage([
            'QueueUrl'        => $queue,
            'WaitTimeSeconds' => 20,
        ]);
    } catch (Throwable $ex) {
        if ($ex->getPrevious() instanceof SignalException) {
            break;
        }
        throw $ex;
    }
    try {
        $processing = true;

        // 何か処理する、途中でシグナルを受けても死なない

    } finally {
        $processing = false;
    }
}

シグナルハンドラからリクエストをキャンセルする → ダメ

ダメでした。WaitTimeSeconds の時間を待ったうえで wait から CancellationException が飛んでくるという動きになりました。

<?php
pcntl_async_signals(true);

$sig_handler = function ($signo) use (&$term, &$async){
    echo "signal:$signo\n";
    if ($async) {
        assert($async instanceof Promise);
        $async->cancel();
        $async = null;
    }
    $term = true;
};

pcntl_signal(SIGHUP, $sig_handler);
pcntl_signal(SIGINT, $sig_handler);
pcntl_signal(SIGQUIT, $sig_handler);
pcntl_signal(SIGTERM, $sig_handler);

$sqs = new SqsClient([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
]);

while (!$term) {
    $async = $sqs->receiveMessageAsync([
        'QueueUrl'        => $queue,
        'WaitTimeSeconds' => 20,
    ]);
    try {
        $messages = $async->wait();
    } catch (CancellationException) {
        break;
    }
    $async = null;

    // 何か処理する、途中でシグナルを受けても死なない
}

素の Guzzle で requestAsync などを使うと大丈夫だったのですが・・・謎。

指定されたコマンドを子プロセスで実行し、特定のシグナルを受けたときは子プロセス終了時にリスタートするシェルスクリプト

はじめに

  • php で作ったキューワーカーのようなサービスをコンテナで実行させる
  • サービスはシグナルをハンドリングしていわゆるに Graceful に終了したい
  • サービスが終了したときは普通にコンテナも終了する
  • ただしローカルの開発環境では特定のシグナルを受けたときはコンテナを終了させずにサービスだけリスタートする
  • そのためにシェルスクリプトを書いてサービスはそのシェルスクリプトから実行する

実装例その1

コンテナの init プロセスとして dumb-init を使って「dumb-init -> sh -> サービス」のようなプロセスツリーになるなら、次のようにシンプルに実装できそうです。

#!/bin/sh

trap restart=1 HUP
trap restart=  INT
trap restart=  QUIT
trap restart=  TERM

while :; do
  restart=
  "$@"
  status=$?
  if [ -z "$restart" ]; then
    exit "$status"
  fi
done

dumb-init がシグナルを受けると(-c オプションが指定されていなければ)直接の子プロセスだけでなく孫プロセスにもまとめてシグナルが転送されます。ので、シェルスクリプトからサービスへシグナルを送る必要はなく、単にサービスが終了したときにリスタートするかどうかの条件として最後に受けたシグナルを使えば良いだけです。

この方法の問題点は↑で列挙しているシグナル(HUP/INT/QUIT/TERM)以外のシグナルを受けると、例えサービスでそれをハンドリングしていても sh のプロセスが落ちてしまい、直下のプロセスが落ちたことで dumb-init も終了してコンテナ自体が終了するため、サービスのプロセスも突然死します。ただ、サービスでハンドリングしているシグナルを列挙しておけば良いだけなので、問題ではありません。

他の問題点をあげるとすれば・・dumb-init からシグナルが複数のプロセスに送られた後、各プロセスでシグナルハンドラが実行されるタイミングは決まっていないだろうので、HUP シグナルの後に sh の trap で restart=1 が実行される前に、サービスのプロセスが終了して sh の while ループを抜けてしまう可能性がゼロでは無いかもしれません(未確認)。ただリスタートしたいのはローカル環境だけのことでプロダクション環境で使いたいわけではないので、この程度の問題は目を瞑ります。

実装例その2

別の実装案。dumb-init が無くても動きます。また、シェルスクリプトが予期しないシグナルなどで終了するときもサービスのプロセスの終了を一定時間待ちます。ただ予期しないシグナルでも trap EXIT を発動させるために bash が必要です()。

#!/bin/bash

on_exit() {
  if [ -n "$pid" ]; then
    kill -TERM "$pid"
    for x in 0 1 2 3 4 5 6 7 8 9; do
      if ! kill -0 "$pid" 2>/dev/null; then
        exit
      fi
      sleep 1
    done
    kill -KILL "$pid"
  fi
}

trap 'restart=1 ; [ -n "$pid" ] && kill -HUP  "$pid"' HUP
trap 'restart=  ; [ -n "$pid" ] && kill -INT  "$pid"' INT
trap 'restart=  ; [ -n "$pid" ] && kill -QUIT "$pid"' QUIT
trap 'restart=  ; [ -n "$pid" ] && kill -TERM "$pid"' TERM
trap 'on_exit' EXIT

while :; do
  restart=
  "$@" &
  pid=$!
  while :; do
    wait
    if kill -0 "$pid" 2>/dev/null; then
      continue
    fi
    pid=
    if [ -z "$restart" ]; then
      exit
    fi
    break
  done
done

この実装だとサービスの終了コードが何であってもシェルスクリプトの終了コードは 0 になってしまいます。

さいごに

特定のシグナルでリスタートしたいことがあるのはローカル環境だけで、プロダクション環境では素朴に dumb-init の直下で実行すればよかったので、それならローカル環境でのみ runit とか supervised とかでリスタートさせればよいか・・と思ったのですがそれだけのために runit やら supervised やらを出してくるのは仰々しい気がしたので、シェルスクリプトを書いてみました。

S3 に保存された CloudFront のログを見やすく整形するツーライナー

要するに TSV や CSV は column コマンドで見やすくなる&# の行はちょっと特殊なので加工が必要。

aws s3 cp s3://XXX/YYY/ZZZ/ ./log/ --recursive --exclude='*' --include '*.2022-10-27-*'
zcat log/*.gz |
  sed -r '/^#Version/d;/^#Fields:/{s/\s/\t/g;s/^#Fields:\s+/#/}' |
  LANG=C sort |
  uniq |
  column -s $'\t' -t |
  less -S

ログに含まれる UNIX タイムスタンプを書式化するワンライナー

ログに日時が [1659316846] のように UNIX タイムスタンプで記録されているときに人にわかりやすく書式化するワンライナー。

cat <<'EOS' >log.log
[1659316843] りんご
[1659316844] ごりら
[1659316845] らっぱ
[1659316846] perl
EOS

cat log.log | perl -MPOSIX -ple 's/^\[(\d+)\]/strftime("[%Y-%m-%dT%H:%M:%S]",localtime($1))/e'
# [2022-08-01T10:20:43] りんご
# [2022-08-01T10:20:44] ごりら
# [2022-08-01T10:20:45] らっぱ
# [2022-08-01T10:20:46] perl

ワンライナーっつっても perl だけど・・これをワンライナーと呼んでよいのかどうか(謎)

ECS でサービスにアクセスがないときにタスクを自動的に停止して、アクセスがあったときに自動的に開始する

ECS で Review Apps のような、マージリクエスト(普段会社で Gitlab を使っているのでそう呼んでますが PR=プルリクエスト でも概ね同じです)の都度、コンテナイメージのビルド、ALB ターゲットグループ作成、ECS サービス作成、ALB リスナーにルール追加、して環境を自動で作成し、マージリクエストをマージする前に実際に動く環境で簡単に動作確認できるようにしようと思ったのですが、よくよく考えると ECS を Fargate で利用してる場合、マージリクエストを開いてから閉じるまでの期間ずっと Fargate の費用が発生するのもどうかな・・と思ったので、アクセスがないときは自動的にタスクを停止し、アクセスがあったときに自動的にタスクを開始する仕組みを作ってみました。

残骸はこちら→ ngyuki-sandbox/aws-ecs-autostart-autostop: aws-ecs-autostart-autostop

ちなみに EC2 にべたにアプリをデプロイするだけなら↓のような方法で簡単にひとつのインスタンスに複数の環境が作れるんですけどね。

概要

ECS サービスから実行されるタスクはサービスの DesiredCount を 0 にすればすべて停止できます。サービスへ一定時間アクセスが無かったときにこれを 0 に、サービスへアクセスがあったときにこれを 1 にできればよいのですが・・

前者は CloudWatch アラームで ECS サービスがアタッチされるターゲットグループの RequestCountPerTarget メトリクスが一定時間 0 になったときのアクションで DesiredCount を 0 にするための Lambda を呼べば出来そうです。

次のようなイメージです。

// ECS サービスのターゲットグループの RequestCountPerTarget が一定時間 0 ならアラーム状態
resource "aws_cloudwatch_metric_alarm" "autostop" {
  alarm_name          = "autostop"
  alarm_actions       = [aws_sns_topic.autostop.arn]
  namespace           = "AWS/ApplicationELB"
  metric_name         = "RequestCountPerTarget"
  statistic           = "Sum"
  treat_missing_data  = "missing"
  period              = 300
  evaluation_periods  = 1
  comparison_operator = "LessThanThreshold"
  threshold           = 1
  dimensions = {
    TargetGroup = aws_lb_target_group.service.arn_suffix
  }
}

// アラーム状態になったときに Lambda を実行
resource "aws_sns_topic" "autostop" {
  name = "autostop"
}
resource "aws_sns_topic_subscription" "autostop" {
  topic_arn = aws_sns_topic.autostop.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.autostop.arn
}
resource "aws_lambda_permission" "autostop" {
  statement_id  = "autostop"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.autostop.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.autostop.arn
}

// Lambda には環境変数で ECS のクラスタ&サービスを渡す
resource "aws_lambda_function" "autostop" {
  // ...snip...

  environment {
    variables = {
      cluster_arn  = var.cluster_arn,
      service_name = var.service_name,
    }
  }
}

Lambda のコードは次のようになります。

const AWS = require('aws-sdk');
const ecs = new AWS.ECS();
exports.handler = async (event, context) => {
    await ecs.updateService({
        cluster: process.env.cluster_arn,
        service: process.env.service_name,
        desiredCount: 0,
    }).promise();
    return {};
};

問題は後者の「サービスへアクセスがあったとき」です。同じように CloudWatch アラームを用いたのでは、アクセスがあってからサービスのタスクが立ち上がるまでに時間が掛かりすぎて使いものになりません。

ので ALB リスナーに、ECS サービスのターゲットグループのルールより優先度が高い Lambda 関数を呼び出すルールを追加し、その Lambda でサービスのタスクを開始&リスナールールの優先度を変更し、ECS サービスの方のターゲットグループにトラフィックが流れるようにします。

次のようなイメージです。

// ECS サービスがアタッチされるターゲットグループのリスナールール
resource "aws_lb_listener_rule" "service" {
  listener_arn = aws_lb_listener.main.arn
  priority     = 100

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.service.arn
  }

  condition {
    host_header {
      values = [var.domain]
    }
  }
}

// Lambda 関数がアタッチされるターゲットグループのリスナールール
resource "aws_lb_listener_rule" "autostart" {
  listener_arn = aws_lb_listener.main.arn

  // サービスを停止(DesiredCount=0)したときは優先度を低く
  // サービスを開始(DesiredCount=1)したときは優先度を高く
  priority     = 1

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.autostart.arn
  }

  // ECS サービスのリスナールールと同じ条件
  condition {
    host_header {
      values = [var.domain]
    }
  }
}

// Lambda 関数の方のリスナールールに設定されるターゲットグループ
resource "aws_lb_target_group" "autostart" {
  name        = "autostart"
  target_type = "lambda"
}
resource "aws_lb_target_group_attachment" "autostart" {
  target_group_arn = aws_lb_target_group.autostart.arn
  target_id        = aws_lambda_function.autostart.arn
}
resource "aws_lambda_permission" "autostart" {
  statement_id  = "autostart"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.autostart.function_name
  principal     = "elasticloadbalancing.amazonaws.com"
  source_arn    = aws_lb_target_group.autostart.arn
}

// Lambda には環境変数でリスナールールも渡す
resource "aws_lambda_function" "autostart" {
  // ...snip...

  environment {
    variables = {
      cluster_arn       = var.cluster_arn,
      service_name      = var.service_name,
      listener_rule_arn = aws_lb_listener_rule.autostart.arn
    }
  }
}

Lambda 関数は次のようになります。

const AWS = require('aws-sdk');
const elbv2 = new AWS.ELBv2();
const ecs = new AWS.ECS();
exports.handler = async (event, context) => {
    await ecs.updateService({
        cluster: process.env.cluster_arn,
        service: process.env.service_name,
        desiredCount: 1,
    }).promise();
    await elbv2.setRulePriorities({
        RulePriorities: [{
            Priority: 999,
            RuleArn: process.env.listener_rule_arn,
        }],
    }).promise();
    return {
        "statusCode": 200,
        "statusDescription": "200 OK",
        "isBase64Encoded": false,
        "headers": { "Content-Type": "text/html" },
        "body": `<h1>Please just a moment. Now starting environment.</h1>`,
    };
};

自動停止の Lambda 関数でも ELB リスナールールの優先度を変更する必要があります。

const AWS = require('aws-sdk');
const elbv2 = new AWS.ELBv2();
const ecs = new AWS.ECS();
exports.handler = async (event, context) => {
    await elbv2.setRulePriorities({
        RulePriorities: [{
            Priority: 1,
            RuleArn: process.env.listener_rule_arn,
        }],
    }).promise();
    await ecs.updateService({
        cluster: process.env.cluster_arn,
        service: process.env.service_name,
        desiredCount: 0,
    }).promise();
    return {};
};

さいごに

最初のアクセスから、Fargate 内でコンテナが開始し、さらにヘルスチェックが通るまで待つ必要があるため、ヘルスチェックの設定を最小限にしても 60 秒程度の時間が必要でした。 60 秒ぐらいなら・・待てなくもないでしょうか。

AWS で Oracle Linux 8 を動かしたメモ

とある事情で AWS で Oracle Linux 8 を使ってみることになったのでそのメモ。

AMI

AMI は所有者を 131827586825 で検索するとずらずら出てきます。

参考:https://community.oracle.com/tech/apps-infra/discussion/4417739/launch-an-oracle-linux-instance-in-aws

aws ec2 describe-images \
  --executable-users all \
  --owners 131827586825 \
  --query 'Images[] | [].{ Name: Name, ImageId: ImageId } | sort_by(@, &Name)' \
  --output table
# -------------------------------------------------------------------
# |                         DescribeImages                          |
# +------------------------+----------------------------------------+
# |         ImageId        |                 Name                   |
# +------------------------+----------------------------------------+
# |  ami-352d1c34          |  OL5.11-i386-10-17-2014-ebs            |
# |  ami-3d2d1c3c          |  OL5.11-x86_64-10-17-2014-ebs          |
# |  ami-0a7a660b          |  OL6-i386-HVM-2015-1-20                |
# |  ami-ce5742cf          |  OL6-i386-PVM-2014-12-11               |
# |  ami-a6637da7          |  OL6-x86_64-HVM-2015-1-20              |
# |  ami-8c23308d          |  OL6-x86_64-PVM-2015-1-12              |
# |  ami-08021f6ff9bb5043e |  OL6.10-x86_64-HVM-2019-01-30          |
# |  ami-06aec29ea727b45c5 |  OL6.10-x86_64-PVM-2019-01-30          |
# |  ami-1011387e          |  OL6.7-i386-HVM-2015-12-04             |
# |  ami-8fefc6e1          |  OL6.7-i386-PVM-2015-12-06             |
# |  ami-3e133a50          |  OL6.7-x86_64-HVM-2015-12-04           |
# |  ami-6a1f3604          |  OL6.7-x86_64-PVM-2015-12-04           |
# |  ami-a022cec1          |  OL6.8-i386-HVM-2016-05-23             |
# |  ami-233cd042          |  OL6.8-i386-PVM-2016-05-23             |
# |  ami-e726ca86          |  OL6.8-x86_64-HVM-2016-05-23           |
# |  ami-6a3dd10b          |  OL6.8-x86_64-PVM-2016-05-24           |
# |  ami-9f8ca9f8          |  OL6.9-i386-HVM-2017-03-29             |
# |  ami-4882a72f          |  OL6.9-i386-PVM-2017-03-29             |
# |  ami-6982a70e          |  OL6.9-x86_64-HVM-2017-03-28           |
# |  ami-afc65dc9          |  OL6.9-x86_64-HVM-2018-01-10           |
# |  ami-c58faaa2          |  OL6.9-x86_64-PVM-2017-03-28           |
# |  ami-70c45f16          |  OL6.9-x86_64-PVM-2018-01-10           |
# |  ami-fce940fc          |  OL7.1-x86_64-HVM-2015-06-17           |
# |  ami-0ce8410c          |  OL7.1-x86_64-PVM-2015-04-06           |
# |  ami-65e0c80b          |  OL7.2-x86_64-HVM-2015-12-10           |
# |  ami-52e1c93c          |  OL7.2-x86_64-PVM-2015-12-10           |
# |  ami-de248ebf          |  OL7.3-x86_64-HVM-2016-11-09           |
# |  ami-1e11e778          |  OL7.4-x86_64-HVM-2017-08-07           |
# |  ami-3ad3485c          |  OL7.4-x86_64-HVM-2018-01-09           |
# |  ami-0918cd1ebaef69218 |  OL7.5-x86_64-HVM-2019-03-04           |
# |  ami-054e85339904efdef |  OL7.6-x86_64-HVM-2019-01-29           |
# |  ami-0950559ec2dfaacec |  OL7.7-x86_64-HVM-2020-02-13           |
# |  ami-0267b0d59b04d0de5 |  OL7.7-x86_64-HVM-2020-02-13-13-14     |
# |  ami-04e6facfaefdca6ec |  OL7.8-x86_64-HVM-2020-04-28           |
# |  ami-07174046d339a2557 |  OL7.9-x86_64-HVM-2020-12-07           |
# |  ami-0ef7c57e516f3dda7 |  OL8.1-x86_64-HVM-2019-12-11-15-50-57  |
# |  ami-0509f2707adadc9c5 |  OL8.1-x86_64-HVM-2020-01-15           |
# |  ami-05cf17097c9e7017f |  OL8.2-x86_64-HVM-2020-05-22           |
# |  ami-0d5bb29b78cc0af39 |  OL8.2-x86_64-HVM-2020-12-09           |
# |  ami-0ab635c88e06025fb |  OL8.3-x86_64-HVM-2020-12-10           |
# |  ami-0fbc34d0cb97e47b3 |  OL8.4-x86_64-HVM-2021-05-28           |
# |  ami-0afe0424c9fd49524 |  OL8.5-x86_64-HVM-2021-11-24           |
# +------------------------+----------------------------------------+

Oracle Linux 8.5 = OL8.5 なので ami-0afe0424c9fd49524 です。

os-release とか

インスタンス作成後に ssh ログインして色々見てみます。

cat /etc/redhat-release
# Red Hat Enterprise Linux release 8.5 (Ootpa)

cat /etc/oracle-release
# Oracle Linux Server release 8.5

cat /etc/os-release
# NAME="Oracle Linux Server"
# VERSION="8.5"
# ID="ol"
# ID_LIKE="fedora"
# VARIANT="Server"
# VARIANT_ID="server"
# VERSION_ID="8.5"
# PLATFORM_ID="platform:el8"
# PRETTY_NAME="Oracle Linux Server 8.5"
# ANSI_COLOR="0;31"
# CPE_NAME="cpe:/o:oracle:linux:8:5:server"
# HOME_URL="https://linux.oracle.com/"
# BUG_REPORT_URL="https://bugzilla.oracle.com/"
#
# ORACLE_BUGZILLA_PRODUCT="Oracle Linux 8"
# ORACLE_BUGZILLA_PRODUCT_VERSION=8.5
# ORACLE_SUPPORT_PRODUCT="Oracle Linux"
# ORACLE_SUPPORT_PRODUCT_VERSION=8.5

ID_LIKErhel が無いですね、RHEL の ID_LIKE はおそらく fedora だけだろうので、それがそのままということなのでしょうけど。

なお、CentOS 8 なら rhel fedora だし AlmaLinux 8 なら rhel centos fedora です。

Kernel

uname -r
# 5.4.17-2136.300.7.el8uek.x86_64

カーネルのバージョンが RHEL8 と全然違います。いわゆる Unbreakable Enterprise Kernel(UEK) です。 Oracle Linux は UEK と RHEL 互換のカーネル(Red Hat Compatible Kernel(RHCK)) が利用できますが、デフォルトが UEK になっているようです。

そのままでもあまり問題無いかもしれませんが RHCK に変更します。見た感じ RHCK のカーネル自体がインストールされていないようなのでインストールします。

ls -l /boot/vmlinuz*
# -rwxr-xr-x. 1 root root 10348096 Oct  9 08:29 /boot/vmlinuz-5.4.17-2136.300.7.el8uek.x86_64

dnf install kernel
# いろいろ

ls -l /boot/vmlinuz*
# -rwxr-xr-x. 1 root root 10221104 Dec 22 02:32 /boot/vmlinuz-4.18.0-348.7.1.el8_5.x86_64
# -rwxr-xr-x. 1 root root 10348096 Oct  9 08:29 /boot/vmlinuz-5.4.17-2136.300.7.el8uek.x86_64

grubby でデフォルトカーネルを切り替えてリブートします。

参考:https://community.oracle.com/tech/apps-infra/discussion/4467791/oracle-linux-how-to-change-default-kernel

grubby --default-kernel
# /boot/vmlinuz-5.4.17-2136.300.7.el8uek.x86_64

grubby --info=ALL | grep -E "^kernel|^index"
# index=0
# kernel="/boot/vmlinuz-5.4.17-2136.300.7.el8uek.x86_64"
# index=1
# kernel="/boot/vmlinuz-4.18.0-348.7.1.el8_5.x86_64"

grubby --set-default /boot/vmlinuz-4.18.0-348.7.1.el8_5.x86_64
# The default is /boot/loader/entries/ec2536de9a9cc53fbc785b744042fb86-4.18.0-348.7.1.el8_5.x86_64.conf with index 1 and kernel /boot/vmlinuz-4.18.0-348.7.1.el8_5.x86_64

grubby --default-kernel
# /boot/vmlinuz-4.18.0-348.7.1.el8_5.x86_64

reboot

カーネルが RHEL 互換になっていることを確認します。

uname -r
# 4.18.0-348.7.1.el8_5.x86_64

EPEL

EPEL は Oracle で専用のリポジトリを持っているらしく epel-release のパッケージ名が異なるようです。

dnf install -y oracle-epel-release-el8

PowerTools

CentOS 8 で EPEL の moreutils をインストールしようとすると PowerTools リポジトリにある perl-IPC-Run が必要になります。

PowerTools はデフォルトで無効なので有効にしてインストールする必要があるのですが、

dnf install --enablerepo=powertools moreutils

Oracle Linux 8 だとリポジトリ名が異なっており PowerTools は ol8_codeready_builder です。

dnf install --enablerepo=ol8_codeready_builder moreutils

他のリポジトリ名の全然違っているため、リポジトリ名を指定してなにかしているなら注意が必要です。

dnf repolist --enablerepo=\* | column -t -s $'\t'
# repo id               repo name
# ol8_UEKR6             Latest Unbreakable Enterprise Kernel Release 6 for Oracle Linux 8 (x86_64)
# ol8_UEKR6_RDMA        Oracle Linux 8 UEK6 RDMA (x86_64)
# ol8_addons            Oracle Linux 8 Addons (x86_64)
# ol8_appstream         Oracle Linux 8 Application Stream (x86_64)
# ol8_baseos_latest     Oracle Linux 8 BaseOS Latest (x86_64)
# ol8_codeready_builder Oracle Linux 8 CodeReady Builder (x86_64) - Unsupported
# ol8_developer_EPEL    Oracle Linux 8 EPEL Packages for Development (x86_64)
# ol8_distro_builder    Oracle Linux 8 Distro Builder (x86_64) - Unsupported
# ol8_kvm_appstream     Oracle Linux 8 KVM Application Stream (x86_64)
# ol8_u0_baseos_base    Oracle Linux 8 BaseOS GA (x86_64)
# ol8_u1_baseos_base    Oracle Linux 8.1 BaseOS (x86_64)
# ol8_u2_baseos_base    Oracle Linux 8.2 BaseOS (x86_64)
# ol8_u3_baseos_base    Oracle Linux 8.3 BaseOS (x86_64)
# ol8_u4_baseos_base    Oracle Linux 8.4 BaseOS (x86_64)
# ol8_u5_baseos_base    Oracle Linux 8.5 BaseOS (x86_64)

参考:https://github.com/oracle/centos2ol/blob/1424a2417aaa383b5ebf0b77b9147584c85c9695/centos2ol.sh#L454

firewalld と iptables

Oracle Linux 8 の素の AMI だと firewalld が有効でした。CentOS 8 の素の AMI だと firewalld は無効だったと思うので、何も知らないと「何故か ssh 以外で接続できない・・」となり数分~十数分ぐらいの時間を無駄にします。

セキュリティグループやネットワークACLで十分ならサーバの firewalld は止めても良いでしょう。

systemctl stop firewalld
systemctl disable firewalld

がしかし、これだけでは不十分。なぜか iptables-services がインストールされています。

rpm -q iptables-services
# iptables-services-1.8.4-20.0.1.el8.x86_64

しかも有効です。

systemctl is-enabled iptables
# enabled

がしかし、firewalld で Conflicts に指定されているため iptables サービスは起動しません。

systemctl show firewalld -p Conflicts
# Conflicts=ip6tables.service nftables.service ebtables.service ipset.service shutdown.target iptables.service

systemctl status iptables
# * iptables.service - IPv4 firewall with iptables
#    Loaded: loaded (/usr/lib/systemd/system/iptables.service; disabled; vendor preset: disabled)
#    Active: inactive (dead)

がしかし、前述のように firewalld を無効にしてリブートすると、次は iptables サービスが起動してきます。

systemctl disable firewalld
reboot
.
.
.
systemctl status iptables
# * iptables.service - IPv4 firewall with iptables
#    Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled; vendor preset: disabled)
#    Active: active (exited) since Tue 2022-01-18 13:20:10 GMT; 25min ago
#     :

ので、firewalld と iptables の両方を無効にする必要があります。

systemctl stop firewalld
systemctl disable firewalld
systemctl disable iptables

tar: command not found

素の AMI だと tar すら入っていませんでした。dnf install @base とかで 普通入ってそうだけど入っていないもの を確認しておくと良いかもしれません。

EC2 でインスタンスを停止せずにルートボリュームを置き換えてみるメモ

少し前に AWS EC2 でルートボリュームをインスタンスを停止せずに交換可能になったと発表がありました。

え、停止せずにって、無停止でってこと? そんな馬鹿な(馬鹿な)

ので、試してみました。

適当にインスタンスを作って、適当にファイルを書いて、

echo 'this is test 1' > test.txt

マネジメントコンソールからスナップショットを作成した後、↑のファイルを上書きします。

echo 'this is test 2' > test.txt

マネジメントコンソールからルートボリュームの置き換えを行います。インスタンス詳細のストレージタブにあります。

f:id:ngyuki:20210520220406p:plain

スナップショットのところにフォーカスすると候補が表示されるので、↑で作成したスナップショットを選択します。

f:id:ngyuki:20210520220419p:plain

置き換えタスクを作成すると、前画面のストレージのところに表示されます。

f:id:ngyuki:20210520220429p:plain

ここが「成功」になれば置き換え完了です。

f:id:ngyuki:20210520220440p:plain

インスタンスに ssh して確認してみると・・

cat test.txt
#=> this is test 1
uptime
#=> 18:34:23 up 0 min,  1 user,  load average: 0.04, 0.01, 0.00

まあ当たり前でした。OS 的には再起動しています。ただ、インスタンスを停止→開始する必要がないので、associate-public-ip-address で付与された PublicIP が変わることもなければ、エフェメラルボリュームのデータもそのまま残ります(普通に停止→開始すると PublicIP は変わるしエフェメラルボリュームのデータも消える)。

さいごに

ルートボリュームをスナップショットで置き換えた後、同じインスタンスで再度ルートボリュームの置き換えをしようとしても、同じスナップショットは選択の候補に現れないようです。

ただ、置き換えるスナップショットを指定しなければルートボリュームの元となったスナップショットで置き換えられるため、直前に置き換えたスナップショットでもう一度置き換えたい場合は単にスナップショットを未指定で置き換えを実行すれば OK です(トライアンドエラーで試行錯誤するときにありそう)。

おそらく、置き換える対象のルートボリュームから作成されたスナップショットしか候補に表示されないようになっているのだと思います。置き換えるとルートボリュームが新しく作成されるので、以前のルートボリュームから作成されていたスナップショットはすべて候補にあがらなくなるのではないかと。

  • ルートボリューム A からスナップショット S を作成する
  • スナップショット S で ルートボリューム A を置き換える
    • 新たにボリューム B が作られて置き換えられる
  • B の置き換えでは S は B のスナップショットではないので候補にあがらない
    • ただし B のベースは S なのでスナップショットが未指定なら S で置き換えられる

なお、これはマネジメントコンソール上で選択の候補にあげられるかどうかだけのものなので、 スナップショットのIDを直接入力すれば候補に無いスナップショットでも置き換え可能だし、 AWS CLI でやる分にはぜんぜん関係ありません。

aws --profile hj-tky ec2 create-replace-root-volume-task \
  --instance-id i-04ff3fb484044124b --snapshot-id snap-0386ee904da7163a8
#=> {
#=>     "ReplaceRootVolumeTask": {
#=>         "ReplaceRootVolumeTaskId": "replacevol-0edbfcd8a8e82d2c2",
#=>         "InstanceId": "i-04ff3fb484044124b",
#=>         "TaskState": "pending",
#=>         "StartTime": "2021-05-17T18:52:29Z",
#=>         "Tags": []
#=>     }
#=> }

これ知らずにいると、なぜか置き換えできない・・とか思って嵌りそうです。