😶🌫️
Cloudflare Workers でメンテナンスページを用意する
動機
Misskeyサーバーを運営していて、アップデートを実施するときにサイトにアクセスできなくなってしまうので、カスタムメンテナンスページを用意したくなりました。
メンテページ実現の選択肢
(😞不採用)NginxやApacheの設定でメンテページを返す
- メンテナンスモードの場合はサーバー上の静的ページを返す、という昔ながらのやり方。
- 1台のWebサーバーで運営しているので、サーバー再起動などしているタイミングだとそもそもメンテページすらアクセスできなくなる。
(🥰採用)Cloudflare Workers を使う
- エッジで実行されるので、Webサーバーが落ちていてもユーザーにはページを返し続けてくれる。
- ダッシュボード上で編集して即デプロイできるので楽しい。
- KVに環境変数を設定することでメンテモードの切り替えも楽ちん。
Cloudflare Workers の基本
Cloudflare Workers は、エッジで動作するサーバーレスプラットフォームです。リクエストをインターセプトし、カスタムロジックを実行できます。
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
// カスタムロジックをここに実装
}
Workersの作成
Cloudflareのダッシュボードの「Workers&Pages」からWorkersを選択して作成しましょう。
私は「maintenance-page」という名前で作りました。
ちょっと長いですがコード全部載せます。
コードを表示
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const maintenanceMode = await MAINTENANCE_KV.get('maintenance_mode')
if (maintenanceMode === 'true') {
const startTime = new Date('2024-08-29T15:15:00+09:00');
const endTime = new Date('2024-08-29T16:45:00+09:00');
const htmlContent = maintenanceHTML
.replace('{{startTime}}', formatJST(startTime))
.replace('{{endTime}}', formatJST(endTime))
.replace('{{startTimestamp}}', startTime.getTime())
.replace('{{endTimestamp}}', endTime.getTime());
return new Response(htmlContent, {
headers: { 'Content-Type': 'text/html' },
status: 503
})
}
return fetch(request)
}
function formatJST(date) {
return date.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });
}
const maintenanceHTML = `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>メンテナンス中</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(to bottom, #f0fff4, #c6f6d5);
}
.container {
background-color: white;
padding: 2rem;
border-radius: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 90%;
width: 400px;
position: relative;
overflow: hidden;
}
h1 {
color: #2d3748;
margin-bottom: 1rem;
}
p {
color: #4a5568;
margin-bottom: 0.5rem;
}
.countdown {
font-size: 1.5rem;
font-weight: bold;
color: #38a169;
background-color: #f0fff4;
border-radius: 9999px;
padding: 0.5rem 1rem;
display: inline-block;
margin: 1rem 0;
}
#wave {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
</style>
</head>
<body>
<div class="container">
<svg id="wave" preserveAspectRatio="none" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="water-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(74, 222, 128, 0.6)" />
<stop offset="100%" stop-color="rgba(74, 222, 128, 0.2)" />
</linearGradient>
</defs>
<path id="wavePath" fill="url(#water-gradient)">
<animate
attributeName="d"
values=""
dur="10s"
repeatCount="indefinite"
begin="0s"
/>
</path>
</svg>
<div class="content">
<h1>🌙メンテナンス中です</h1>
<p>いつもちゃすきーを見にきてくれてありがとう🍵</p>
<p>現在サーバーアップデートなどの事情により、ご利用いただけません。ご不便をおかけしてすみません。</p>
<p>開始時刻: <span id="startTime">{{startTime}}</span></p>
<p>終了予定: <span id="endTime">{{endTime}}</span></p>
<div class="countdown" id="countdown"></div>
<p>ご理解とご協力をお願いいたします。<br>お気に入りのお茶でも飲んでお待ちください。</p>
</div>
</div>
<script>
const startTime = new Date({{startTimestamp}});
const endTime = new Date({{endTimestamp}});
function getJSTDate() {
return new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tokyo' }));
}
function updateWavePath(waterLevel) {
const wavePath = document.getElementById('wavePath');
const d = \`
M 0 \${400 - waterLevel * 4}
Q 100 \${380 - waterLevel * 4} 200 \${400 - waterLevel * 4}
T 400 \${400 - waterLevel * 4}
V 400
H 0
Z
\`;
wavePath.setAttribute('d', d);
const animateTag = wavePath.querySelector('animate');
animateTag.setAttribute('values', \`
\${d};
M 0 \${400 - waterLevel * 4 + 20}
Q 100 \${400 - waterLevel * 4 + 20} 200 \${400 - waterLevel * 4 + 20}
T 400 \${400 - waterLevel * 4 + 20}
V 400
H 0
Z;
\${d}
\`);
}
function updateCountdown() {
try {
let waterLevel;
const now = getJSTDate();
const diff = endTime - now;
if (diff > 0) {
const minutes = Math.floor(diff / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
document.getElementById('countdown').textContent = \`残り時間: \${minutes}分 \${seconds}秒\`;
const progress = Math.min(Math.max(((now - startTime) / (endTime - startTime)) * 100, 0), 100);
const minWaterLevel = 20;
const waterLevel = minWaterLevel + (progress * (100 - minWaterLevel) / 100);
updateWavePath(waterLevel);
} else {
document.getElementById('countdown').textContent = 'メンテナンス完了';
waterLevel = 100;
updateWavePath(waterLevel);
}
} catch (error) {
console.error('更新中にエラーが発生しました:', error);
}
}
// ページロード時に即座に更新
updateCountdown();
// 1秒ごとに更新
setInterval(updateCountdown, 1000);
console.log('スクリプトが正常に読み込まれました');
</script>
</body>
</html>
`;
このようなページが作成されます。
KVにメンテモードの有効/無効を保持
MAINTENANCE_KVという名前空間を作成して、maintenance_modeというキーに、true or false を値に設定します。
KVのバインド
Workersの
[設定]>[変数]>[KV 名前空間のバインディング]
からKVとWorkers内で参照する変数名のバインドを設定できます。
MAINTENANCE_KVという変数名に、KV名前空間 MAINTENANCE_KV(そのまま)をバインドします。
ルートの設定
どのURLに対してメンテページを表示させるかも柔軟に設定可能です。
Workersの
[設定]>[トリガー]>[ルート]から、
hostname/*
などとするとサイト全体がメンテページにルーティングされます。
おわりに
以上で設定完了です。
ちゃすきーという、お茶好き専用のMisskeyサーバーを運営しているので、興味ある方は新規登録ぜひ!
Discussion