<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Calin Florescu</title>
    <description>The latest articles on DEV Community by Calin Florescu (@calinflorescu).</description>
    <link>https://dev.to/calinflorescu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1474397%2Fbe30c14e-8309-4744-9754-02881f842829.png</url>
      <title>DEV Community: Calin Florescu</title>
      <link>https://dev.to/calinflorescu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/calinflorescu"/>
    <language>en</language>
    <item>
      <title>Debugging an Invisible Scaling Limit on EKS</title>
      <dc:creator>Calin Florescu</dc:creator>
      <pubDate>Fri, 20 Mar 2026 11:27:51 +0000</pubDate>
      <link>https://dev.to/calinflorescu/debugging-an-invisible-scaling-limit-on-eks-1p57</link>
      <guid>https://dev.to/calinflorescu/debugging-an-invisible-scaling-limit-on-eks-1p57</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you've ever tried scaling a deployment past 1000 pods on EKS and watched everything just... stop, with no errors, no warnings, and pods that look healthy but never actually receive traffic — this one's for you.&lt;/p&gt;

&lt;p&gt;I ran into this exact situation on a client's EKS cluster. The HPA was configured to scale well beyond 1000 replicas, and it did spin up the pods. They started fine, containers were healthy, but something was off: the readiness probes weren't even being evaluated. The pods were stuck in a limbo where they existed but didn't really &lt;em&gt;exist&lt;/em&gt; as far as the load balancer was concerned.&lt;/p&gt;

&lt;p&gt;The cluster was running Kubernetes 1.33 with the &lt;a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/" rel="noopener noreferrer"&gt;AWS Load Balancer Controller&lt;/a&gt; v2.12, using IP-mode target registration behind an Application Load Balancer. That last detail turned out to matter a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;The fact that exactly 1000 instances were spun up and registered normally — and none above — made me think about some kind of quota or limit being reached on the EKS or AWS level. A quick check confirmed two important AWS quotas: &lt;code&gt;Targets per Target Group per Region&lt;/code&gt; and &lt;code&gt;Targets per Application Load Balancer&lt;/code&gt;, both with a default value of 1000. Raising these was a necessary first step.&lt;/p&gt;

&lt;p&gt;But even after raising the quotas, the pods were not marked as ready, and they were not showing up as new targets. From a container perspective, everything looked fine — processes were starting correctly, no crazy delays with the readiness probes.&lt;/p&gt;

&lt;p&gt;The nodes were fine too. Kubelets weren't starved of resources, there weren't too many pods scheduled per node, and IPs were correctly assigned. Nothing pointed to anything being off in that direction.&lt;/p&gt;

&lt;p&gt;The next thing I dug into was the internal networking of Kubernetes and how the pods get registered as targets for the target groups. And that's when I found the culprit: the AWS Load Balancer Controller and the way it uses the Endpoint API to manage target registration.&lt;/p&gt;

&lt;p&gt;The controller monitors several Kubernetes resources — Services, Ingresses, Pods, Nodes, and critically, &lt;strong&gt;Endpoints/EndpointSlices&lt;/strong&gt; — to determine which backends should be registered as targets in AWS Elastic Load Balancing target groups.&lt;/p&gt;

&lt;p&gt;This behaviour depends on the target type configured for the controller. In my case, it was set to IP, which heavily relies on Endpoints/EndpointSlices.&lt;/p&gt;

&lt;p&gt;In IP mode, the controller registers &lt;strong&gt;individual pod IPs&lt;/strong&gt; directly into the target group:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The controller's reconciliation loop watches for changes to the &lt;strong&gt;Endpoints&lt;/strong&gt; objects for the relevant Service.&lt;/li&gt;
&lt;li&gt;When an EndpointSlice is created or updated, the controller extracts the list of ready pod IP addresses and their associated ports.&lt;/li&gt;
&lt;li&gt;It compares this set against the currently registered targets in the AWS target group.&lt;/li&gt;
&lt;li&gt;It calls &lt;code&gt;RegisterTargets&lt;/code&gt; for any new pod IPs and &lt;code&gt;DeregisterTargets&lt;/code&gt; for any stale ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After understanding this, the problem was obvious: &lt;strong&gt;my controller was configured to use the Endpoint API for IP registration, and according to the &lt;a href="https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/" rel="noopener noreferrer"&gt;Kubernetes docs&lt;/a&gt;, each Endpoint object created for a service is capped at 1000 entries.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Endpoint object for my Service was truncated, so no changes were detected for the newly added pods, and no reconciliation was triggered on the controller side to register the new targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  But wait — why did the readiness probes look weird?
&lt;/h3&gt;

&lt;p&gt;There was still a nagging question: why did the pods &lt;em&gt;look&lt;/em&gt; ready at a glance, even though the readiness probe seemingly never ran?&lt;/p&gt;

&lt;p&gt;This is where the pod readiness gate comes in. The AWS LB Controller uses a custom readiness condition (&lt;code&gt;target-health.elbv2.k8s.aws/targetgroupbinding&lt;/code&gt;) that works like this: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A pod starts and gets an IP, but that custom condition starts as &lt;code&gt;False&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;The controller registers the pod IP in the target group, polls the AWS health check, and only patches the condition to &lt;code&gt;True&lt;/code&gt; once AWS reports the target as healthy. Until that happens, the pod isn't truly "Ready" from the EndpointSlice's perspective, meaning kube-proxy won't route traffic to it either.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filh5pqny4clkeyheg28h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filh5pqny4clkeyheg28h.png" alt=" " width="800" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's actually a nice mechanism — it prevents dropped connections during rollouts by ensuring the load balancer marks a target as healthy before Kubernetes starts sending traffic to it.&lt;/p&gt;

&lt;p&gt;But here's the catch: if the pod never gets registered in the target group in the first place (because the Endpoint object was truncated at 1000), the controller never even starts that health check dance. The readiness gate stays &lt;code&gt;False&lt;/code&gt; forever, silently. No error, no event, nothing in the logs saying "hey, I skipped this pod." It just doesn't happen.&lt;/p&gt;

&lt;p&gt;That's what made this so tricky to debug. The symptom looked like a readiness probe issue, but the actual cause was three layers deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;After understanding how the AWS LB Controller worked and how it was configured, the solution was straightforward: configure it to use the &lt;strong&gt;EndpointSlices API&lt;/strong&gt; instead of the limited Endpoint one. EndpointSlices don't have the same 1000-entry cap per object — they split endpoints across multiple slices, so the controller can see all of your pod IPs regardless of scale.&lt;/p&gt;

&lt;p&gt;Combined with the AWS quota increases mentioned earlier, this got the deployment scaling well beyond the 1000 pod barrier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I wrote this up for two reasons. The obvious one: if you're hitting this exact wall, I hope this saves you the hours I spent staring at perfectly healthy pods that refused to serve traffic.&lt;/p&gt;

&lt;p&gt;The less obvious one is a reminder — mostly to myself — about what happens when we work on top of deep abstraction layers day after day. The Endpoint API silently truncating at 1000 entries, with no warning and no error, buried under controllers, CRDs, and cloud provider integrations — that's the kind of thing you can only debug if you understand what's actually happening beneath the tools you're using. Abstractions are great, they're how we make progress, but when they break, they break quietly. And the only thing that helps at that point is knowing what's underneath.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>aws</category>
      <category>networking</category>
    </item>
    <item>
      <title>Streamlining Microservices Management: A Unified Helm Chart Approach</title>
      <dc:creator>Calin Florescu</dc:creator>
      <pubDate>Tue, 07 May 2024 05:55:28 +0000</pubDate>
      <link>https://dev.to/calinflorescu/streamlining-microservices-management-a-unified-helm-chart-approach-59g7</link>
      <guid>https://dev.to/calinflorescu/streamlining-microservices-management-a-unified-helm-chart-approach-59g7</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello, I want to discuss with you today a problem I recently encountered with the microservices architecture and the solution I found. I hope my experience with this matter will help someone in the future or save some time.&lt;/p&gt;

&lt;p&gt;This article is relevant if you use a container orchestration tool like Kubernetes or Openshift and manage the releases using Helm. However, it can also be a starting point for an idea in multiple areas.&lt;/p&gt;

&lt;p&gt;These concepts are familiar, and I have not invented them; I combined them into a solution that works for the current situation of the project I am working on.&lt;/p&gt;

&lt;p&gt;With this in mind, let’s start!&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Microservice architecture is great for scalability, development speed, and making your solution technology agnostic by allowing each team to develop their service in the preferred technology. It also leverages the service's ownership to the development team that created it.&lt;/p&gt;

&lt;p&gt;Even if everything sounds great, a problem arises: engineers are managing the Helm Charts that are the bases for the releases that will be installed in the clusters.&lt;/p&gt;

&lt;p&gt;Usually, the main focus of the engineers is to make the most efficient code possible, so the focus won’t be on having the best Helm templates or configurations.&lt;/p&gt;

&lt;p&gt;It’s also usually impossible in a large ecosystem to have a DevOps engineer dedicated to each team managing a microservice that ensures the best possible configuration and best practices.&lt;/p&gt;

&lt;p&gt;This combination of factors is generating a situation where even if 80% of all services are using the same templates (e.g. Deployments, Services, etc.), all of them are written differently, the best practices are not respected, and there might be code duplication or even unused code because of the copy-paste pattern that is happening when a new service is created (this can be avoided using a template repository). Also, if you need to change something on all microservices, you must work in multiple repositories.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn9qmy61h2pafl7willm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn9qmy61h2pafl7willm.png" alt="Microservices Represented as Multiple Languages" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a DevOps, working in this setup is like speaking a new language with each microservice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;A centralised helm chart with template definitions that can be added as a dependency in all microservice charts sounds like a great idea. This way, the engineers are left only to configure the templates with the necessary values based on the service specification and decide what Kubernetes resources they need.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuonp92h0to1rvqi4gks4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuonp92h0to1rvqi4gks4.png" alt="Microservices Represented as a Single Language" width="800" height="665"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, when there is a need to update the configuration for a specific resource, e.g. adding affinities, tolerations, etc., a DevOps engineer will need to do the change in a single place and using the versioning feature will be able to propagate the change to all Releases.&lt;/p&gt;

&lt;p&gt;To integrate the central charts into the microservice one, we can use the dependency functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies:
- name: unified-templates
  version: 1.0.0
  repository: &amp;lt;repository_where_chart_is_stored&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;I created a new repository and initialised a Helm chart using the helm create command to implement this.&lt;/p&gt;

&lt;p&gt;Regarding the way the templates are imported, I decided to create functions. It is easier to track what objects are in use on a microservice, and you can easily template the object with the local values scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Defining a template
{{ define "deployment" }}
api: v1
kind: Deployment
metadata:
  name: {{ include "unified-helm-template.fullname" . }}
spec:
  {{- with .Values.deployment }}
  replicas: {{ .replicas }}
  {{- end }}
{{ end }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Importing a template
{{ include "deployment" . }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Configuring the chart
fullnameOverride: &amp;lt;service_release_name&amp;gt;

## Configuring a template
deployment:
 replicas: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the helpers functions to work correctly, since the templating will be done in the service chart, we need to define the fullnameOverride variable to match the release name. Like this, we can use in the unified charts the predefined helpers, which will use the override value to generate proper naming and labels.&lt;/p&gt;

&lt;p&gt;To configure the template in the child chart, an engineer needs to define an object matching the name of the functions under which it will configure the required values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overriding
&lt;/h2&gt;

&lt;p&gt;Sometimes, you must implement custom functionality, add some logic, or extend the template's functionality quickly. For this situation, I don’t want to restrict the user with the initial implementation, but it would be cool to allow them to override it.&lt;/p&gt;

&lt;p&gt;To achieve this, I added a change to how the template is defined. Now, you can merge a second function with the initial one, obtaining the override functionality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Template definition with override logic
{{- define "deployment" -}}
    {{- $override := include "deploymentOverride" . | fromYaml -}}
    {{- $base := include "deploymentInstance" . | fromYaml -}}
    {{- if $override -}}
        {{- $merged := mustMergeOverwrite (dict) $base $override -}}
        {{- toYaml $merged -}}
    {{- else -}}
        {{- toYaml $base -}}
    {{- end -}}
{{- end -}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Importing the template
{{ include "deployment" .}}

## Override for custom use case
{{ define "deploymentOverride" }}
spec:
 replicas: {{ ternary true false (eq .Values.customVar "true") }}
{{ end }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Documentation
&lt;/h2&gt;

&lt;p&gt;Good documentation for the templates' configurations is essential for centralising them. To manage this, I used a tool to generate markdown documentation from Helm value files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# -- Object that configures Deployment instance
deployment:
  # -- Define the minimum number of seconds for which the pod should be running without crushing before being considered healthy
  minReadySeconds:
  # -- Decide if the pods should not be restarted if you have secrets used as env vars and they are updated
  skipSyncSecrets: true
  # -- Decide if the pods should be restarted if you have configs used as env vars and they are updated
  skipSyncConfigs: true
  # -- Update strategy
  strategy:
  # -- Number of replicas that will be created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To automate the process of creating and updating the documentation, I added the execution of this tool as a pre-commit hook so that every time someone contributes to the repo, the documentation will be automatically updated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repos:
  - repo: &amp;lt;https://github.com/norwoodj/helm-docs&amp;gt;
    rev: v1.2.0
    hooks:
      - id: helm-docs
        args:
          # Make the tool search for charts only under the `unified-templates` directory
          - --chart-search-root=unified-templates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Unit Testing
&lt;/h2&gt;

&lt;p&gt;I needed a way to test the templates I had written before exposing them to the engineers, so I used the &lt;a href="https://github.com/quintush/helm-unittest"&gt;helm-unittest&lt;/a&gt; plugin to write some unit tests. I know it’s not the perfect way to test functionality, but we are assured that the templating works as desired.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;suite: Deployment tests
templates:
  - deployment/deployment.yaml
tests:
  - it: renders a valid deployment instance resource
    values:
    - ./values/deployment.yaml
    asserts:
    - isKind:
        of: Deployment
    - matchSnapshot: {}
  - it: configures the deployment name correctly
    values:
    - ./values/deployment.yaml
    asserts:
    - equal:
        path: metadata.name
        value: service-template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;There is a fine line between a guideline and a rule, between adding some generic best practices and forcing someone to adapt to a particular style. A good engineer has to develop solutions that consider all of these things.&lt;/p&gt;

&lt;p&gt;The solution described above combines all of them, providing a guideline and some strict rules but also allowing for adaptation when necessary.&lt;/p&gt;

&lt;p&gt;What is good for me might be bad for you, and as a DevOps, you have to consider everyone’s well-being, from client to engineer. It’s hard at times, but there lies the beauty.&lt;/p&gt;

&lt;p&gt;You can find a template of this solution &lt;a href="https://github.com/CalinFlorescu/unified-helm-chart"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>helm</category>
      <category>systemdesign</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
