スプリント開始時には、タスク整理だけでなく開発環境の準備も欠かせません。2週間サイクルでクライアントのWordPress案件を進めるWeb制作会社では、最初のチケットに取りかかる前にステージング環境を用意するのが一般的です。Kinstaを利用している場合は、MyKinstaから環境を作成できます。

この作業自体は数分で終わりますが、小さなタスクだからこそ後回しにされがちです。

Kinsta APIを使えば、この手間を自動化できます。たとえば、Jiraでスプリントが開始されたタイミングでWebhookをトリガーし、ミドルウェア側でイベントを受け取るよう設定できます。そこからペイロードを読み込み、対象のKinstaサイトへ紐づけたうえでAPIを呼び出し、新しいステージング環境を自動で作成できます。

制作会社が環境のプロビジョニングを自動化すべき理由

スプリントの計画後に毎回環境を作成するとなると、MyKinstaを開き、数多くのサイト一覧から対象のクライアントサイトを探し、環境を作成して名前を付け、それからJiraに戻る必要があります。作業自体は複雑ではありませんが、すべてのクライアントプロジェクトで、毎回適切なタイミングに行わなければなりません。

もしこの作業を省略すると、チームは前回のスプリントで使っていた環境のまま作業を続けることになります。すると変更が積み重なり、不具合が発生したときには、原因の切り分けがデバッグというより“発掘作業”のようになってしまいます。

開始前に必要なもの

Kinsta APIとJiraを連携するには、少なくとも1つ以上のWordPressサイトを運用しているMyKinstaアカウント、Webhookを設定するための管理者権限を持つJira Cloudアカウント、そしてローカル環境にインストールされたNode.jsが必要になります。

Kinsta APIを利用するには、まず API キーを作成します。MyKinstaにログインし、「企業の設定」>「APIキー」に移動し、「APIキーを作成」をクリックしてください。

MyKinstaの「APIキー」画面
MyKinstaの「APIキー」画面

次にキーに名前を付けて有効期間を設定し、「生成」をクリックします。キーが表示されるのは一度のみのため、次に進む前にメモしておいてください。

この値を、Kinstaの企業IDとあわせて、プロジェクトルートにある.envファイルへ追加します。

KINSTA_API_KEY=your_api_key_here
KINSTA_COMPANY_ID=your_company_id_here

JiraとKinstaのサイトIDを取得する

自動化を設定するには、各クライアントプロジェクトに対応するKinstaのサイトIDが必要です。これは、サイト作成時にKinstaによって割り当てられるUUIDです。サイトIDは、MyKinstaで対象サイトを開いた際のURLから確認できます。また、APIキーを設定した状態でGET /sitesエンドポイントを呼び出して取得することも可能です。

https://my.kinsta.com/sites/details/fbab4927-e354-4044-b226-29ac0fbd20ca/…

Jira側では、連携したい各プロジェクトの数値ボードIDが必要です。これはURL内に表示されています(この例では2)。

https://your-domain.atlassian.net/jira/software/projects/SCRUM/boards/2

これは、Jiraがsprint_started WebhookのペイロードにoriginBoardIdとして含める値と同じものです。ボードIDとサイトIDのマッピングは、.envファイルに保存します。

board_id_client_a=2
SITE_ID_CLIENT_A=fbab4927-e354-4044-b226-29ac0fbd20ca
ボードID_CLIENT_B=5
SITE_ID_CLIENT_B=44b5a6d1-c83f-4b0e-9a1c-2e7dbc903fa1

また、ローカル開発環境では、Jiraからlocalhostへ直接アクセスすることはできません。そのため、Ngrokを使ってローカルポートを一時的なパブリックURLとして公開する必要があります。ミドルウェアをデプロイして公開URLを取得した後は、そのURLに置き換えることができます。

JiraとKinsta APIを使ってスプリント環境のプロビジョニングを自動化する方法

この連携は、JiraとKinstaの2つのシステムをまたいで動作します。Jiraでは、スプリントが開始されるとWebhookが実行され、イベントペイロードがミドルウェアに送信されます。Kinsta側では、ミドルウェアがペイロードからボードIDを読み取り、設定済みのマッピングを使ってサイトIDを特定します。そのうえでKinsta APIを呼び出し、スプリント名に基づいた標準ステージング環境を作成します。

1. スプリントイベントのためにJira webhookを登録する

Jira CloudでWebhookを登録する方法は2つあります。多くのチームにとって簡単なのは、JiraのUIを使う方法です。画面右上のメニューから「Settings」>「System」を開きます。

Jira Cloudの一般設定内にある「System」オプション
Jira Cloudの一般設定内にある「System」オプション

そこから「Advanced」>「WebHooks」を選択し、「Create a WebHook」をクリックします。

WebHooksセクションの右上に「Create a WebHook」が表示される
WebHooksセクションの右上に「Create a WebHook」が表示される

ここでWebhook名を入力し、ミドルウェアのURLの末尾に/sprintを追加したものを貼り付けます(現時点ではダミーURLでも構いません)。続いて、「Events」で「Sprint」>「started」を選択してください。これにより、Jiraインスタンス全体でsprint_startedイベントが発生するたびに実行される管理者Webhookが作成されます。

JiraのWebhook作成フォーム
JiraのWebhook作成フォーム

2つ目の方法は、POST /rest/webhooks/1.0/webhookを使用するREST APIです。Webhookの登録をデプロイスクリプトに組み込みたい場合は、こちらの方法が便利です。

curl -X POST \
  https://your-domain.atlassian.net/rest/webhooks/1.0/webhook \
  -u [email protected]:your-api-token \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Sprint provisioning webhook",
    "url": "https://your-middleware-url.com/sprint",
    "events": ["sprint_started"],
    "filters": {},
    "excludeBody": false
  }'

PUT /rest/webhooks/1.0/webhook/refreshを呼び出すと、Webhookの有効期限を30日間延長できます。Jiraでsprint_startedイベントが発生すると、ペイロードは以下のような構造のJSON POSTとしてエンドポイントに送信されます。

{
  "timestamp": 1705431600000,
  "webhookEvent": "sprint_started",
  "sprint": {
    "id": 15,
    "self": "https://your-domain.atlassian.net/rest/agile/1.0/sprint/15",
    "state": "active",
    "name": "Sprint 12",
    "startDate": "2026-02-02T00:00:00.000Z",
    "endDate": "2026-02-27T00:00:00.000Z",
    "originBoardId": 2,
    "goal": "Complete payment processing improvements"
  }
}

ミドルウェアは、sprint.originBoardIdを使って対応するKinstaサイトIDを特定し、sprint.nameを使って新しい環境に名前を付けます。Jiraインスタンス内で発生するすべてのsprint_startedイベントがこのエンドポイントに送信されますが、設定済みのボードIDマッピングによって、対象のクライアントプロジェクトだけを処理し、それ以外は無視する仕組みになっています。

2. ミドルウェアエンドポイントを構築する

Webhookを設定したら、次は新しいNode.jsプロジェクトを初期化し、dotenvとあわせてExpress.jsをインストールします。

npm init -y
npm install express dotenv

expressはルーティングとリクエスト処理を担当し、dotenv.envファイルの環境変数を読み込みます。続いて、サーバーをセットアップするためにapp.jsを作成します。

// app.js
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();
const app = express();

// Raw body parser on the /sprint route enables HMAC signature verification
app.use('/sprint', express.raw({ type: 'application/json' }));
app.use(express.json());

const KinstaAPIUrl = 'https://api.kinsta.com/v2';
const headers = {
  'Content-Type': 'application/json',
  Authorization: `Bearer ${process.env.KINSTA_API_KEY}`
};

// Board ID to Kinsta site ID config map
const siteConfig = {
  [process.env.BOARD_ID_CLIENT_A]: process.env.SITE_ID_CLIENT_A,
  [process.env.BOARD_ID_CLIENT_B]: process.env.SITE_ID_CLIENT_B,
};

function verifyJiraSignature(req) {
  const signature = req.headers['x-hub-signature'];
  const secret = process.env.JIRA_WEBHOOK_SECRET;
  if (!signature || !secret) return false;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post('/sprint', async (req, res) => {
  if (!verifyJiraSignature(req)) {
    return res.status(401).json({ message: 'Invalid signature' });
  }

  const body = JSON.parse(req.body);
  const { webhookEvent, sprint } = body;

  if (webhookEvent !== 'sprint_started') {
    return res.status(200).json({ message: 'Event ignored' });
  }

  const boardId = String(sprint.originBoardId);
  const siteId = siteConfig[boardId];

  if (!siteId) {
    console.log(`No site configured for board ${boardId}`);
    return res.status(200).json({ message: 'Board not mapped' });
  }

  // Kinsta API calls added in the steps below
  res.status(200).json({ message: 'Received' });
});

app.listen(3000, () => console.log('Middleware running on port 3000'));

エンドポイントを保護するために、Webhook設定時にはシークレットキーを生成してください。Jiraでは任意項目として扱われていますが、安全に運用するうえでは実質的に必須です。

エンドポイントのセキュリティ

Jiraは各ペイロードに署名し、そのハッシュ値をsha256=<hash>形式でX-Hub-Signatureヘッダーに含めます。Webhookシークレットは、他の認証情報とあわせて.envファイルに追加します。

JIRA_WEBHOOK_SECRET=your_webhook_secret_here

検証関数はapp.js内にあり、Node.js標準のcryptoモジュールを使用します。この関数は、リクエストヘッダーから署名を取得し、生のリクエストボディをもとに期待されるHMACを計算したうえで、timingSafeEqualを使ってタイミング攻撃を防ぎながら両者を比較します。以下は、app.js内の該当箇所です。

const crypto = require('crypto');

function verifyJiraSignature(req) {
  const signature = req.headers['x-hub-signature'];
  const secret = process.env.JIRA_WEBHOOK_SECRET;
  if (!signature || !secret) return false;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

この関数は、POST /sprintルートハンドラの冒頭で呼び出されます。検証に失敗した場合、ミドルウェアは直ちに401レスポンスを返し、それ以降の処理は実行されません。

app.post('/sprint', async (req, res) => {
  if (!verifyJiraSignature(req)) {
    return res.status(401).json({ message: 'Invalid signature' });
  }

  const body = JSON.parse(req.body);
  // …rest of the handler
});

verifyJiraSignatureはHMACの計算に生のリクエストボディを必要とするため、このルートでは/sprintパスに対してexpress.raw()を使用しています。検証が完了した後は、JSON.parse(req.body)を使うことで、express.json()を使用した場合と同じ形式でデータを扱えます。

3. Kinsta APIで認証してサイト環境を取得する

Kinsta APIへのすべてのリクエストでは、Bearerトークン認証を使用します。app.js内のheaders定数が、アプリケーション全体のAPIリクエストで共通して利用されます。ファイル冒頭のrequire('dotenv').config()によって、.env内の環境変数は他の処理より先に読み込まれます。そのため、APIキーをソースコード内に直接記述する必要はありません。

また、KinstaのプロビジョニングAPIではサイトIDではなく環境IDを使用するため、headers定数の下にgetEnvironmentId関数を追加します。

const getEnvironmentId = async (siteId) => {
  const resp = await fetch(
    `${KinstaAPIUrl}/sites/${siteId}/environments`,
    { method: 'GET', headers }
  );
  const data = await resp.json();
  return data.site.environments[0].id;
};

この関数は、GET /sites/{siteId}/environmentsを呼び出し、レスポンス内の最初の環境(通常は本番環境)のIDを返します。もし対象サイトで複数の環境を使用しており、特定の環境を指定したい場合は、単純に最初の結果を使うのではなく、環境名を基準に一致するものを取得するようにしてください。

4. Kinsta APIを使用してプレーンなステージング環境を作成する

サイトIDと環境IDを取得したら、ミドルウェアはPOST /sites/{siteId}/environments/plainを呼び出してスプリント用の環境を作成します。この処理は、getEnvironmentIdの下に追加するcreateSprintEnvironment関数で実装します。

const createSprintEnvironment = async (siteId, sprintName) => {
  const resp = await fetch(
    `${KinstaAPIUrl}/sites/${siteId}/environments/plain`,
    {
      method: 'POST',
      headers,
      body: JSON.stringify({
        display_name: sprintName,
        is_premium: false
      })
    }
  );
  const data = await resp.json();
  return data;
};

display_nameは、MyKinstaに表示される環境名です。Jiraペイロードのsprint.nameをそのまま使用することで、MyKinsta上の各環境がどのスプリントに対応しているかを判別しやすくなります。is_premiumフラグは、作成する環境を標準ステージング環境にするか、プレミアムステージング環境にするかを指定します。falseに設定すると、標準ステージング環境が作成されます。

リクエストがKinstaに受け付けられると、完成した環境そのものではなく、operation_idを含む202 Acceptedレスポンスが返されます。

{
  "operation_id": "environments:add-plain-54fb80af-576c-4fdc-ba4f-b596c83f15a1",
  "message": "Adding plain environment in progress",
  "status": 202
}

Kinstaの非同期処理により、プロビジョニングが完了するまでリクエストスレッドがブロックされるのを防げます。operation_idは、処理の進捗を追跡するためにエンドポイントへ渡す識別子です。次に、POST /sprintルートを更新し、2つの関数を順番に呼び出すようにします。

app.post('/sprint', async (req, res) => {
  if (!verifyJiraSignature(req)) {
    return res.status(401).json({ message: 'Invalid signature' });
  }

  const body = JSON.parse(req.body);
  const { webhookEvent, sprint } = body;

  if (webhookEvent !== 'sprint_started') {
    return res.status(200).json({ message: 'Event ignored' });
  }

  const boardId = String(sprint.originBoardId);
  const siteId = siteConfig[boardId];

  if (!siteId) {
    console.log(`No site configured for board ${boardId}`);
    return res.status(200).json({ message: 'Board not mapped' });
  }
try {
    const envId = await getEnvironmentId(siteId);
    const result = await createSprintEnvironment(siteId, sprint.name);
    res.status(200).json(result);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Environment creation failed' });
  }
});

tryブロックを使うと、複数のif文に依存するよりも処理をすっきり書けます。ただし、Jiraの署名検証は他の処理より前に実行する必要があるため、ルートハンドラの冒頭に残してください。

5. 操作ステータスをポーリングし、プロビジョニングを確認する

プロビジョニングの完了を確認するには、createSprintEnvironmentの下にpollOperation関数を追加し、GET /operations/{operation_id}をポーリングします。ステータスが200で返されるまで、処理の進捗を確認します。

const pollOperation = async (operationId, intervalMs = 5000, maxAttempts = 12) => {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    await new Promise(resolve => setTimeout(resolve, intervalMs));
    const resp = await fetch(
      `${KinstaAPIUrl}/operations/${operationId}`,
      { method: 'GET', headers }
    );
    const data = await resp.json();
    if (data.status === 200) {
      console.log(`Environment ready: ${operationId}`);
      return data;
    }
    if (data.status >= 400) {
      throw new Error(`Operation failed: ${data.message}`);
    }
  }
  throw new Error('Operation timed out after maximum attempts');
};

このループは、各試行の間に5秒待機しながら、最大1分間プロビジョニングの完了を待ちます。200は処理完了を示し、4xxステータスが返された場合は、設定やリクエスト内容に問題がある可能性を示しています。

WordPressサイトの環境一覧を表示したMyKinsta
WordPressサイトの環境一覧を表示したMyKinsta

これらの処理をnode app.jsで実行し、Jiraでスプリントを開始すると、1〜2分以内にMyKinstaに新しい環境が表示されます。

制作会社の開発フローをスプリントに合わせて自動化する

この連携により、Jiraでスプリントを開始するだけで、MyKinsta内にクリーンで名前付きのプレーンなステージング環境を自動作成できます。Webhookが起動すると、ミドルウェアがボードIDから対象サイトを特定し、Kinsta APIが環境の作成を実行します。チームは、準備済みの環境ですぐに作業へ取りかかれます。

ミドルウェアの準備ができたら、Sevallaはシンプルなデプロイ先として利用できます。プロジェクトをGitサービスへプッシュし、リポジトリを接続して環境変数を設定したあと、JiraのWebhook URLを本番用のURLへ更新するだけです。

また、Kinstaのエージェンシーパートナープログラムは、複数のクライアント案件を管理する制作会社に適しています。専用サポートや共同マーケティングの機会に加え、Kinsta API上で構築する自動化ワークフローを支えるインフラパートナーシップも提供されています。

Joel Olawanle Kinsta

Kinstaでテクニカルエディターとして働くフロントエンド開発者。オープンソースをこよなく愛する講師でもあり、JavaScriptとそのフレームワークを中心に200件以上の技術記事を執筆している。