VS Back to Home
Ops Runbook · Cloud Foundry

Cloud Foundry Stack Migration, Without the Downtime Gamble

A systems engineer at a multi-monitor desk, surrounded by printed landscape diagrams and architecture schematics

A patch-and-restage procedure for moving applications between stacks using a rolling strategy — including the quota check most people skip until it bites them.

cflinuxfs4 → cflinuxfs5
Creator: Vladimir Savekov (SAP BASIS Consultant) Method: Patch + rolling restage Read time: ~10 min

Purpose

This procedure describes how to migrate Cloud Foundry applications from stack cflinuxfs4 to stack cflinuxfs5 using a patch + rolling restage approach — without requiring the original application source or manifest to be re-pushed.

01

Prerequisites

  • CF CLI v8.1.0 or later — required for --strategy rolling.
bash
cf version
  • jq installed — used to parse and format CF API output.
bash
jq --version
  • Proxy environment variables — set these if your network requires an outbound proxy to reach the CF API endpoint.
env
HTTP_PROXY=http://<proxy-host>:<proxy-port>
https_proxy=http://<proxy-host>:<proxy-port>
http_proxy=http://<proxy-host>:<proxy-port>
HTTPS_PROXY=http://<proxy-host>:<proxy-port>
  • Sufficient permissions (Space Developer or higher) in every targeted org/space.
  • A current inventory of affected applications and their stacks — see Section 2.
02

Inventory affected applications

List all applications across orgs/spaces along with their current stack and buildpack, so you know exactly what needs migrating.

bash
cf curl "/v3/apps?per_page=5000&include=space.organization" | jq '
  (.included.spaces | INDEX(.guid)) as $spaces |
  (.included.organizations | INDEX(.guid)) as $orgs |
  [ .resources[] | {
      app: .name,
      org: $orgs[$spaces[.relationships.space.data.guid].relationships.organization.data.guid].name,
      space: $spaces[.relationships.space.data.guid].name,
      lifecycle
  } ]'

Organize the output into a table, sorted by stack and space, so migration targets are clear:

StackSpaceAppBuildpack
cflinuxfs4env-1app-abuildpack-x
cflinuxfs4env-1app-bbuildpack-y
cflinuxfs4env-2app-abuildpack-x
cflinuxfs5env-3app-a(auto-detected)

Inspect any individual application in more detail before migrating it:

bash
cf app <app-name>
i

Watch the instance countPay attention to the instances field in the output — this affects whether the migration can be truly zero-downtime. More on this in Section 4.

Check instance count for one app

bash
cf app <app-name> | grep instances

Check instance counts for every app in the current space

bash
cf curl "/v3/apps?per_page=5000" | jq -r '.resources[].guid' | while read guid; do
  name=$(cf curl /v3/apps/$guid | jq -r '.name')
  instances=$(cf curl /v3/apps/$guid/processes | jq '[.resources[] | select(.type=="web")][0].instances')
  echo "$name: $instances"
done
03

Migration steps (per application)

1

Patch the desired stack

This updates the application's desired-state record only. It does not rebuild or redeploy anything by itself.

bash
cf curl /v3/apps/$(cf app <app-name> --guid) -X PATCH \
  -d '{"lifecycle":{"type":"buildpack","data":{"stack":"cflinuxfs5"}}}'
2

Restage with a rolling strategy

This is the step that actually rebuilds the droplet against the new stack and performs the deployment.

bash
cf restage <app-name> --strategy rolling

Rolling restage starts new instance(s) on the new stack, health-checks them, and only then removes the old instance(s) — reducing (but not always eliminating) downtime.

04

Important: instance count affects downtime

!

--strategy rolling only reduces downtime risk with 2+ instances.With a single instance (1/1), there is no second instance to serve traffic while the new one starts, so a brief gap is still likely.

Recommended for single-instance apps

bash
cf scale <app-name> -i 2
cf restage <app-name> --strategy rolling
cf scale <app-name> -i 1   # scale back down if 1 is the normal desired state

Risks of temporary scale-up

  • Quota — doubles memory consumption while at 2 instances; confirm org/subaccount has headroom before scaling, or the scale may fail.
  • Shared state — if the app holds local/in-memory session state, briefly running 2 instances can cause inconsistent behavior for users mid-request (not downtime, but a data-consistency risk).
  • Scaling up/down itself does not cause downtime — the existing instance keeps serving throughout.
→

Checking quota headroom before scalingIf you want to check the headroom before scaling up, you can utilize this script — it generates a human-readable table of memory usage.

05

Batch migration script (per org/space)

bash
cf target -o "<org-name>" -s "<space-name>"

for app in app-a app-b app-c; do
  guid=$(cf app "$app" --guid)
  echo "Patching $app ($guid) to cflinuxfs5..."
  cf curl /v3/apps/$guid -X PATCH \
    -d '{"lifecycle":{"type":"buildpack","data":{"stack":"cflinuxfs5"}}}'

  echo "Restaging $app with rolling strategy..."
  cf restage "$app" --strategy rolling
done

Repeat per org/space, migrating lower environments first (e.g. dev → qa → prod) and validating at each stage before proceeding.

06

Validation checklist (per application)

  • cf app <app-name> shows the new stack.
  • Droplet was actually rebuilt (staging timestamp / droplet GUID changed — not just lifecycle metadata).
  • Buildpack version used after migration matches expectations (newer stacks may auto-detect newer buildpack versions — check for runtime version drift).
  • cf logs <app-name> --recent shows no staging or startup errors.
  • Application routes respond as expected (health check / smoke test).
  • Instance count restored to its normal desired state, if temporarily scaled up.
07

Rollback

If an application fails to start cleanly on the new stack:

bash
cf curl /v3/apps/$(cf app <app-name> --guid) -X PATCH \
  -d '{"lifecycle":{"type":"buildpack","data":{"stack":"cflinuxfs4"}}}'
cf restage <app-name> --strategy rolling
08

Notes and caveats

  • Task-like or one-shot applications (e.g. database migration/deployer jobs) may not benefit from --strategy rolling the same way long-running services do — evaluate case by case whether rolling restage is appropriate, or whether a simple restage/redeploy suffices.
  • Always migrate and validate lower environments before touching production.
  • Keep a second terminal open on cf logs <app-name> --recent during each restage to catch failures early.

Appendix A — Memory usage report script

This helper script displays a quick overview of an org's memory quota (total / used / free) alongside a per-app memory usage breakdown for the currently targeted space. Useful for confirming quota headroom before scaling an app up — see Section 4.

✓

No modifications requiredSimply target the org/space you want to check, then run the script as-is.

bash
cf target -o "<org-name>" -s "<space-name>"
memory_usage_table.sh
#!/usr/bin/env bash
# Displays org memory quota (total / used / free) plus a per-app memory usage
# breakdown for the currently targeted org/space.
#
# Requires: cf CLI (logged in + targeted), jq
# Usage:    ./memory_usage_table.sh
#           Auto-detects a 'cf' binary on PATH, or falls back to ./cf in the
#           current directory if 'cf' is not found on PATH.

set -euo pipefail

if command -v cf >/dev/null 2>&1; then
  CF="cf"
elif [[ -x "./cf" ]]; then
  CF="./cf"
else
  echo "Error: 'cf' not found on PATH and no executable ./cf in current directory." >&2
  exit 1
fi

# ---- Org-level quota summary -------------------------------------------

org_name=$($CF target | sed -n 's/^org:[[:space:]]*//p')
space_name=$($CF target | sed -n 's/^space:[[:space:]]*//p')
org_guid=$($CF org "$org_name" --guid)
quota_name=$($CF org "$org_name" | sed -n 's/^quota:[[:space:]]*//p')

total_mb=$($CF quota "$quota_name" | sed -n 's/^total memory:[[:space:]]*//p')
used_mb=$($CF curl "/v3/organizations/${org_guid}/usage_summary" | jq -r '.usage_summary.memory_in_mb')

# normalize total_mb ("200G" / "20480M" / plain number) to MB
if [[ "$total_mb" == *G ]]; then
  total_mb=$(( ${total_mb%G} * 1024 ))
elif [[ "$total_mb" == *M ]]; then
  total_mb=${total_mb%M}
fi

free_mb=$(( total_mb - used_mb ))

echo "=================================================================="
printf "ORG:   %s\n" "$org_name"
printf "SPACE: %s\n" "$space_name"
echo "=================================================================="
echo ""
printf "%-20s %-15s\n" "METRIC" "MEMORY (MB)"
printf "%-20s %-15s\n" "--------------------" "---------------"
printf "%-20s %-15s\n" "Total Quota" "${total_mb}MB"
printf "%-20s %-15s\n" "Current Usage" "${used_mb}MB"
printf "%-20s %-15s\n" "Free" "${free_mb}MB"
echo ""

# ---- Per-app memory breakdown (current space) --------------------------

echo "Per-app memory usage in space '$space_name':"
echo ""
printf "%-30s %-12s %-15s %-15s\n" "APP" "INSTANCES" "MEM/INSTANCE" "TOTAL MEM"
printf "%-30s %-12s %-15s %-15s\n" "------------------------------" "------------" "---------------" "---------------"

space_guid=$($CF curl "/v3/spaces?names=${space_name}&organization_guids=${org_guid}" | jq -r '.resources[0].guid // empty')

app_total_mb=0

if [[ -z "$space_guid" ]]; then
  printf "%-30s\n" "(could not resolve space GUID)"
else
  apps=$($CF curl "/v3/apps?space_guids=${space_guid}&per_page=5000" | jq -r '.resources[]? | "\(.guid) \(.name)"')

  if [[ -z "$apps" ]]; then
    printf "%-30s\n" "(no apps found in this space)"
  else
    while read -r guid name; do
      [[ -z "$guid" ]] && continue
      proc=$($CF curl "/v3/apps/${guid}/processes" | jq '[.resources[]? | select(.type=="web")][0] // {}')
      instances=$(echo "$proc" | jq -r '.instances // 0')
      mem=$(echo "$proc" | jq -r '.memory_in_mb // 0')
      this_total=$(( instances * mem ))
      app_total_mb=$(( app_total_mb + this_total ))

      printf "%-30s %-12s %-15s %-15s\n" "$name" "$instances" "${mem}MB" "${this_total}MB"
    done <<< "$apps"
  fi
fi

printf "%-30s %-12s %-15s %-15s\n" "------------------------------" "------------" "---------------" "---------------"
printf "%-30s %-12s %-15s %-15s\n" "TOTAL (this space)" "" "" "${app_total_mb}MB"
echo ""
echo "Note: 'Current Usage' above is org-wide (all spaces); the per-app table"
echo "only covers apps in the currently targeted space."

Usage

bash
chmod +x memory_usage_table.sh
cf target -o "<org-name>" -s "<space-name>"
./memory_usage_table.sh

Sample output

output
==================================================================
ORG:   <org-name>
SPACE: <space-name>
==================================================================

METRIC               MEMORY (MB)
-------------------- ---------------
Total Quota          204800MB
Current Usage        1280MB
Free                 203520MB

Per-app memory usage in space '<space-name>':

APP                            INSTANCES    MEM/INSTANCE    TOTAL MEM
------------------------------ ------------ --------------- ---------------
app-a                          1            256MB           256MB
app-b                          1            256MB           256MB
app-c                          1            512MB           512MB
------------------------------ ------------ --------------- ---------------
TOTAL (this space)                                           1024MB

Note: 'Current Usage' above is org-wide (all spaces); the per-app table
only covers apps in the currently targeted space.