0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloud Foundryで環境ごとにアプリケーションの設定を変える方法 — mtaext, Cloud Transport Management, CI/CD

Last updated at Posted at 2025-06-27

はじめに

Cloud Foundryの本番環境でアプリケーションを実行する場合、最低2つのインスタンスを立ち上げておくのがベストプラクティスとされています。

You may have a rule dictating apps will always have at least 2 instances running to distribute the workload. This is actually a best practice and encouraged to be adopted.

引用:SAP BTP runtimes, my personal considerations and preferences on Cloud Foundry, Kyma, ABAP runtimes

また、Application Autoscalerを使って負荷状況に応じてインスタンス数を増減することも推奨されます。

一方で、本番環境以外ではCloud Foundryのリソースを節約したいので、インスタンスは1つで十分です。環境ごとに設定を変えたい場合はどうしたらよいでしょうか?

mtaextを使う

MTAアプリケーションをデプロイする際にmtaextという拡張ファイルを使用することができます。環境別にmtaextファイルを用意しておき、ビルドまたはデプロイの際にmtaextファイルを指定することで環境ごとの追加の設定を入れることができます。

mtaextファイルの記載例

test.mtaext
_schema-version: 3.3.0
ID: cap-mtaext-ext
extends: cap-mtaext # mta.yamlのID
version: 1.0.0 # mta.yamlのversion

# 拡張する対象のモジュールまたはリソースと設定値
modules:
  - name: cap-mtaext-srv
    parameters:
      instances: 2

mtaextファイルの中で新規のresourcesを定義することはできません。できるのは、既存のモジュールまたはリソースの拡張のみです。

ビルド時に取り込む

mbt build --extensions test.mtaext

デプロイ時に取り込む

cf deploy <mtar> -e test.mtaext

Cloud Transport Managementでは一つのMTAアーカイブを移送するので、mtaextはデプロイ時に取り込みます。Cloud Transport ManagementのUIでは、事前にノードの"MTA Extension Descriptors"ボタンからmtaextファイルをアップロードしておくことでMTAのインポート時に取り込まれます。

image.png

とはいえ、マニュアルでアップロードするという運用は現実的でないので、Continuous Integration and Delivery Serviceを使って自動化する方法を検討します。

シナリオ

CAPで作ったアプリケーションをContinuous Integration and Delivery Serviceでビルドし、Cloud Transport Management経由でデプロイします。移送先は"test"サブアカウントです。"test"環境では、アプリケーションのインスタンスを2つにするとともにApplication Autoscalerでスケーリングの設定をします。

image.png

前提

Cloud Transport Managementにtrial環境、test環境に対応したノードを作成し、移送ルートを設定しておきます。
参考:Configuring the Landscape

image.png

mtaextを使用したデプロイ手順

プロジェクトの設定

CAPのプロジェクトで以下の設定を行います。ソースコードは以下に格納しています。

mta.yaml

Application Autoscalerのサービスインスタンスはmtaextで追加することができないので、mta.yamlに定義しています。trial環境へはスケーリングのための設定がない状態でデプロイされます。

_schema-version: 3.3.0
ID: cap-mtaext
version: 1.0.0
description: "A simple CAP project."
parameters:
  enable-parallel-deployments: true
build-parameters:
  before-all:
    - builder: custom
      commands:
        - npm ci
        - npx cds build --production
modules:
  - name: cap-mtaext-srv
    type: nodejs
    path: gen/srv
    parameters:
      instances: 1
      buildpack: nodejs_buildpack
      memory: 256M
    build-parameters:
      builder: npm-ci
    provides:
      - name: srv-api # required by consumers of CAP services (e.g. approuter)
        properties:
          srv-url: ${default-url}
    requires:
      - name: cap-mtaext-auth
      - name: cap-mtaext-autoscaler

resources:
  - name: cap-mtaext-auth
    type: org.cloudfoundry.managed-service
    parameters:
      service: xsuaa
      service-plan: application
      path: ./xs-security.json
      config:
        xsappname: cap-mtaext-${org}-${space}
        tenant-mode: dedicated
  # リソースはmtaextで追加できないのでここで定義      
  - name: cap-mtaext-autoscaler
    type: org.cloudfoundry.managed-service
    parameters:
      service: autoscaler
      service-plan: standard

test.mtaext

ここでは2つの設定を行っています。

  • アプリケーションのインスタンスの数を2つにする
  • Application Autoscalerの設定を行う(参考:Dynamic Scaling Policy
_schema-version: 3.3.0
ID: cap-mtaext-ext
extends: cap-mtaext
version: 1.0.0

modules:
  - name: cap-mtaext-srv
    parameters:
      instances: 2
    requires:
      - name: cap-mtaext-autoscaler
        parameters:
          config:
            instance_min_count: 2
            instance_max_count: 3
            scaling_rules:
              - metric_type: cpuutil
                threshold: 80
                breach_duration_secs: 60
                cool_down_secs: 300
                operator: ">"
                adjustment: "+1"
              - metric_type: cpuutil
                threshold: 60
                breach_duration_secs: 60
                cool_down_secs: 300
                operator: "<="
                adjustment: "-1"

Clout Transport Managementにmtaextをアップロードするためのスクリプト

以下のスクリプトでは、Cloud Transport Managementの未公開のAPIを使用してmtaextをアップロードしています。このためAPIの仕様変更などでスクリプトが動かなくなる可能性があります。

関連Q&A
https://community.sap.com/t5/technology-q-a/how-to-upload-mtaext-file-to-a-node-in-sap-cloud-transport-management-via/qaq-p/14134185

プロジェクトのルートに以下のファイルを作成します。

upload-mtaext.js

const fs = require('fs');
const axios = require('axios');
const FormData = require('form-data');

// Validate required environment variables
const {
  CTMS_SERVICE_KEY,
  NODE_ID,
  MTA_ID,
  MTA_VERSION,
  DESCRIPTION,
  MTAEXT_NAME
} = process.env;

if (!CTMS_SERVICE_KEY || !NODE_ID || !MTA_ID || !MTA_VERSION || !DESCRIPTION || !MTAEXT_NAME) {
  console.error("❌ Required environment variables are missing");
  process.exit(1);
}

console.log("🚀 Starting .mtaext upload");

let serviceKey;
try {
  serviceKey = JSON.parse(CTMS_SERVICE_KEY);
} catch (err) {
  console.error("❌ Failed to parse CTMS_SERVICE_KEY as JSON");
  process.exit(1);
}

const CLIENT_ID = serviceKey.uaa.clientid;
const CLIENT_SECRET = serviceKey.uaa.clientsecret;
const OAUTH_URL = serviceKey.uaa.url + '/oauth/token';
const TMS_URL = serviceKey.uri;

async function getAccessToken() {
  try {
    const response = await axios.post(OAUTH_URL, new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    }), {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    });
    return response.data.access_token;
  } catch (err) {
    console.error("❌ Failed to retrieve access token");
    console.error("Error code:", err.response ? err.response.status : 'N/A');
    console.error("Error details:", err.message);
    process.exit(1);
  }
}

async function uploadMtaext(token) {
  const form = new FormData();
  form.append('file', fs.createReadStream(MTAEXT_NAME));
  form.append('mtaVersion', MTA_VERSION);
  form.append('description', DESCRIPTION);

  try {
    const response = await axios.post(`${TMS_URL}/v2/nodes/${NODE_ID}/mtaExtDescriptors`, form, {
      headers: {
        ...form.getHeaders(),
        Authorization: `Bearer ${token}`
      },
      validateStatus: () => true
    });

    return response.status;
  } catch (err) {
    console.error("❌ Upload request failed", err.message);
    process.exit(1);
  }
}

async function findAndDeleteExistingDescriptor(token) {
  try {
    const res = await axios.get(`${TMS_URL}/v2/nodes/${NODE_ID}/mtaExtDescriptors`, {
      headers: { Authorization: `Bearer ${token}` }
    });

    const match = res.data.mtaExtDescriptors.find(d => d.mtaId === MTA_ID && d.mtaVersion === MTA_VERSION);
    if (!match) {
      console.error(`❌ Descriptor with mtaId=${MTA_ID} and version=${MTA_VERSION} not found`);
      process.exit(1);
    }

    console.log(`🗑️ Deleting existing descriptor ID ${match.id}...`);
    await axios.delete(`${TMS_URL}/v2/nodes/${NODE_ID}/mtaExtDescriptors/${match.id}`, {
      headers: { Authorization: `Bearer ${token}` }
    });
    console.log("✅ Deleted descriptor");
  } catch (err) {
    console.error("❌ Failed to find/delete existing descriptor", err.message);
    process.exit(1);
  }
}

async function main() {
  const token = await getAccessToken();
  console.log("🔐 Access token retrieved");

  console.log("📤 Uploading .mtaext (initial attempt)...");
  const status = await uploadMtaext(token);

  if (status === 201) {
    console.log("✅ Upload succeeded (201 Created)");
    process.exit(0);
  } else if (status !== 422) {
    console.error(`❌ Upload failed with unexpected status ${status}`);
    process.exit(1);
  }

  console.warn("⚠️ Upload failed with 422 — checking for existing descriptor to delete...");
  await findAndDeleteExistingDescriptor(token);

  console.log("📤 Retrying upload...");
  const retryStatus = await uploadMtaext(token);
  if (retryStatus === 201) {
    console.log("✅ Re-upload succeeded");
  } else {
    console.error(`❌ Re-upload failed with status ${retryStatus}`);
    process.exit(1);
  }
}

main();

スクリプトを動かすのに必要なモジュールをpackage.jsonに指定しておきます。

  "devDependencies": {
    ...
    "axios": "^1.10.0",
    "form-data": "^4.0.3"
  }

インプット

変数 説明
CTMS_SERVICE_KEY Cloud Transport Managementのサービスキーを登録したCredentialの名前 ctmskey
NODE_ID mtaextファイルをアップロードするノードのID(名前ではなく数字)※ 4
MTA_ID mtaextファイルが拡張するmtaのID cap-mtaext
MTA_VERSION mtaextファイルが拡張するmtaのバージョン 1.0.0の形式、または *
DESCRIPTION mtaextファイルの説明 test
MTAEXT_NAME mtaextファイルの名前 test.mtaext

※ノードIDは以下のAPIで取得できる(このAPIも未公開)

GET {{apiUrl}}/v2/nodes
Authorization: Bearer {{TokenRequest.response.body.access_token}}

処理の流れ

  1. 環境変数からCloud Transport Managementの認証情報を取得
  2. mtaextファイルをアップロード(同じファイルが存在する場合、422 Unprocessable Contentのエラーが返る)
  3. 422エラーの場合は既存のmtaextファイルを削除し、再アップロード

Continuous Integration and Deliveryのための設定

Credentialsの設定

Cloud Transport Managementのサービスキーを登録します。名前は任意です。
image.png

リポジトリの設定

デプロイ対象のプロジェクトが格納されたGitリポジトリを登録します。
image.png

ジョブの設定

以下の内容でジョブを設定します。

Pipeline: Cloud Foundry Environment
image.png

Build Tool: mta
Build Tool Version: Java 21 Node 22
image.png

Releaseステージでは、Cloud Transport Managementへのアップロードの設定をします。
Transport Operation: Upload to / <アップロード先のノード>
Service Key: <Credentialsで登録したサービスキーの名前>
image.png

Releaseステージの"Additional Commands"でmtaextアップロード用のスクリプトを指定し、必要な変数を設定します。コマンドを追加するときに"Run Last in Stage"を選択してください。
image.png

Command: npm install && node upload-mtaext.js
Additional Credentials, Additional Variables: スクリプトのインプットを参照

image.png

ビルド、デプロイ

実行前、TESTノードのMTA Extension Descriptorsは空です。
image.png

ジョブを実行してアプリケーションをビルド、デプロイします。
image.png

Releaseステージのログを見ると、mtaextファイルのアップロードが正常に終了したことがわかります。
image.png

TESTノードのMTA Extension Descriptorsにアップロードしたmtaextが入っています。
image.png

Cloud Transport Managementでインポート

DEV (=trial)、TESTの各ノードで移送依頼をインポートします。

image.png

trial環境

アプリケーションインスタンスは1つです。
image.png

Application Autoscalerのインスタンスはありますが、スケーリングの設定はありません。
image.png

test環境

アプリケーションインスタンスは2つです。
image.png

Application Autoscalerにスケーリングの設定が入っています。
image.png

2回目のジョブ実行

アプリケーションの設定を変えずにジョブを実行します。TESTノードにはすでにmtaextファイルが存在するため、mtaextの削除と再アップロードが行われます。

image.png

おわりに

本記事では、環境ごとに設定を変える方法としてmtaextを利用し、CI/CD パイプラインからCloud Transport ManagementにAPIを使ってmtaextファイルを自動アップロードする手順を紹介しました。現在このAPIは未公開なので、公式なAPIとして公開されることを望みます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?