はじめに
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ファイルの記載例
_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のインポート時に取り込まれます。
とはいえ、マニュアルでアップロードするという運用は現実的でないので、Continuous Integration and Delivery Serviceを使って自動化する方法を検討します。
シナリオ
CAPで作ったアプリケーションをContinuous Integration and Delivery Serviceでビルドし、Cloud Transport Management経由でデプロイします。移送先は"test"サブアカウントです。"test"環境では、アプリケーションのインスタンスを2つにするとともにApplication Autoscalerでスケーリングの設定をします。
前提
Cloud Transport Managementにtrial環境、test環境に対応したノードを作成し、移送ルートを設定しておきます。
参考:Configuring the Landscape
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の仕様変更などでスクリプトが動かなくなる可能性があります。
プロジェクトのルートに以下のファイルを作成します。
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}}
処理の流れ
- 環境変数からCloud Transport Managementの認証情報を取得
- mtaextファイルをアップロード(同じファイルが存在する場合、
422 Unprocessable Contentのエラーが返る) - 422エラーの場合は既存のmtaextファイルを削除し、再アップロード
Continuous Integration and Deliveryのための設定
Credentialsの設定
Cloud Transport Managementのサービスキーを登録します。名前は任意です。

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

ジョブの設定
以下の内容でジョブを設定します。
Pipeline: Cloud Foundry Environment

Build Tool: mta
Build Tool Version: Java 21 Node 22

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

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

Command: npm install && node upload-mtaext.js
Additional Credentials, Additional Variables: スクリプトのインプットを参照
ビルド、デプロイ
実行前、TESTノードのMTA Extension Descriptorsは空です。

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

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

Cloud Transport Managementでインポート
DEV (=trial)、TESTの各ノードで移送依頼をインポートします。
trial環境
Application Autoscalerのインスタンスはありますが、スケーリングの設定はありません。

test環境
Application Autoscalerにスケーリングの設定が入っています。

2回目のジョブ実行
アプリケーションの設定を変えずにジョブを実行します。TESTノードにはすでにmtaextファイルが存在するため、mtaextの削除と再アップロードが行われます。
おわりに
本記事では、環境ごとに設定を変える方法としてmtaextを利用し、CI/CD パイプラインからCloud Transport ManagementにAPIを使ってmtaextファイルを自動アップロードする手順を紹介しました。現在このAPIは未公開なので、公式なAPIとして公開されることを望みます。








