📑

CircleCI : Flaky Test におけるパイプラインの再実行 max_auto_reruns パラメータ

に公開

2025年8月6日に失敗したパイプラインで起動したワークフローのリトライ機能がリリースされました。
https://circleci.com/changelog/automatic-retry-of-workflows-released/
ワークフローが失敗した場合に、手動操作なしで自動的に “rerun from failed” が実行され、失敗したジョブだけが再実行されます(成功済みのジョブは再実行されません)。UI でも自動リトライ状況と試行回数が表示されます。
この機能は Flaky Test(何も変えていないのに成功/失敗が揺れる不安定テスト)や、ネットワーク遅延・外部依存の一時的障害など一過性の失敗に対して有効です。

Flaky Test とは

「flaky test(フレイキーテスト)」とは、同じテストコードを何も変更していないのに、あるときは成功し、あるときは失敗する不安定なテストのことを指します。再現性が低く、同じ環境で実行しても結果が安定しないケースがあります。テスト対象のバグではなく、テストコードや環境要因に依存して失敗することが多いく、意図せずCI/CDパイプラインを停止させてしまいます。

たとえば、テストで呼び出すコードがランダム値を使っている、時刻依存の処理が含まれている、非同期処理のタイミングがずれる、ネットワークやファイル I/O の遅延、など理由は様々です。

シンプルな例で言えばWeb Serverをインストールして起動するconfig.ymlを作成した際に、npm や git コマンドで必要なライブラリやモジュールをインストールする際にダウンロードが遅い、OSの状況によりインストールにいつもより時間がかかる、などでtest用curlコマンドが間に合わないケース、などがあります。

max_auto_reruns パラメータ

この問題を解決させるためにリリースされた機能が max_auto_reruns パラメータです。workflowの設定に以下の通り記載をしておくことでパイプラインが失敗した際に、指定回数再実行を行います。

workflows:
  deploy:
    jobs:
      - deploy-nodeapp
    # ここで自動リトライ回数を指定
    max_auto_reruns: 2

成功した際にはパイプラインは再実行されません。

さっそくやってみる

前回の記事でnodeを用いたwebサーバをSSHでLinuxにログインしたのちデプロイする手順を纏めました。
https://zenn.dev/kameoncloud/articles/a5b0d0999544d5

config.yml,server.jsの2つをさらに置換します。

config.yml
version: 2.1

jobs:
  deploy-nodeapp:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout

      # CircleCIに登録したSSH鍵を使用(Fingerprintで指定)
      - add_ssh_keys:
          fingerprints:
            - "SHA256:xxKOKDhqcEfxkek9slpnuS4mDrXaNRPC2eMjq1oXNBk"

      # デプロイ先サーバの known_hosts 登録
      - run:
          name: Register known_hosts
          command: |
            mkdir -p ~/.ssh
            ssh-keyscan 163.43.218.xx >> ~/.ssh/known_hosts

      # Nodeアプリのデプロイ&起動
      - run:
          name: Deploy & start node app
          command: |
            ssh -p 22 ubuntu@163.43.218.xx "
              set -eo pipefail

              APP_DIR=/home/ubuntu/nodeapp
              REPO_URL=https://github.com/h-kameda-sakura/apitest.git

              wait_for_apt_lock() {
                sudo dpkg --configure -a >/dev/null 2>&1 || true
                while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
                   || sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
                  echo '[apt] Waiting for lock to be released...'
                  sleep 3
                done
              }

              if command -v apt-get >/dev/null 2>&1; then
                export DEBIAN_FRONTEND=noninteractive
                wait_for_apt_lock
                sudo apt-get update -y
                wait_for_apt_lock
                sudo apt-get install -y git curl
              elif command -v dnf >/dev/null 2>&1; then
                sudo dnf install -y git curl
              elif command -v yum >/dev/null 2>&1; then
                sudo yum install -y git curl
              elif command -v zypper >/dev/null 2>&1; then
                sudo zypper -n install git curl
              fi

              mkdir -p ~/.ssh
              ssh-keyscan github.com >> ~/.ssh/known_hosts || true

              if ! command -v node >/dev/null 2>&1; then
                if command -v apt-get >/dev/null 2>&1; then
                  export DEBIAN_FRONTEND=noninteractive
                  wait_for_apt_lock
                  curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
                  wait_for_apt_lock
                  sudo apt-get install -y nodejs
                elif command -v dnf >/dev/null 2>&1; then
                  curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
                  sudo dnf install -y nodejs
                elif command -v yum >/dev/null 2>&1; then
                  curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
                  sudo yum install -y nodejs
                elif command -v zypper >/dev/null 2>&1; then
                  curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
                  sudo zypper -n install nodejs
                else
                  echo Unsupported OS >&2; exit 1
                fi
              fi

              if [ -d \"\$APP_DIR/.git\" ]; then
                git -C \"\$APP_DIR\" fetch --all
                git -C \"\$APP_DIR\" checkout main
                git -C \"\$APP_DIR\" pull --ff-only
              else
                sudo mkdir -p \"\$APP_DIR\"
                sudo chown -R ubuntu:ubuntu \"\$APP_DIR\"
                git clone \"\$REPO_URL\" \"\$APP_DIR\"
              fi

              cd \"\$APP_DIR\"
              npm ci || npm install

              if [ -f nodeapp.service ]; then
                sudo install -m 0644 nodeapp.service /etc/systemd/system/nodeapp.service
                sudo systemctl daemon-reload
                sudo systemctl enable nodeapp
              fi

              sudo systemctl restart nodeapp || sudo systemctl start nodeapp
              sudo systemctl status nodeapp --no-pager || true
            "

      # 🔍 HTTPテスト(/test を叩く)
      - run:
          name: Test http://163.43.218.xx:8080/test
          command: |
            URL="http://163.43.218.xx:8080/test"
            echo "Waiting 3 seconds before test..."
            sleep 3
            code=$(curl -fsS -o /dev/null -w "%{http_code}" --connect-timeout 3 "$URL" || true)
            if [ "$code" = "200" ]; then
              echo "OK: $URL/test returned 200"
              exit 0
            else
              echo "Test failed for $URL (status:$code)" >&2
              exit 1
            fi

workflows:
  deploy:
    jobs:
      - deploy-nodeapp
    # ここで自動リトライ回数を指定
    max_auto_reruns: 2
server.js
// server.js (http版 + /test失敗)
const http = require('http');

const port = process.env.PORT || 8080;
const server = http.createServer((req, res) => {
  if (req.url === '/test') {
    const fail = Math.random() < 0.5;
    res.writeHead(fail ? 500 : 200, {'Content-Type':'application/json; charset=utf-8'});
    res.end(JSON.stringify({
      ok: !fail,
      message: fail ? 'intentional failure for test' : 'success'
    }));
    return;
  }

  // デフォルト /
  res.writeHead(200, {'Content-Type':'text/plain; charset=utf-8'});
  res.end('Hello from Node.js on port ' + port + '\n');
});

server.listen(port, '0.0.0.0', () => {
  console.log(`Server running at http://0.0.0.0:${port}/`);
});

config.ymlではhttp://163.43.218.xx:8080/testにcurlを実行し200が戻ればテストが成功しパイプラインが完了します。そのエンドポイントは50%の確率で200,500どちらかが戻る様になっています。
config.yml max_auto_reruns: 2によりパイプラインが失敗しても2回までリトライ処理が行われます。

Discussion