--- title: "How To: Use Right-Size Recommendations within a CI-CD pipeline" slug: "how-to-use-right-size-recommendations-within-ci-cd-pipeline" updated: 2026-10-05T13:49:33Z published: 2026-10-05T13:49:33Z canonical: "resources.realtheory.io/how-to-use-right-size-recommendations-within-ci-cd-pipeline" --- > ## Documentation Index > Fetch the complete documentation index at: https://resources.realtheory.io/llms.txt > Use this file to discover all available pages before exploring further. # How To: Use Right-Size Recommendations within a CI-CD pipeline After deploying the RealTheory collector to a Kubernetes cluster (See [Installing RealTheory in a Cluster](/v1/docs/installing-realtheory-in-a-cluster)), the cluster and its associated objects, including any Recommendations, become available in the RealTheory API within minutes. ### Overview RealTheory analyzes how your Kubernetes workloads actually use CPU and memory, and recommends new requests and limits for each container. These right-size recommendations can be retrieved from a CI/CD pipeline, decoded, and applied to your deployment manifests so that the next deploy picks them up. This is a four-step process: 1. **Retrieve the right-size recommendations for a workload** using the `Recommendations` endpoint with a query scoped to the workload's cluster, namespace, and name. Example query: `classification=RightSizeWorkload AND metadata.clusterName=prod-east AND metadata.namespaceName=checkout AND metadata.objectName=checkout-api` 2. **Choose the recommendation to use**, skipping any recommendation that your team has dismissed or postponed in the RealTheory console. 3. **Decode the container entries** held in the recommendation's `properties`. 4. **Apply the recommended values to your manifest**, leaving any field without a recommendation unchanged. A [worked example](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#worked-example) and a [sample Python implementation](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#sample-implementation-python) follow the steps. **Notes:** - All calls to RealTheory APIs must be authenticated and authorized. See [Authentication](/v1/docs/authentication) for more details. - Queries should be URL encoded; otherwise the request might be rejected. ### Step 1: Retrieve the right-size recommendations for a workload To get the right-size recommendations for a workload, call the method below on the [K8sRecommendations](https://api-dev.realtheory.io/swagger/k8srecommendations/index.html) endpoint. ``` GET /theory/api/v1/k8srecommendations/recommendations/search/query/{query}?index={index}&count={count}&order={order}&direction={direction} ``` where - `{endpoint}` - is the URL of the RealTheory SaaS portal for your account. - `{query}` - is a [query](/v1/docs/queries) used to limit the results to right-size recommendations for a given workload. - `{index}` - is the starting index of the recommendations to include in the list. This parameter is optional and defaults to 0 if not specified. - `{count}` - is the maximum number of recommendations to return, up to a limit of 500. This parameter is optional and defaults to 100 if not specified. - `{order}` - is a string containing the name of the attribute to sort on. This parameter is optional. - `{direction}` - is a number that controls the direction of the sort. A value equal to or greater than 0 indicates ascending order; a value less than 0 indicates descending order. This parameter is optional and defaults to 0 if not specified. The `{query}` should include the following conditions: - `classification = RightSizeWorkload` - returns only workload right-size recommendations, rather than other kinds of recommendation for the same workload. - `metadata.clusterName`, `metadata.namespaceName`, and `metadata.objectName` - scope the results to a single workload. Workload names are not unique across clusters and namespaces, so all three should be included. `metadata.objectName` is the workload's `metadata.name` in your manifest. - `metadata.objectType` - is optional, and is the workload kind, for example `Deployment`, `StatefulSet`, or `DaemonSet`. Include it if a namespace can contain workloads of different kinds with the same name. See [Metadata](/v1/docs/metadata) for more details on the metadata fields available to queries. #### Example ``` GET theory/api/v1/k8srecommendations/recommendations/search/query/classification=RightSizeWorkload AND metadata.clusterName=prod-east AND metadata.namespaceName=checkout AND metadata.objectName=checkout-api?index=0&count=100 ``` which returns the right-size recommendations for the workload `checkout-api` in namespace `checkout` on cluster `prod-east`. **Notes:** - The query forms part of the path, so it must be URL encoded. For example, spaces become `%20`. - See [Queries](/v1/docs/queries) for the full query syntax, including the supported operators and logical connectors. - Use names exactly as they appear in Kubernetes: the cluster name as shown in the RealTheory console, and the namespace and workload names from your manifest. - If your pipeline already knows the cluster, namespace, or workload ID, use `metadata.clusterId`, `metadata.namespaceId`, or `metadata.objectId` in place of `metadata.clusterName`, `metadata.namespaceName`, or `metadata.objectName` respectively. Queries on IDs are indexed and faster than queries on names, which are not indexed, so IDs should be preferred where available. If you know the workload ID, `classification=RightSizeWorkload AND metadata.objectId=<workload-id>` is sufficient on its own: the workload ID is guaranteed to be unique across your fleet, so the cluster and namespace conditions are not needed. See [Objects](/v1/docs/objects) for more details on querying related objects. - Results are paged, and the response's `x-total-count` header gives the total number of matches. A fully scoped query normally returns at most one recommendation. See [Paging](/v1/docs/paging) for more details on how to page through all results. - Expired recommendations are not returned. - Your credentials only return recommendations for clusters you are authorized to view. #### Output Returns an array of [Recommendation](/v1/docs/k8srecommendations-recommendation) objects. See the [worked example](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#worked-example) below for a sample right-size recommendation. ### Step 2: Choose the recommendation to use The query does most of the filtering. Three checks remain on your side: - **Skip dismissed recommendations**, where `state` is `Dismissed`. Your team has dismissed them in the RealTheory console. - **Skip postponed recommendations**, where `state` is `Postponed` and `postponedUntil` is still in the future. Your team has postponed them in the RealTheory console, so they should not be applied yet. - **If more than one recommendation remains**, use the one with the latest `updated` timestamp. **Notes:** - `state` is one of `New`, `Read`, `Postponed`, `Dismissed`, or `Unknown`. Respecting `Dismissed` and `Postponed` means your pipeline honors the decisions your team makes in the RealTheory console. RealTheory's own automation applies the same rule. - You may also want to gate on how much evidence the recommendation rests on. See `right-size-metrics-span` and `confidence` under [Recommendation Properties](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#recommendation-properties) below. ### Step 3: Decode the container entries The recommended values are carried in the recommendation's `properties` object, which is a map of string keys to string values. #### Recommendation Properties - `right-size-container-count`: Represents the number of container entries, numbered from `0`, for example `2`. Its presence also identifies a right-size recommendation. - `right-size-container-{n}`: Represents one entry per container, for `n` from `0` to count − 1. See [Container Entries](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#container-entries) below. - `right-size-strategy`: Represents the optimization strategy used. Supported values are `performance`, `cost`, and `balanced`. - `right-size-basis`: Represents why the change is recommended. Supported values are `cost`, `reliability`, and `performance`. The value may be a comma-separated list, for example `performance,cost`, and should be treated as case-insensitive with whitespace trimmed. This property is optional. - `right-size-metrics-span`: Represents how much metric history the recommendation is based on, for the least-observed container, for example `3d2h30m`. The value is expressed in days, hours, and minutes in descending order, and is capped at `8d`. This property is optional: if it is missing, the span is unknown, which is not the same as `0m` (measured, under a minute). **Note:** The top-level `confidence` field of the [Recommendation](/v1/docs/k8srecommendations-recommendation) (`None`, `Low`, `Medium`, or `High`) is also useful for gating. #### Container Entries Each `right-size-container-{n}` value is a list of `key=value` fields separated by semicolons: ``` name=checkout-api;cpu-request=100m->250m;cpu-limit=->500m;memory-request=70Mi->128Mi;memory-limit=210Mi->256Mi;replicas=3;cost-impact=$40.32; ``` where - `name` - is the container name, matching `spec.containers[].name` in your pod template, for example `checkout-api`. - `cpu-request` - is the CPU request, in the form currently deployed value `->` recommended value, for example `100m->250m`. - `cpu-limit` - is the CPU limit, in the same form, for example `<not set>->500m`. - `memory-request` - is the memory request, in the same form, for example `70Mi->128Mi`. - `memory-limit` - is the memory limit, in the same form, for example `210Mi->256Mi`. - `replicas` - is the replica count used to estimate cost, for example `3`. This value is informational only: the recommendation never changes your replica count. - `cost-impact` - is the estimated cost impact in US dollars, formatted as currency, for example `$40.32`. It can include thousands separators (`$1,234.56`) and can be negative. This value is informational only. **Notes:** - The left-hand side of `->` is the value RealTheory observed deployed when it made the recommendation. The right-hand side is the recommended value. - `<not set>` means there is no value: on the left, the field was not set on the deployed container; on the right, there is no recommendation for that field. #### CPU and Memory Limits RealTheory can be configured to include or leave out CPU limits and memory limits in its recommendations. Requests are always included. By default: - **CPU limits** are not included, so the recommended value is `<not set>`. - **Memory limits** are included, so the recommended value is a quantity, for example `512Mi`. With the default configuration, each container's `cpu-limit` ends in `-><not set>`. Following the rules in [Step 4](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#step-4-apply-the-values-to-your-manifest), your pipeline leaves the CPU limits in your manifests as they are and updates the memory limits. If you want CPU limits recommended as well, or do not want memory limits recommended, ask your RealTheory administrator to change the configuration. Your parser does not need to change: it simply sees quantities instead of `<not set>`, or the other way round. Values are standard Kubernetes resource quantities that can be written straight into a manifest: - **CPU** - millicores below one core (`2m`, `250m`), otherwise cores with up to four decimal places (`1`, `1.5`, `2.25`). - **Memory** - `Gi`, `Mi`, or `Ki`, whichever represents the value exactly (`1Gi`, `1.5Gi`, `128Mi`, `1500Ki`). #### Parsing Rules Following these rules keeps your parser compatible with future additions to the format: 1. Read `right-size-container-count` as an integer. If it is missing or not a number, the recommendation is not a right-size recommendation. 2. For each `n` from `0` to count − 1, read `right-size-container-{n}`. Skip any index that is missing or empty. 3. Split the entry on `;` and ignore empty parts. The entry ends with a trailing `;`. 4. Split each part at the **first** `=` into key and value, and trim whitespace from both. Ignore parts without an `=`. 5. Keys are lower-case. **Ignore keys you do not recognize**, as new fields may be added. 6. Split each resource value at the **first** `->` into deployed and recommended. If there is no `->`, treat the whole value as the recommended value. 7. Compare `<not set>` case-insensitively, and treat it as "no value". **Note:** Container names cannot contain `;` or `=` (Kubernetes restricts them to lower-case letters, digits, and `-`), so no escaping is needed. ### Step 4: Apply the values to your manifest Each container entry maps to the container with the same `name` in your workload's pod template. For a Deployment, StatefulSet, or DaemonSet, that is `spec.template.spec.containers[]`: - `cpu-request` - maps to `resources.requests.cpu`. - `cpu-limit` - maps to `resources.limits.cpu`. - `memory-request` - maps to `resources.requests.memory`. - `memory-limit` - maps to `resources.limits.memory`. Apply these rules: - **If the recommended value is a quantity**, set the manifest field to it. - **If the recommended value is `<not set>`**, leave the manifest field as it is. Do not add it and do not remove it: the recommendation has nothing to say about that field. RealTheory's own automation applies the same rule. - **If a container is in the recommendation but not in your manifest**, ignore it. - **If a container is in your manifest but not in the recommendation**, leave it unchanged. - **Keep each request at or below its limit.** Kubernetes rejects a container whose request exceeds its limit. This can happen when you apply a recommended request but keep an existing limit because the recommended limit is `<not set>`. In that case, skip that request change and flag it for review rather than writing a manifest that will not deploy. #### Detecting Manifest Drift The deployed (left-hand) values tell you what the recommendation was calculated against. Before applying a field, compare its deployed value with the value currently in your manifest. If they differ, the manifest has changed since the recommendation was made: someone may have tuned it by hand, or it has not been deployed yet. Consider skipping those fields and flagging them for review. **Note:** Compare the values as quantities, not as text: `1000m` and `1` are the same CPU, and `1024Mi` and `1Gi` are the same memory. ### Worked Example The query from [Step 1](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#step-1-retrieve-the-right-size-recommendations-for-a-workload) returns the [Recommendation](/v1/docs/k8srecommendations-recommendation) below. It uses the [default limit configuration](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#cpu-and-memory-limits), so both CPU limits are `<not set>` on the recommended side. ``` [ { "id": "6f1c2a9e-3d4b-4c1a-9b8e-2f7a5d0c1e34", "metadata": { "objectType": "Deployment", "objectName": "checkout-api", "clusterName": "prod-east", "namespaceName": "checkout", ... }, "state": "New", "classification": "RightSizeWorkload", "confidence": "High", "properties": { "right-size-strategy": "balanced", "right-size-basis": "cost", "right-size-metrics-span": "8d", "right-size-container-count": "2", "right-size-container-0": "name=checkout-api;cpu-request=500m->250m;cpu-limit=->;memory-request=512Mi->384Mi;memory-limit=1Gi->512Mi;replicas=3;cost-impact=$40.32;", "right-size-container-1": "name=log-shipper;cpu-request=100m->20m;cpu-limit=200m->;memory-request=64Mi->32Mi;memory-limit=128Mi->64Mi;replicas=3;cost-impact=$6.10;" }, "updated": "2026-10-04T08:15:00Z", "postponedUntil": null, ... } ] ``` **Note:** Content has been omitted from the JSON response above for brevity. The recommendation decodes to: | Container | Field | Deployed | Recommended | Action | | --- | --- | --- | --- | --- | | `checkout-api` | CPU request | `500m` | `250m` | Set to `250m` | | `checkout-api` | CPU limit | not set | not set | Leave as is | | `checkout-api` | Memory request | `512Mi` | `384Mi` | Set to `384Mi` | | `checkout-api` | Memory limit | `1Gi` | `512Mi` | Set to `512Mi` | | `log-shipper` | CPU request | `100m` | `20m` | Set to `20m` | | `log-shipper` | CPU limit | `200m` | not set | Leave as is (`200m`) | | `log-shipper` | Memory request | `64Mi` | `32Mi` | Set to `32Mi` | | `log-shipper` | Memory limit | `128Mi` | `64Mi` | Set to `64Mi` | Applied to the manifest: ``` spec: template: spec: containers: - name: checkout-api resources: requests: cpu: 250m # was 500m memory: 384Mi # was 512Mi limits: memory: 512Mi # was 1Gi - name: log-shipper resources: requests: cpu: 20m # was 100m memory: 32Mi # was 64Mi limits: cpu: 200m # unchanged: no recommendation memory: 64Mi # was 128Mi ``` ### Sample Implementation (Python) This sample covers Steps 1 to 4. It requires Python 3.11 or later, uses [`requests`](https://pypi.org/project/requests/), and works on a manifest already loaded into a dictionary, for example with [PyYAML](https://pypi.org/project/PyYAML/). ``` from datetime import datetime, timezone from urllib.parse import quote import requests NOT_SET = "" RESOURCE_FIELDS = { "cpu-request": ("requests", "cpu"), "cpu-limit": ("limits", "cpu"), "memory-request": ("requests", "memory"), "memory-limit": ("limits", "memory"), } def get_recommendations(base_url, auth_header, cluster, namespace, name, kind=None): """Return the right-size recommendations for one workload, across all pages.""" query = ( f"classification=RightSizeWorkload" f" AND metadata.clusterName={cluster}" f" AND metadata.namespaceName={namespace}" f" AND metadata.objectName={name}" ) if kind: query += f" AND metadata.objectType={kind}" url = f"{base_url}/theory/api/v1/k8srecommendations/recommendations/search/query/{quote(query, safe='')}" index, count, results = 0, 100, [] while True: response = requests.get(url, params={"index": index, "count": count}, headers={"Authorization": auth_header}, timeout=30) response.raise_for_status() page = response.json() results.extend(page) if len(page) < count: return results index += count def is_postponed(recommendation): until = recommendation.get("postponedUntil") if recommendation.get("state") != "Postponed" or not until: return False return datetime.fromisoformat(until.replace("Z", "+00:00")) > datetime.now(timezone.utc) def choose(recommendations): """Return the newest recommendation that isn't dismissed or postponed, or None.""" candidates = [r for r in recommendations if r.get("state") != "Dismissed" and not is_postponed(r)] return max(candidates, key=lambda r: r.get("updated", ""), default=None) def normalize(value): value = value.strip() return None if value == "" or value.lower() == NOT_SET else value def split_transition(value): """Split 'deployed->recommended'; a value without '->' is the recommended value.""" if "->" in value: deployed, recommended = value.split("->", 1) return normalize(deployed), normalize(recommended) return None, normalize(value) def decode(recommendation): """Decode the container entries into dictionaries keyed by field name.""" properties = recommendation.get("properties") or {} try: count = int(properties.get("right-size-container-count", "")) except ValueError: return [] containers = [] for n in range(count): entry = properties.get(f"right-size-container-{n}") if not entry: continue fields = {} for part in entry.split(";"): if "=" in part: key, value = part.split("=", 1) fields[key.strip()] = value.strip() container = {"name": fields.get("name")} for key in RESOURCE_FIELDS: if key in fields: container[key] = split_transition(fields[key]) containers.append(container) return containers def apply(manifest, containers): """Write recommended values into the pod template; fields without a recommendation are left as they are.""" targets = {c["name"]: c for c in manifest["spec"]["template"]["spec"].get("containers", [])} for container in containers: target = targets.get(container["name"]) if target is None: continue resources = target.setdefault("resources", {}) for key, (section, resource) in RESOURCE_FIELDS.items(): _, recommended = container.get(key, (None, None)) if recommended is not None: resources.setdefault(section, {})[resource] = recommended ``` #### Example ``` recommendations = get_recommendations("https://", AUTH_HEADER, cluster="prod-east", namespace="checkout", name="checkout-api", kind="Deployment") chosen = choose(recommendations) if chosen is not None: apply(manifest, decode(chosen)) ``` **Notes:** - `AUTH_HEADER` is the value of the `Authorization` header for your RealTheory API credentials. See [Authentication](/v1/docs/authentication) for more details. - The sample leaves out the request-versus-limit check and drift detection described in [Step 4](/docs/how-to-use-right-size-recommendations-within-ci-cd-pipeline#step-4-apply-the-values-to-your-manifest), because both require Kubernetes quantity parsing.