<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fmarslan.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fmarslan.com/" rel="alternate" type="text/html" /><updated>2026-09-07T07:59:58+00:00</updated><id>https://fmarslan.com/feed.xml</id><title type="html">F.M. Arslan</title><subtitle>Yazılım mimarisi, dağıtık sistemler, bulut platformları ve mühendislik üzerine teknik notlar.
</subtitle><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><entry xml:lang="en-US"><title type="html">From Code to Customer: Packaging a Product with Kubernetes</title><link href="https://fmarslan.com/en/2026/09/07/from-code-to-customer-packaging-a-product-with-kubernetes.html" rel="alternate" type="text/html" title="From Code to Customer: Packaging a Product with Kubernetes" /><published>2026-09-07T00:00:00+00:00</published><updated>2026-09-07T00:00:00+00:00</updated><id>https://fmarslan.com/en/2026/09/07/from-code-to-customer-packaging-a-product-with-kubernetes</id><content type="html" xml:base="https://fmarslan.com/en/2026/09/07/from-code-to-customer-packaging-a-product-with-kubernetes.html"><![CDATA[<p>When an application must be installed in different customers’ own environments, I want to combine a cohesive development experience and a single delivery package with the capabilities of distributed execution. I want to understand the product as a whole, follow the relationships between its features, and start working without rebuilding its surroundings. At runtime, I also want to manage each part according to its needs.</p>

<p>These expectations came together in a product I worked on recently. The application ran as separate workloads on Kubernetes, while customers received its entire ecosystem through one installation package.</p>

<p>I think of this as <strong>a product that feels monolithic to develop and install, but runs as a distributed system</strong>. With clear component boundaries, we can add useful runtime and operational capabilities to that cohesive experience. There is also a cost to carrying this responsibility.</p>

<h2 id="why-did-i-need-this-approach">Why did I need this approach?</h2>

<p>The delivery model is on-premises. Unlike a centrally operated SaaS service where customers receive accounts, this product is installed in each customer’s own infrastructure. The application arrives with the environment it needs to run.</p>

<p>That environment includes a database, identity management, messaging, and monitoring. Each component supports the application. Selecting, connecting, and configuring these tools manually for every customer could turn delivery into a recurring integration project.</p>

<p>I wanted installation knowledge to become part of the product. The system prepared for one customer needed to be installable for another through the same recipe, using that customer’s settings. From the customer’s perspective, this should be one product installation.</p>

<p>The runtime requirements still made separate components useful. A user-facing service and a background worker may have different load profiles. Established open-source tools can handle identity or data storage. Delivering these parts together does not require them to run and grow in the same way.</p>

<h2 id="one-development-experience-one-delivery-package">One development experience, one delivery package</h2>

<p>What I appreciate about developing a monolithic application is the ability to keep the whole product in mind. I open the project, see how its parts relate, and evaluate a feature in context. A distributed application can preserve that experience.</p>

<p>Here, “monolithic feel” describes development and installation. <strong>Code organization, the delivery package, and runtime topology are separate design decisions.</strong> A product developed together and delivered in one package can run as separate processes, and where needed on different nodes, in Kubernetes.</p>

<p>The customer’s entry point looks like this, using an illustrative command name:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>product <span class="nb">install</span>
</code></pre></div></div>

<p>Once the package is available in the target environment, supported server prerequisites are met, and connection details are supplied, this command starts the installation flow. K3s prepares Kubernetes; Helm installs the platform services and application. Initial database definitions, application access settings, and component readiness checks belong to the same flow.</p>

<p>The customer does not have to run a separate installation procedure for every service. Environment-specific settings remain separate, while the package carries the knowledge of how the components fit together.</p>

<p>In this project, application image building also happens inside the target environment. BuildKit produces the image on the platform where it will run. Components requiring compilation use toolchains and base images appropriate for the supported processor architecture. The image is pushed to the local Zot registry and runs on the same platform. Runtime settings are applied during installation, while secrets are provided without baking them into the image.</p>

<p>For the customer, this path from source code to a running application becomes part of the product’s installation behavior.</p>

<h2 id="where-should-cohesion-end-and-boundaries-begin">Where should cohesion end and boundaries begin?</h2>

<p>This is the decision that determines whether the approach works. If a unified product experience makes every component dependent on the internal details of the others, we lose much of the value of distributed execution.</p>

<p><strong>The package defines the product boundary. Component responsibilities need their own boundaries.</strong></p>

<p>It should be clear which data a component owns, which API or message contracts it exposes, and who is affected when it changes. Requiring components to understand one another’s internal tables, implementation details, or private configuration expands the impact of change.</p>

<p>The same distinction applies to updates. We can ship a product release containing components tested together. That does not mean every application change must reinstall the database, identity service, and entire platform. Compatible changes should be applicable to the relevant component. Changes to data schemas or service contracts need their own coordination.</p>

<p>A shared development environment is comfortable when responsibilities are clear. Otherwise, every change inside the package can become a whole-product concern.</p>

<p>The question of where we manage complexity, which I explored in my writing about cloud-native and monolithic systems, matters here too. We can offer cohesion to developers and customers while keeping component relationships explicit in the platform design.</p>

<h2 id="what-do-we-gain">What do we gain?</h2>

<p>The first benefit is repeatable delivery. The order of installation and the required initialization settings travel with the product. Checking existing versions, configuration, and health is part of that experience. Installation and upgrades become defined product operations.</p>

<p>The second benefit is runtime visibility and targeted intervention. When logs, metrics, and health information are associated with components, we can focus investigation on the relevant part. With suitable boundaries, we can restart, update, or change the capacity of one component while limiting the effect on the rest of the product.</p>

<p>This separation also supports performance management. We can add consumers to a busy background workload or scale request-handling services according to their own load. Appropriate metrics and autoscaling policies can automate these changes. Instead of growing the whole product at the same rate, we can expand the part that needs capacity.</p>

<p>This is a capability we can build on the architecture. Application concurrency, data-layer capacity, and available cluster resources determine the result. In an on-premises environment, autoscaling still operates within the customer’s hardware capacity and configured limits.</p>

<p>Another benefit is broader access to the open-source ecosystem. We can integrate established tools for identity, messaging, or monitoring into the product. Components do not all need to use the same language. As an illustrative example, an API could use .NET and a background worker Python, while the customer installs both as parts of one product.</p>

<p>The Lego analogy is useful here. Service contracts, data ownership, and version compatibility are the connection points. When those are defined well, we can use the accumulated work of existing tools and focus our own effort on the product’s distinctive functionality.</p>

<h2 id="a-useful-consequence-developing-in-the-same-environment">A useful consequence: Developing in the same environment</h2>

<p>Once we have an installable ecosystem for customers, we can install the same package for development. For me, easier local development is a natural benefit of this delivery model.</p>

<p>Tools such as Docker Compose and <a href="https://aspire.dev/get-started/faq/">Aspire</a>, familiar from the .NET ecosystem, can also bring services into a shared development experience. The property I want to preserve here is that the actual customer platform runs in development through its own installation recipe.</p>

<p>We use the same Kubernetes distribution, database, messaging, and identity services. The application communicates with the real components; I do not replace them with substitutes for development. A background task connects to the product’s messaging service, and a sign-in flow uses its identity service.</p>

<p>In this sense, I develop in a separate instance of the production environment. It is isolated from the customer’s live system and data and can run in a local virtual machine or on development servers. Hardware capacity, node count, and settings may differ, while the components and operating recipe remain consistent. Performance and high-availability assessments still require tests appropriate to the target capacity.</p>

<p><strong>Developing inside the ecosystem the product will use lets us address some delivery uncertainties earlier.</strong></p>

<h2 id="where-do-we-pay-for-this-convenience">Where do we pay for this convenience?</h2>

<p>For the customer to install with one command, we must design everything behind that command. Component compatibility, startup order, access rules, and data relationships become part of product maintenance. Open-source updates and security fixes must be evaluated within that whole.</p>

<p>Installation is the beginning of the lifecycle. Backup and recovery, schema changes, application updates, and support need their own design. Responsibilities must also be clear between the product team and the customer. One installation command alone does not guarantee easy operations.</p>

<p>On-premises customers may remain on different versions. Network access, certificates, storage, and hardware capacity can vary. Defining supported environments and validating upgrade paths takes ongoing effort.</p>

<p>Working on offline installation made this delivery responsibility especially clear. Images and build dependencies must be prepared in advance, package integrity checked, and target prerequisites defined. Supporting restricted-network environments adds responsibilities to release preparation.</p>

<p>There is also a resource cost. Running a full platform alongside a small application requires memory, storage, and operational knowledge. If images are built in the target environment, build capacity and permissions must be included. The simpler development and installation experience is supported by an investment in platform engineering.</p>

<h2 id="when-would-i-choose-it-and-when-would-i-avoid-it">When would I choose it, and when would I avoid it?</h2>

<p>I consider this approach for products that will be installed repeatedly in different customers’ infrastructure and developed over time. Components with different runtime needs, targeted intervention or scaling requirements, and useful open-source dependencies can justify turning the installation ecosystem into part of the product.</p>

<p>The team’s ability to maintain the platform is part of that decision. Writing the installation recipe once is not enough. It must evolve with versions, security updates, and customer environments.</p>

<table>
  <thead>
    <tr>
      <th>A need that strengthens the case</th>
      <th>A situation favoring a simpler solution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Repeated installation in different customer environments</td>
      <td>Limited installation needs with few dependencies</td>
    </tr>
    <tr>
      <td>Component-specific capacity and intervention</td>
      <td>Adequate operations with one process or a few containers</td>
    </tr>
    <tr>
      <td>A team and product lifetime that support platform maintenance</td>
      <td>Scope and team capacity that cannot justify the maintenance cost</td>
    </tr>
    <tr>
      <td>Clearly defined, supportable target environments</td>
      <td>Customer infrastructure unable to meet the product’s resource needs</td>
    </tr>
  </tbody>
</table>

<p>If the customer already has a standardized Kubernetes platform, I reconsider including a cluster in the package. Adapting the application and its dependencies to that platform may be more appropriate. A centrally delivered SaaS product also changes the justification, because customer-by-customer installation is no longer the same requirement.</p>

<p>My deciding factors are how the product will be delivered and how it will operate over the years. When we can preserve one product experience while making useful use of component independence, this design has a strong purpose.</p>

<p>One area where I contribute in development and consulting is considering these decisions together: developers must be able to understand the product, customers must be able to install it, and operations teams must be able to manage it. Bringing those needs into one design makes the application a product we can keep delivering and supporting.</p>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="kubernetes" /><category term="platform-engineering" /><category term="developer-experience" /><category term="software-architecture" /><category term="distributed-systems" /><summary type="html"><![CDATA[A cohesive development experience and one on-premises installation package for a distributed application: boundaries, benefits, costs, and when to choose it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/packaged-distributed-application.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/packaged-distributed-application.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="tr-TR"><title type="html">Koddan Müşteri Ortamına: Kubernetes ile Bir Ürünü Paketlemek</title><link href="https://fmarslan.com/tr/2026/09/07/koddan-musteri-ortamina-kubernetes-ile-bir-urunu-paketlemek.html" rel="alternate" type="text/html" title="Koddan Müşteri Ortamına: Kubernetes ile Bir Ürünü Paketlemek" /><published>2026-09-07T00:00:00+00:00</published><updated>2026-09-07T00:00:00+00:00</updated><id>https://fmarslan.com/tr/2026/09/07/koddan-musteri-ortamina-kubernetes-ile-bir-urunu-paketlemek</id><content type="html" xml:base="https://fmarslan.com/tr/2026/09/07/koddan-musteri-ortamina-kubernetes-ile-bir-urunu-paketlemek.html"><![CDATA[<p>Farklı müşterilerin kendi ortamlarına kurulacak bir uygulamada, geliştirme bütünlüğünü ve tek paketle teslim kolaylığını dağıtık çalışma yetenekleriyle birleştirmek istiyorum. Özelliklerin birbiriyle ilişkisini takip etmek, ortamı açıp çalışmaya başlamak ve geliştirdiğim şeyi tek bir ürün olarak teslim etmek benim için değerli. Çalışma anında ise her parçanın kendi ihtiyacına göre yönetilebilmesini istiyorum.</p>

<p>Yakın zamanda üzerinde çalıştığım bir üründe bu beklentiler aynı tasarımda buluştu. Uygulama Kubernetes üzerinde ayrı iş yükleriyle çalışıyor; müşteriye kendi ekosistemiyle birlikte tek bir kurulum paketi olarak gidiyordu.</p>

<p>Bu deneyimi anlatırken aklıma gelen ifade, <strong>monolitik tadında geliştirilen ve kurulan, dağıtık çalışan bir ürün</strong> oldu. Bu bütünlüğü korurken bileşenlerin sınırlarını da doğru çizebilirsek, geliştirme rahatlığının üzerine önemli çalışma ve işletim yetenekleri ekleyebiliyoruz. Bunun için üstlenmemiz gereken bir maliyet de var.</p>

<h2 id="neden-böyle-bir-yapıya-ihtiyaç-duydum">Neden böyle bir yapıya ihtiyaç duydum?</h2>

<p>Bu çalışmanın teslim modeli on-premise. Merkezi olarak işlettiğimiz ve müşterilere hesap açtığımız bir SaaS hizmetinden farklı olarak, her müşterinin kendi altyapısına kurulacak bir ürün geliştiriyoruz. Uygulama, ihtiyaç duyduğu çalışma ortamıyla birlikte müşteriye gidiyor.</p>

<p>Bu ortamda veritabanı, kimlik yönetimi, mesajlaşma ve izleme gibi bileşenler var. Bunların her biri uygulamanın çalışmasında bir rol üstleniyor. Her müşteri kurulumunda bu araçları yeniden seçmek, elle bağlamak ve başlangıç ayarlarını ayrı ayrı yapmak, teslimatı tekrar eden bir entegrasyon işine dönüştürebilir.</p>

<p>Ben kurulum bilgisini ürünün içine almak istedim. Bir müşteride hazırladığımız sistemin, başka bir müşteride de o ortama ait ayarlarla aynı tarif üzerinden kurulabilmesi gerekiyordu. Müşterinin gördüğü deneyim tek bir ürün kurulumu olmalıydı.</p>

<p>Çalışma ihtiyaçları ise ayrı bileşenleri anlamlı kılıyordu. Kullanıcıya cevap veren bölümle arka planda işleyen görevler aynı yük profiline sahip olmayabilir. Kimlik yönetimi veya veri saklama için açık kaynak ekosistemindeki araçlardan yararlanabiliriz. Bu parçaları birlikte teslim etmek, hepsinin aynı şekilde çalışmasını ve büyümesini gerektirmiyor.</p>

<h2 id="geliştirirken-bir-bütün-teslim-ederken-tek-paket">Geliştirirken bir bütün, teslim ederken tek paket</h2>

<p>Monolitik uygulama geliştirmenin sevdiğim tarafı, ürünün zihnimde bir bütün olarak kalması. Projeyi açıyorum, parçaların birbiriyle ilişkisini görüyorum ve geliştirdiğim özelliği bu bütünün içinde değerlendirebiliyorum. Dağıtık çalışan bir sistemde de bu deneyimi korumak mümkün.</p>

<p>Burada “monolitik tadında” ifadesiyle geliştirme ve kurulum deneyimini tarif ediyorum. <strong>Kodun organizasyonu, teslimat paketi ve çalışma topolojisi farklı tasarım kararları.</strong> Birlikte geliştirdiğimiz ve tek paketle teslim ettiğimiz ürün, Kubernetes üzerinde ayrı süreçlerde ve gerektiğinde farklı node’larda çalışabilir.</p>

<p>Müşterinin gördüğü giriş noktası, temsili bir isimle şöyle:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>urun <span class="nb">install</span>
</code></pre></div></div>

<p>Paket hedef ortama alındığında, desteklenen sunucu ön koşulları ve gerekli bağlantı bilgileri sağlandığında, bu komut bütün kurulum akışını başlatıyor. K3s ile Kubernetes hazırlanıyor; Helm üzerinden platform servisleri ve uygulama kuruluyor. Veritabanının başlangıç tanımları, uygulamanın erişim ayarları ve bileşenlerin hazır olma kontrolleri de aynı akışta ele alınıyor.</p>

<p>Böylece müşteri her servis için ayrı kurulum adımları yürütmüyor. Ortama ait ayarlar ayrı tutuluyor, bileşenlerin nasıl bir araya geleceğinin bilgisi paketle birlikte taşınıyor.</p>

<p>Bu çalışmada uygulamanın imaj üretimini de hedef ortamın içine aldım. BuildKit, uygulama imajını çalışacağı platformda üretiyor; derleme gerektiren parçalar desteklenen işlemci mimarisine uygun araç zinciri ve taban imajlarla hazırlanıyor. İmaj yerel registry olan Zot’a aktarılıp aynı platformda çalıştırılıyor. Ortama ait çalışma ayarları kurulumda uygulanırken gizli bilgiler imaja gömülmeden sağlanıyor.</p>

<p>Müşteri açısından kaynak koddan çalışan uygulamaya uzanan bu süreç, ürünün kurulum davranışının bir parçası oluyor.</p>

<h2 id="bütünlük-nerede-bitmeli-sınırlar-nerede-başlamalı">Bütünlük nerede bitmeli, sınırlar nerede başlamalı?</h2>

<p>Bu yaklaşımın başarısını belirleyen nokta bence burası. Tek ürün deneyimi oluştururken her parçayı birbirinin iç yapısına bağımlı hale getirirsek, dağıtık çalışmanın getirisini azaltırız.</p>

<p><strong>Paketin sınırı ürünün sınırıdır. Bileşenlerin sorumluluk sınırları ise ayrıca tasarlanmalıdır.</strong></p>

<p>Bir bileşenin hangi veriyi yönettiği, diğerlerine hangi API veya mesaj sözleşmesiyle ulaştığı ve değiştiğinde kimleri etkilediği anlaşılır olmalı. Her bileşenin başka bir bileşenin iç tablolarını, ayrıntılı uygulama davranışını veya özel yapılandırmasını bilmesi, değişikliklerin etki alanını büyütür.</p>

<p>Aynı ayrım güncelleme için de geçerli. Birlikte test edilmiş bileşenleri tek ürün sürümünde paketleyebiliriz. Bu, her uygulama değişikliğinde veritabanını, kimlik servisini ve bütün platformu yeniden kurmamız gerektiği anlamına gelmez. Uyumlu değişikliklerin yalnızca ilgili bileşene uygulanabilmesini tasarlamak gerekir. Veri şeması veya servis sözleşmesi değiştiğinde ise koordinasyon ihtiyacı ayrıca ele alınır.</p>

<p>Ortak geliştirme ortamı, açık sorumluluk sınırlarıyla birlikte rahatlık sağlar. Aksi durumda tek paket içindeki her değişiklik, bütün ürünün yeniden değerlendirilmesini gerektiren bir işe dönüşebilir.</p>

<p><a href="/2026/04/08/monolith-vs-cloud-native-gercek-dunya-karsilastirma.html">Cloud-native ve monolit üzerine yazımda</a> bahsettiğim karmaşıklığın nerede yönetildiği konusu burada da belirleyici. Bütünlüğü geliştiriciye ve müşteriye sunarken, parçalar arasındaki ilişkileri platform tasarımında görünür tutmak gerekiyor.</p>

<h2 id="karşılığında-ne-kazanıyoruz">Karşılığında ne kazanıyoruz?</h2>

<p>İlk kazanım, teslimatın tekrarlanabilir hale gelmesi. Yeni bir müşteri ortamına giderken hangi bileşenin önce kurulacağı ve hangi başlangıç ayarlarının uygulanacağı ürünle birlikte geliyor. Kurulumun mevcut sürüm, yapılandırma ve sağlık durumunu dikkate alması da bu deneyimin parçası. Böylece kurulum ve güncelleme, tanımlı ürün işlemleri olarak ele alınabiliyor.</p>

<p>İkinci kazanım, çalışma anındaki görünürlük ve müdahale esnekliği. Loglar, metrikler ve sağlık bilgileri bileşenlerle ilişkilendirildiğinde, incelemeyi ilgili noktaya yönlendirebiliyoruz. Sınırları doğru kurulmuş bir sistemde belirli bir bileşeni yeniden başlatmak, güncellemek veya kapasitesini değiştirmek mümkün oluyor. Müdahalenin etki alanını daraltarak ürünün diğer işlevlerini koruyabiliyoruz.</p>

<p>Bu ayrım performans yönetiminde de değerli. Yoğunlaşan bir arka plan işine daha fazla tüketici ayırabilir, istek karşılayan servisleri kendi yüklerine göre ölçekleyebiliriz. Uygun metrikler ve autoscaling politikaları tanımlandığında bu kapasite değişiklikleri otomatik yürütülebilir. Bütün ürünü aynı oranda büyütmek yerine, ihtiyaç duyulan parçayı büyütme imkânı elde ederiz.</p>

<p>Bu, mimarinin üzerine kurulabilen bir yetenektir. Uygulamanın paralel çalışmaya uygunluğu, veri katmanının kapasitesi ve kümenin kullanılabilir kaynakları sonucu belirler. On-premise ortamda otomatik ölçekleme de müşterinin mevcut donanım kapasitesi ve tanımlanan sınırlar içinde gerçekleşir.</p>

<p>Bir diğer kazanım, açık kaynak ekosisteminden daha geniş yararlanabilmek. Kimlik yönetimi, mesajlaşma veya izleme için belirli bir işi iyi yapan araçları ortak ürün düzenine dahil edebiliyoruz. Her bileşenin aynı dilde yazılması da gerekmiyor. Temsili olarak bir API .NET ile, arka plan görevi Python ile geliştirilebilir; müşteri ikisini aynı ürünün parçaları olarak kurar.</p>

<p>Lego benzetmesi burada anlamlı. Parçaları bir arada tutan bağlantı noktaları; servis sözleşmeleri, veri sahipliği ve sürüm uyumudur. Bunları doğru tanımladığımızda, mevcut araçların birikiminden yararlanıp kendi emeğimizi ürünün özgün işlevlerine yönlendirebiliriz.</p>

<h2 id="bu-tasarımın-bir-meyvesi-aynı-ortamda-geliştirmek">Bu tasarımın bir meyvesi: Aynı ortamda geliştirmek</h2>

<p>Müşteriye kurulabilir bir ekosistem hazırladığınızda, aynı paketi geliştirme için de kurabiliyorsunuz. Benim için lokal geliştirme kolaylığı, bu teslim modelinin doğal kazanımlarından biri.</p>

<p>Docker Compose veya .NET ekosisteminden tanıdığımız <a href="https://aspire.dev/get-started/faq/">Aspire</a> gibi araçlarla da servisleri ortak bir geliştirme deneyiminde buluşturabiliyoruz. Burada korumak istediğim özellik, müşteriye teslim edilen platformun kendi kurulum tarifiyle geliştirme ortamında da çalışması.</p>

<p>Aynı Kubernetes dağıtımı, aynı veritabanı, aynı mesajlaşma ve kimlik servisleri kullanılıyor. Uygulama gerçek bileşenlerle iletişim kuruyor; geliştirme için onların yerine muadil araçlar koymuyorum. Arka plan görevini yazarken ürünün mesajlaşma servisine, giriş akışını geliştirirken ürünün kimlik servisine bağlanıyorum.</p>

<p>Bu anlamda üretim ortamının ayrı bir örneğinde geliştirme yapıyorum. Müşterinin canlı sisteminden ve verilerinden ayrı olan bu ortam, yerel bir sanal makinede veya geliştirme sunucularında kurulabilir. Donanım kapasitesi, node sayısı ve ortam ayarları değişse de kullanılan bileşenler ve çalışma tarifi korunur. Performans ve yüksek erişilebilirlik değerlendirmeleri için ise hedef kapasiteye uygun testler gerekir.</p>

<p><strong>Ürünü çalışacağı ekosistemin içinde geliştirebilmek, teslimata hazırlanırken taşıdığımız belirsizliklerden bir bölümünü daha erken ele almamızı sağlıyor.</strong></p>

<h2 id="bu-rahatlığın-bedelini-nerede-ödüyoruz">Bu rahatlığın bedelini nerede ödüyoruz?</h2>

<p>Müşterinin tek komutla kurulum yapabilmesi için o komutun arkasındaki işleri bizim tasarlamamız gerekiyor. Bileşenlerin sürüm uyumu, başlangıç sırası, erişim kuralları ve veri ilişkileri ürünün bakım kapsamına giriyor. Açık kaynak araçların güncellemelerini ve güvenlik düzeltmelerini de bu bütün içinde değerlendirmeliyiz.</p>

<p>Kurulum, yaşam döngüsünün başlangıcı. Yedekleme ve geri yükleme, veri şeması geçişleri, uygulama güncellemeleri ve destek süreçleri ayrıca tasarlanmalı. Bunların sorumluluğunun ürün ekibi ile müşteri arasında nasıl paylaşılacağı da açık olmalı. Tek kurulum, tek başına kolay işletim garantisi vermiyor.</p>

<p>On-premise teslimatta farklı müşteriler farklı sürümlerde kalabilir. Ağ erişimi, sertifika düzeni, depolama ve donanım kapasitesi değişebilir. Desteklenen ortamların sınırını tanımlamak ve güncelleme yollarını doğrulamak için sürekli emek gerekiyor.</p>

<p>Çevrimdışı kurulum üzerinde çalışırken de bu teslim sorumluluğu belirginleşti. Gerekli imajları ve build bağımlılıklarını önceden hazırlamak, paket bütünlüğünü kontrol etmek ve hedef platformun ön koşullarını belirlemek gerekiyor. İnternet erişimi kısıtlı ortamlara teslim imkânı kazanırken, dağıtım paketinin hazırlanmasına daha fazla sorumluluk ekliyoruz.</p>

<p>Kaynak maliyeti de var. Küçük bir uygulamanın yanında bütün bir platform çalıştırmak; bellek, disk ve işletim bilgisi gerektiriyor. Hedef ortamda imaj üretmeyi seçiyorsak build işlemlerinin kapasitesini ve yetkilerini de hesaba katmalıyız. Geliştirme ve kurulumdaki sadeliğin karşılığında, platform mühendisliğine yatırım yapıyoruz.</p>

<h2 id="ne-zaman-tercih-ederim-ne-zaman-etmem">Ne zaman tercih ederim, ne zaman etmem?</h2>

<p>Bu yaklaşımı, farklı müşterilerin kendi altyapılarına tekrar tekrar kurulacak ve zaman içinde geliştirilecek ürünlerde değerlendiririm. Birden fazla çalışma ihtiyacı olan bileşenler, kısmi müdahale veya ayrı ölçekleme gereksinimi ve açık kaynak servislerden yararlanma isteği varsa, kurulum ekosistemini ürünleştirmek anlamlı hale gelir.</p>

<p>Ekibin bu platformu sürdürebilmesi de kararın parçası. Kurulum tarifini bir kez hazırlamak yeterli olmaz; sürümler, güvenlik güncellemeleri ve müşteri ortamlarıyla birlikte yaşatabilmek gerekir.</p>

<table>
  <thead>
    <tr>
      <th>Bu yaklaşımı güçlendiren ihtiyaç</th>
      <th>Daha sade bir çözümü öne çıkaran durum</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Farklı müşteri ortamlarına tekrarlanan kurulum</td>
      <td>Az bağımlılıkla karşılanabilen sınırlı kurulum ihtiyacı</td>
    </tr>
    <tr>
      <td>Bileşenlere ayrı kapasite ve müdahale gereksinimi</td>
      <td>Tek süreç veya birkaç container ile yeterli işletim</td>
    </tr>
    <tr>
      <td>Ortak platformu sürdürebilecek ekip ve ürün ömrü</td>
      <td>Platform bakım maliyetini karşılamayan kapsam ve ekip kapasitesi</td>
    </tr>
    <tr>
      <td>Tanımlanabilir, desteklenebilir hedef ortamlar</td>
      <td>Ürünün kaynak ihtiyacını karşılamayan müşteri altyapısı</td>
    </tr>
  </tbody>
</table>

<p>Müşterinin zaten standartlaştırılmış bir Kubernetes platformu varsa, kendi kümemi de pakete dahil etme kararını yeniden değerlendiririm. O durumda uygulama ve bağımlılıklarını mevcut platforma uyarlamak daha uygun olabilir. Merkezi bir SaaS teslim modelinde de müşteri başına kurulum ihtiyacı aynı olmadığı için bu paketleme yatırımının gerekçesi değişir.</p>

<p>Benim için tercih ölçütü, ürünün nasıl teslim edileceği ve yıllar içinde nasıl işletileceği. Tek ürün deneyimini korurken parçaların çalışma özgürlüğünü anlamlı biçimde kullanabiliyorsak, bu tasarım güçlü bir karşılık veriyor.</p>

<p>Geliştirme ve danışmanlık çalışmalarında değer ürettiğim alanlardan biri de bu kararları birlikte ele almak: geliştiricinin ürünü anlayabilmesi, müşterinin kurabilmesi ve operasyon ekibinin yönetebilmesi. Bu üçünü aynı tasarımda buluşturmak, uygulamayı sürdürülebilir biçimde teslim edilebilir bir ürüne dönüştürüyor.</p>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="kubernetes" /><category term="platform-engineering" /><category term="developer-experience" /><category term="yazılım-mimarisi" /><category term="dağıtık-sistemler" /><summary type="html"><![CDATA[Monolitik tadında geliştirme ve tek paketle on-premise teslim: Dağıtık çalışan bir üründe sınırlar, kazanımlar, maliyetler ve tercih ölçütleri.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/packaged-distributed-application.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/packaged-distributed-application.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en-US"><title type="html">How I Use AI: Operations Are Automated, Decisions Stay With Me</title><link href="https://fmarslan.com/en/2026/08/07/how-i-use-ai-operations-without-giving-up-decisions.html" rel="alternate" type="text/html" title="How I Use AI: Operations Are Automated, Decisions Stay With Me" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://fmarslan.com/en/2026/08/07/how-i-use-ai-operations-without-giving-up-decisions</id><content type="html" xml:base="https://fmarslan.com/en/2026/08/07/how-i-use-ai-operations-without-giving-up-decisions.html"><![CDATA[<p>AI now handles much of my development work, Kubernetes and Azure operations, documentation, issue management, social media tasks, and even the maintenance of this blog. I have not delegated the decisions, however. I standardize the work, document its boundaries, and let AI execute it.</p>

<p>I did not reach this point by discovering one clever prompt. The path included local-model experiments, oversized skill files, exhausted token allowances, and automation that could fail in dangerous ways. The central lesson was simple: running a model and operating a reliable AI system are very different problems.</p>

<h2 id="why-local-models-remain-attractive">Why local models remain attractive</h2>

<p>Running a SaaS-backed agent on my own computer initially made me uncomfortable, and the concern has not disappeared. Data privacy, confidential company information, and knowing what is sent to which provider are serious questions with no universal answer.</p>

<p>An entirely local, offline model is therefore appealing. Yet much of the value delivered by a cloud AI product does not come from model weights alone. Search, web clients, document parsers, indexes, code execution, tool integrations, identity, and permission controls all affect the quality of the result.</p>

<p>Starting a local model often gives you an advanced chatbot. To reproduce a capable operator, you must also provide tools, APIs, retrieval, access policies, observability, and an execution environment. Taken far enough, this becomes a private AI platform that you must build and operate yourself.</p>

<p>That can be a valid choice. It resembles running your own mail server at home: possible, sometimes necessary, but accompanied by maintenance, security, hardware, and time costs. Unless you are a large organization, generate heavy usage, or earn revenue from the infrastructure, a managed service can remain cheaper. That was the outcome of my own experiments.</p>

<h2 id="a-model-is-not-an-operator-by-itself">A model is not an operator by itself</h2>

<p>An LLM produces likely output from the context it receives. It can say “I don’t know,” but that does not mean it will reliably identify every missing fact. In operational work, the tendency to fill an unspecified gap with a plausible assumption is a risk.</p>

<p>Consider a well-documented user-management API. If its endpoints, authentication, request bodies, and error responses are explicit, the documentation becomes part of the agent’s operating context. The agent can construct a user-creation request and, when it has network access and credentials, execute it.</p>

<p>Location matters. A cloud agent cannot automatically reach a service on a private local network. An agent running on the relevant machine or network may reach it when properly authorized. What the model knows is only one part of the system; where the agent runs and what it may access are equally important.</p>

<h2 id="from-api-documentation-to-a-skill">From API documentation to a skill</h2>

<p>API documentation explains the technical call. A skill defines the policy for using it:</p>

<ul>
  <li>Which documentation must be read first?</li>
  <li>Which environment is the default?</li>
  <li>Which actions must remain read-only?</li>
  <li>Which changes require human approval?</li>
  <li>How will the outcome be verified and reported?</li>
  <li>Should an error trigger a retry or stop the workflow?</li>
</ul>

<p>A skill for a user-management application can instruct the agent to fetch the current API documentation, prepare the request, assess its impact, and report the result. A general-purpose model then becomes a limited operator for a specific application.</p>

<pre><code class="language-mermaid">flowchart LR
    I["Request"] --&gt; S["Relevant skill"]
    S --&gt; D["Current documentation"]
    D --&gt; P["Plan and command"]
    P --&gt; R["Independent review"]
    R --&gt;|Approved| E["Execution"]
    R --&gt;|Revise| P
    E --&gt; V["Verification and report"]
</code></pre>

<p>The skill should not memorize every command. It should define how to find the authoritative source, which boundaries apply, and when the agent must stop.</p>

<h2 id="a-second-and-third-pair-of-eyes">A second and third pair of eyes</h2>

<p>Agents make mistakes in API calls just as they do in code. A development error may cost time; an incorrect production call can cause data loss or an outage.</p>

<p>I therefore put independent checks into the workflow. One agent interprets the request and prepares the action. A second agent, with separate context, reviews both the requirement and the proposed command. If it rejects the plan, nothing runs; its findings return to the first workflow for revision.</p>

<p>A second pass by the same model is helpful but not fully independent. For higher-risk work, review can be sent to another model or provider. What people describe as “build with Codex, review with Claude, verify with Gemini” becomes more useful when expressed as a repeatable policy rather than a personal habit.</p>

<p>This is still not a proof of correctness. Two models can share the same false assumption. Human approval, least privilege, backups, dry runs, and before-and-after verification remain necessary for irreversible operations.</p>

<h2 id="simulating-the-viewpoints-of-a-team">Simulating the viewpoints of a team</h2>

<p>Engineering titles such as junior, senior, lead, architect, and tester exist because a team needs different questions and responsibilities. I use a similar separation in agent workflows. The value does not come from pretending that each role is a real person; it comes from reviewing the same change through distinct, constrained contexts.</p>

<p>An architect may define boundaries, a developer implement the change, a reviewer examine risk and consistency, and a tester validate acceptance criteria. When this exists only in chat, the history quickly becomes fragile. When the workflow operates through an issue-management system, each role advances the issue, records evidence, and hands it to the next stage. The virtual team then works on a persistent record rather than an ephemeral conversation.</p>

<h2 id="what-a-local-ai-platform-actually-contains">What a local AI platform actually contains</h2>

<p>Open-source and self-hosted components are available for this kind of system, but the following layers show the distance between downloading a model and running an operational platform:</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Example</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Local model runtime</td>
      <td><a href="https://github.com/ollama/ollama/blob/main/docs/api.md">Ollama</a></td>
      <td>Manage local models and expose them through REST and tool calling</td>
    </tr>
    <tr>
      <td>High-throughput serving</td>
      <td><a href="https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/">vLLM</a></td>
      <td>Serve GPU-backed models through an OpenAI-compatible HTTP API</td>
    </tr>
    <tr>
      <td>Model gateway</td>
      <td><a href="https://docs.litellm.ai/">LiteLLM</a></td>
      <td>Put multiple providers behind one interface with authorization, limits, and cost tracking</td>
    </tr>
    <tr>
      <td>Operator interface</td>
      <td><a href="https://docs.openwebui.com/">Open WebUI</a></td>
      <td>Combine local and cloud models with tools, knowledge, and a user interface</td>
    </tr>
    <tr>
      <td>Search</td>
      <td><a href="https://docs.searxng.org/">SearXNG</a></td>
      <td>Provide a self-hosted metasearch layer over multiple sources</td>
    </tr>
    <tr>
      <td>Agent orchestration</td>
      <td><a href="https://langchain-ai.github.io/langgraph/index.html">LangGraph</a></td>
      <td>Build long-running, stateful workflows with human approval points</td>
    </tr>
    <tr>
      <td>Tool connectivity</td>
      <td><a href="https://modelcontextprotocol.io/docs/learn/architecture">Model Context Protocol</a></td>
      <td>Expose tools, resources, and prompts to agents through a client-server protocol</td>
    </tr>
    <tr>
      <td>Code search and access</td>
      <td><a href="https://beaconbay.github.io/ck/">CK</a></td>
      <td>Search code locally with hybrid and semantic retrieval, then expose results to agents through MCP</td>
    </tr>
  </tbody>
</table>

<p>This is not a deployment recipe. Identity, secret storage, sandboxing, logs, evaluations, backups, and network policy still have to be designed. A GPU cluster makes experimentation possible, but renting compute and operating a reliable, isolated model service are different businesses.</p>

<p>Installing the tools is the most visible and often the easiest part. The real work is analyzing an organization’s processes, identifying decision and authority boundaries, delivering the right context at the right time, designing safety gates, and integrating the pieces with existing platforms. The same tool list can produce very different operating models in two organizations.</p>

<h2 id="how-i-applied-the-approach">How I applied the approach</h2>

<p>I gradually moved the Kubernetes clusters, Azure services, development work, technical documentation, social media processes, and blog operations that I manage into this model, provided they are authorized for AI use. Today I can send an instruction from my phone and have it executed through an authorized working environment even when I am away from my desk. It sounds a little like Jarvis from Iron Man, but the mechanism is not magic. It is standards, access, and documentation.</p>

<p>I follow roughly the same sequence for each kind of work:</p>

<ol>
  <li>Turn the task into a repeatable standard.</li>
  <li>Document decision points and prohibited actions.</li>
  <li>Prepare the authoritative documentation.</li>
  <li>Make the skill retrieve only the relevant material.</li>
  <li>Add review and approval gates according to risk.</li>
  <li>Verify the outcome independently after execution.</li>
</ol>

<p>Writing the documentation can take as long as writing code. The difference is that I now prepare the definition once for many recurring tasks. The agent can use that source for later development or operational work without requiring the process to be explained again.</p>

<p>Ambiguity must be treated as a design flaw. I do not want a personal photograph inserted into an article without permission, a private message sent to the wrong person, or a month-old backup restored over production. These constraints cannot be captured by saying “be careful.” They require target verification, explicit prohibitions, approval conditions, and a rollback plan.</p>

<h2 id="the-token-wall">The token wall</h2>

<p>My first system worked, but it was not economical. The weekly allowance of the plan I used, which cost roughly €100, stopped being enough. Email, cluster and Azure operations, coding, issue tracking, and repeated review loops all consumed the same token budget. For a while, I had to buy additional capacity.</p>

<p>Optimization became the point where my own engineering contribution mattered most. Instead of sending all available context to every task, I designed a way to select only what was relevant.</p>

<p>One of my earliest skill files exceeded 100,000 words. In my current use case, I manage a similar scope with roughly 5,000 words of core instruction plus documentation loaded on demand. This is a personal example of what better context selection achieved, not a general promise of the same reduction for every workload. I did more than edit for brevity; I changed the context architecture.</p>

<ul>
  <li>Common rules live in one place.</li>
  <li>Task-specific documents load only when needed.</li>
  <li>Agents receive the relevant issue and files, not the entire history.</li>
  <li>Output format and length are constrained.</li>
  <li>Tools return structured results instead of repeated prose.</li>
  <li>Simple tasks do not use the most expensive model or multiple reviews.</li>
</ul>

<p>The same plan can now handle authorized reviews, development, issue management, cluster and platform operations, and much of my personal work. Lower token use is only one benefit. With less irrelevant context, results also became more consistent.</p>

<p>There is no universal optimization recipe here. The type of work, available tools, data sensitivity, cost of failure, and expected output must be analyzed together before the right combination of models, context, and controls can be selected. An efficient system comes less from copying a ready-made skill than from understanding how an organization actually works and designing for that environment.</p>

<h2 id="employer-policy-data-and-authority">Employer policy, data, and authority</h2>

<p>Technical capability is not authorization. When work involves an employer, customer, or regulated data, company policy, contracts, data classification, regulation, and provider terms all matter.</p>

<p>Sending information from defense, healthcare, finance, or another controlled environment to a general SaaS service may be unacceptable. An internal model, private cloud, data masking, or a restricted set of approved tools may be required instead. The appropriate design depends on the industry, the data, and the threat model.</p>

<p>AI should therefore not be introduced secretly into a workplace process. Security, legal, management, and engineering stakeholders need an explicit operating model. An agent should never receive broader permissions than the human role it represents.</p>

<h2 id="what-i-gained">What I gained</h2>

<p>The most valuable result is not that I can produce more work. It is that I recovered time. I can socialize more, go to the sea with my son almost every day, watch a film in the evening, and make our time together more meaningful.</p>

<p>In the past, part of my attention could remain trapped in a technical problem while I was with other people. Now I can record the problem and delegate research, alternatives, and the first implementation steps. I return when a real decision is required.</p>

<p>That does not reduce my responsibility. I delegated the operations, not the ownership.</p>

<h2 id="dependency-and-skill-decay">Dependency and skill decay</h2>

<p>AI-driven work creates dependency. A service outage can stop the process. More importantly, stepping away from the craft for too long can weaken your understanding and troubleshooting ability.</p>

<p>Junior engineers may never build the fundamentals; senior engineers may forget details they no longer practice. When operating an agent replaces understanding the system, engineering can gradually become little more than being a customer. I explored this risk further in <a href="/en/2025/11/25/artificial-intelligence-and-the-human-mind-the-erosion-of-thinking-decay-of-code-and-loss-of-control.html">Artificial Intelligence and the Human Mind: The Erosion of Thinking, Decay of Code, and Loss of Control</a>.</p>

<p>I therefore still do some work manually, read generated commands, explain architectural decisions, and make sure I can intervene without AI when the system fails. Automation should not purchase speed by giving up competence.</p>

<h2 id="decisions-stay-with-me">Decisions stay with me</h2>

<p>AI is no longer just a chatbot in my workflow, but it is not an unrestricted virtual employee either. It is an operational layer grounded in documentation, constrained by skills, and reviewed by other agents or models when the risk justifies it.</p>

<p>Successful delegation did not mean allowing AI to make more decisions. It meant implementing my decisions with less repetition, better records, and lower operational effort. Building this kind of system begins before model selection: the processes must be analyzed and organization-specific authority and control mechanisms must be designed. The technology components may be available, but turning them into a dependable operating model is the real design work.</p>

<p>The final review remains mine, because even when AI acts on my behalf, the model will not be the one accountable for the outcome.</p>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="artificial-intelligence" /><category term="ai-agents" /><category term="automation" /><category term="skills" /><category term="llm" /><summary type="html"><![CDATA[How I turned AI from a chatbot into an operational layer using skills, documentation, independent review, explicit permissions, and context optimization.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/ai-operations-decision-authority-cover.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/ai-operations-decision-authority-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="tr-TR"><title type="html">AI’ı Nasıl Kullanıyorum? Operasyonlar AI’da, Kararlar Bende</title><link href="https://fmarslan.com/tr/2026/08/07/aiyi-nasil-kullaniyorum-operasyonlar-aida-kararlar-bende.html" rel="alternate" type="text/html" title="AI’ı Nasıl Kullanıyorum? Operasyonlar AI’da, Kararlar Bende" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://fmarslan.com/tr/2026/08/07/aiyi-nasil-kullaniyorum-operasyonlar-aida-kararlar-bende</id><content type="html" xml:base="https://fmarslan.com/tr/2026/08/07/aiyi-nasil-kullaniyorum-operasyonlar-aida-kararlar-bende.html"><![CDATA[<p>Bugün geliştirmeden Kubernetes ve Azure operasyonlarına, dokümantasyondan bu blogun yönetimine kadar işlerimin büyük bölümünü AI ile yürütüyorum. Ancak kararları AI’a devretmedim; işi standartlaştırıyor, sınırlarını yazıyor ve uygulanmasını ona bırakıyorum.</p>

<p>Bu noktaya tek bir iyi prompt yazarak gelmedim. Yerel model denemeleri, büyüyen skill dosyaları, tükenen token limitleri ve hataya açık otomasyonlar üzerinden ilerledim. Sonunda öğrendiğim şey şuydu: Bir modeli çalıştırmakla, güvenilir bir operasyon sistemi kurmak aynı şey değil.</p>

<h2 id="yerel-model-fikri-neden-hâlâ-cazip">Yerel model fikri neden hâlâ cazip?</h2>

<p>Yerel bilgisayarımda SaaS tabanlı bir agent çalıştırmak ilk zamanlarda beni ciddi biçimde tedirgin ediyordu. Veri gizliliği, şirket bilgilerinin dışarı çıkması ve hangi içeriğin hangi sağlayıcıya gönderildiği hâlâ önemsediğim konular. Bunların kolay veya herkes için geçerli tek bir cevabı yok.</p>

<p>Modeli tamamen yerelde ve çevrimdışı çalıştırma fikri bu nedenle çok çekici. Fakat bulut servislerinden aldığımız katma değer yalnızca model ağırlıklarından gelmiyor. Arama motoru, web istemcisi, doküman ayrıştırıcıları, indeksleme, kod çalıştırma ortamı, araç bağlantıları, kimlik ve izin yönetimi gibi katmanlar modelin iş kalitesini doğrudan etkiliyor.</p>

<p>Yerelde yalnızca bir model ayağa kaldırdığınızda çoğu zaman gelişmiş bir sohbet botu elde ediyorsunuz. Aynı kalite ve hareket alanı için modele araçlar, API’ler, arama, erişim politikaları, gözlemlenebilirlik ve bir yürütme ortamı sağlamanız gerekiyor. Bunu yeterince ileri götürdüğünüzde kendiniz için küçük bir AI platformu kurmaya başlıyorsunuz.</p>

<p>Bu tercih yanlış değil. Kendi e-posta sunucunuzu evde çalıştırmaya benziyor: Yapabilirsiniz, belirli koşullarda yapmanız da gerekebilir; fakat bakım, güvenlik, donanım ve zaman maliyetini de üstlenirsiniz. Büyük bir kurum değilseniz, yoğun kullanım üretmiyorsanız veya bu altyapıdan doğrudan gelir elde etmiyorsanız hazır hizmet çoğu zaman daha ekonomik kalıyor. Benim denemelerimde de sonuç bu oldu.</p>

<h2 id="model-tek-başına-operatör-değildir">Model tek başına operatör değildir</h2>

<p>LLM, kendisine verilen bağlama göre olası çıktıyı üretir. “Bilmiyorum” diyebilir; fakat bu, her bilgi boşluğunu güvenilir biçimde tespit ettiği anlamına gelmez. Eksik talimatın bıraktığı alanı makul görünen bir tahminle doldurması özellikle operasyon işlerinde tehlikelidir.</p>

<p>Bir kullanıcı yönetimi API’sinin iyi hazırlanmış dokümantasyonunu modele verdiğimizi düşünelim. Endpoint, kimlik doğrulama, istek gövdesi ve hata cevapları açıkça yazılmışsa doküman aynı zamanda çalışma bağlamının bir parçası olur. Agent, kullanıcı oluşturmak için gerekli çağrıyı hazırlayabilir ve çalıştığı ortamdan API’ye erişebiliyorsa isteği uygulayabilir.</p>

<p>Burada kritik ayrım erişimdir. Bulutta çalışan bir agent, yerel ağınızdaki servise kendiliğinden ulaşamaz. Yerel makinede veya ilgili ağ içinde çalışan bir agent, uygun kimlik bilgileri ve izinler sağlandığında bunu yapabilir. Modelin ne bildiği kadar, agent’ın nerede çalıştığı ve neye yetkili olduğu da sonucu belirler.</p>

<h2 id="api-dokümanından-skille">API dokümanından skill’e</h2>

<p>API dokümanı çağrının teknik biçimini açıklar. Skill ise bu API’yi kullanırken uyulacak operasyon politikasını tanımlar:</p>

<ul>
  <li>Önce hangi doküman okunacak?</li>
  <li>Hangi ortam varsayılan kabul edilecek?</li>
  <li>Hangi işlemler yalnızca okunabilir olacak?</li>
  <li>Hangi değişiklik kullanıcı onayı gerektirecek?</li>
  <li>Sonuç nasıl doğrulanacak ve raporlanacak?</li>
  <li>Hata durumunda yeniden deneme mi, durma mı tercih edilecek?</li>
</ul>

<p>Kullanıcı yönetimi uygulaması için hazırlanan bir skill, agent’a önce güncel API dokümanını bulmasını, uygun isteği üretmesini, değişikliğin etkisini kontrol etmesini ve sonucu bildirmesini söyleyebilir. Böylece genel amaçlı bir sohbet modeli, sınırları belirli bir uygulamayı işletebilen basit bir operatöre dönüşür.</p>

<p>Benim kullandığım temel akış şu:</p>

<pre><code class="language-mermaid">flowchart LR
    I["İhtiyaç"] --&gt; S["İlgili skill"]
    S --&gt; D["Güncel dokümantasyon"]
    D --&gt; P["Plan ve komut"]
    P --&gt; R["Bağımsız kontrol"]
    R --&gt;|Onay| E["Uygulama"]
    R --&gt;|Revizyon| P
    E --&gt; V["Doğrulama ve rapor"]
</code></pre>

<p>Skill komut ezberletmez; doğru kaynağı nasıl bulacağını, hangi sınırlar içinde hareket edeceğini ve ne zaman duracağını tarif eder.</p>

<h2 id="i̇kinci-ve-üçüncü-göz">İkinci ve üçüncü göz</h2>

<p>Agent kod yazarken hata yapabildiği gibi API çağrısında da hata yapabilir. Geliştirme ortamındaki küçük bir hata zaman kaybettirir; production ortamındaki yanlış çağrı ise doğrudan veri ve hizmet kaybına dönüşebilir.</p>

<p>Bu yüzden skill içinde bağımsız kontrol adımları tanımlıyorum. İlk agent ihtiyacı yorumlayıp komutu hazırlar. Ayrı bağlamdaki ikinci agent, hem ihtiyacı hem de önerilen işlemi inceler. Onay vermezse işlem uygulanmaz; bulgular ilk akışa dönerek planın revize edilmesini sağlar.</p>

<p>Aynı modelin ikinci kez bakması yararlı olsa da ortak model hatasına karşı tam bağımsızlık sağlamaz. Risk yükseldiğinde farklı bir model veya sağlayıcı üzerinden çapraz kontrol uygulanabilir. Codex ile hazırlanmış bir değişikliği Claude ya da Gemini ile gözden geçirmek gibi görünen yöntem, sürecin içine yazıldığında kişisel bir alışkanlık olmaktan çıkar ve tekrarlanabilir kontrol mekanizmasına dönüşür.</p>

<p>Bu yine de matematiksel bir doğruluk garantisi değildir. İki model aynı yanlış varsayıma dayanabilir. Bu nedenle geri döndürülemez işlemlerde insan onayı, en az yetki, yedekleme, dry-run, işlem öncesi ve sonrası doğrulama gibi klasik kontroller devam etmelidir.</p>

<h2 id="bir-takımı-simüle-etmek">Bir takımı simüle etmek</h2>

<p>Junior, senior, lead, architect ve tester gibi roller ekiplerde boşuna ortaya çıkmadı. Her biri probleme farklı bir açıdan bakar ve farklı sorumluluk taşır. Benzer ayrımı agent iş akışına verdiğimde tek bir uzun prompt yerine rolü ve bağlamı sınırlandırılmış çalışmalar elde ediyorum.</p>

<p>Örneğin architect çözüm sınırlarını belirleyebilir, developer değişikliği uygulayabilir, reviewer risk ve tutarlılığı inceleyebilir, tester ise kabul ölçütlerini doğrulayabilir. Bu rollerin her biri ayrı bir insan olduğu için değil, aynı işin farklı sorularla incelenmesini sağladığı için değerlidir.</p>

<p>İş yalnızca sohbet bağlamında tutulduğunda geçmiş hızla kaybolur. Süreci issue yönetim sistemine taşıdığımda ise roller issue’yu ilerletir, yorum bırakır, çıktıları kaydeder ve işi sonraki role devreder. Böylece sanal ekip, yaşayan bir iş kaydı üzerinde çalışır.</p>

<h2 id="yerel-bir-ai-platformunun-gerçek-bileşenleri">Yerel bir AI platformunun gerçek bileşenleri</h2>

<p>Bu yapıyı kurarken kullanılabilecek açık kaynak veya self-hosted bileşenler mevcut. Fakat aşağıdaki tablo, “bir model indirdim” ile “operasyon yapabilen bir platform kurdum” arasındaki mesafeyi de gösteriyor:</p>

<table>
  <thead>
    <tr>
      <th>Katman</th>
      <th>Örnek</th>
      <th>Ne sağlar?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Yerel model çalıştırma</td>
      <td><a href="https://github.com/ollama/ollama/blob/main/docs/api.md">Ollama</a></td>
      <td>Modelleri yerelde yönetmek, REST API ve tool calling üzerinden kullanmak</td>
    </tr>
    <tr>
      <td>Yüksek performanslı model sunumu</td>
      <td><a href="https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/">vLLM</a></td>
      <td>GPU üzerinde modelleri OpenAI uyumlu HTTP servisi olarak sunmak</td>
    </tr>
    <tr>
      <td>Model gateway</td>
      <td><a href="https://docs.litellm.ai/">LiteLLM</a></td>
      <td>Birden fazla model sağlayıcısını tek arayüz, yetkilendirme, limit ve maliyet takibi arkasında toplamak</td>
    </tr>
    <tr>
      <td>Operatör ve kullanıcı arayüzü</td>
      <td><a href="https://docs.openwebui.com/">Open WebUI</a></td>
      <td>Yerel ve bulut modellerini araçlar, doküman bilgisi ve kullanıcı arayüzüyle birleştirmek</td>
    </tr>
    <tr>
      <td>Arama</td>
      <td><a href="https://docs.searxng.org/">SearXNG</a></td>
      <td>Birden fazla kaynaktan sonuç toplayan, kendi ortamınızda çalıştırılabilen metasearch katmanı</td>
    </tr>
    <tr>
      <td>Agent orkestrasyonu</td>
      <td><a href="https://langchain-ai.github.io/langgraph/index.html">LangGraph</a></td>
      <td>Uzun süreli, state tutan ve insan onayı içerebilen agent akışları kurmak</td>
    </tr>
    <tr>
      <td>Araç bağlantısı</td>
      <td><a href="https://modelcontextprotocol.io/docs/learn/architecture">Model Context Protocol</a></td>
      <td>Araç, kaynak ve prompt’ları istemci-sunucu modeliyle agent’a açmak</td>
    </tr>
    <tr>
      <td>Kod arama ve erişim</td>
      <td><a href="https://beaconbay.github.io/ck/">CK</a></td>
      <td>Kod tabanında çevrimdışı hibrit ve semantik arama yapmak, sonuçları MCP üzerinden agent’a açmak</td>
    </tr>
  </tbody>
</table>

<p>Liste bir kurulum reçetesi değil. Kimlik yönetimi, secret saklama, sandbox, loglama, değerlendirme, yedekleme ve ağ politikaları gibi üretim sorumlulukları ayrıca çözülmek zorunda. GPU’lu bir cluster’ınız varsa bunları deneyebilirsiniz; eski kripto madencilerinin donanımı burada yeniden anlam kazanabilir. Yine de donanım kiralamak başka, güvenilir ve izole bir model hizmeti işletmek başka iştir.</p>

<p>Araçları kurmak bu sistemin en görünür, fakat çoğu zaman en kolay kısmıdır. Asıl çalışma; kurumun süreçlerini analiz etmek, karar ve yetki sınırlarını çıkarmak, doğru bağlamı doğru anda sağlamak, güvenlik kapılarını tasarlamak ve bütün parçaları mevcut platformlarla güvenilir biçimde birleştirmektir. Aynı araç listesi iki farklı kurumda tamamen farklı bir operasyon modeline dönüşebilir.</p>

<h2 id="ben-bu-yapıyı-nasıl-uyguladım">Ben bu yapıyı nasıl uyguladım?</h2>

<p>Kendi yönettiğim ve AI kullanımı için yetkilendirilmiş kapsamdaki Kubernetes cluster’larını, Azure servislerini, geliştirme işlerini, teknik dokümantasyonu, sosyal medya süreçlerini ve bu blogun yönetimini aşama aşama AI’a taşıdım. Bugün bilgisayar başında olmasam bile telefonumdan verdiğim talep yetkilendirilmiş çalışma ortamım üzerinden yürütülebiliyor. Biraz Iron Man’deki Jarvis benzetmesini çağrıştırıyor; fakat arkasında sihir değil, standart, erişim ve dokümantasyon var.</p>

<p>Her iş için aynı sırayı izliyorum:</p>

<ol>
  <li>İşi tekrarlanabilir bir standarda dönüştürüyorum.</li>
  <li>Karar noktalarını ve yasak alanları yazıyorum.</li>
  <li>Gerekli dokümantasyonu hazırlıyorum.</li>
  <li>Skill’in doğru dokümanı seçmesini sağlıyorum.</li>
  <li>Risk seviyesine göre review ve onay kapıları ekliyorum.</li>
  <li>İşlem sonrasında sonucu bağımsız olarak doğrulatıyorum.</li>
</ol>

<p>Doküman hazırlamak bazen kod yazmak kadar zaman alıyor. Fark şu: Eskiden hem kodu hem dokümanı üretirken artık bazı işlerde yalnızca iyi tanımı ve dokümanı bir kez hazırlıyorum. Tekrarlanan kodlama veya operasyon adımlarını agent aynı kaynağa dayanarak yürütebiliyor.</p>

<p>Dokümanda tahmine açık boşluk bırakmamak çok önemli. Bir fotoğrafımın izinsiz biçimde makaleye eklenmesini, özel mesajın yanlış kişiye gönderilmesini veya production veritabanına eski bir yedeğin dönülmesini istemem. Bu sınırlar yalnızca “dikkatli ol” cümlesiyle değil; hedef doğrulama, açık yasaklar, onay koşulları ve geri alma planıyla tanımlanmalı.</p>

<h2 id="token-duvarı-ve-optimizasyon">Token duvarı ve optimizasyon</h2>

<p>İlk kurduğum sistem çalışıyordu; fakat ekonomik değildi. Kullandığım yaklaşık 100 avroluk paketin haftalık limiti yetmemeye başladı. E-posta yönetimi, cluster ve Azure operasyonları, kod geliştirme, issue takibi ve çoklu review akışları aynı bütçeden token tüketiyordu. Uzun süre ek paket almak zorunda kaldım.</p>

<p>Asıl katma değerim bu noktadan sonra ortaya çıktı: Her göreve bütün bağlamı göndermek yerine yalnızca gerekli bağlamı seçmek.</p>

<p>İlk skill dosyalarımdan biri 100 bin kelimeyi aşmıştı. Bugün kendi kullanım senaryomda benzer kapsamı yaklaşık 5 bin kelimelik çekirdek talimat ve ihtiyaç anında okunan dokümanlarla yönetiyorum. Bu oran her işte aynı sonucu verecek genel bir tasarruf vaadi değil; doğru bağlam seçiminin etkisini gösteren kişisel bir örnek. Yaptığım şey yalnızca metni kısaltmak değil, bağlam mimarisini değiştirmekti.</p>

<ul>
  <li>Ortak kuralları tek yerde tutuyorum.</li>
  <li>Göreve özel dokümanı ihtiyaç anında yüklüyorum.</li>
  <li>Agent’a bütün geçmişi değil ilgili issue ve dosyaları veriyorum.</li>
  <li>Çıktı biçimini ve uzunluğunu sınırlıyorum.</li>
  <li>Tekrarlanan açıklamalar yerine araçlardan yapılandırılmış sonuç alıyorum.</li>
  <li>Basit işlerde pahalı model ve çoklu review kullanmıyorum.</li>
</ul>

<p>Bugün izin verilen kapsam içindeki teknik review, kod geliştirme, issue yönetimi, cluster ve platform operasyonlarıyla kişisel işlerimin büyük bölümü aynı paket içinde rahatça yürüyebiliyor. Kazanç yalnızca daha az token değil; modelin gereksiz bağlam içinde kaybolmaması sayesinde daha tutarlı sonuç.</p>

<p>Burada herkese uygulanabilecek tek bir optimizasyon reçetesi yok. İş türü, kullanılan araçlar, veri hassasiyeti, hata maliyeti ve beklenen çıktı birlikte analiz edilmeden bir kurum için doğru model, bağlam ve kontrol düzeni belirlenemez. Verimli bir yapı, hazır bir skill’i kopyalamaktan çok o işletmenin gerçek çalışma biçimini doğru okuyup ona özel bir sistem tasarlamayı gerektirir.</p>

<h2 id="i̇şveren-veri-ve-yetki-sınırı">İşveren, veri ve yetki sınırı</h2>

<p>İş yerinizle ilgili süreçleri AI ile yürütüyorsanız teknik olarak yapabiliyor olmanız yeterli değildir. İşverenin politikası, müşteri sözleşmeleri, veri sınıflandırması, düzenleyici şartlar ve kullanılan sağlayıcının koşulları birlikte değerlendirilmelidir.</p>

<p>Savunma sanayii, sağlık, finans veya başka bir regüle alanda genel amaçlı SaaS hizmetine veri göndermek kabul edilemez olabilir. Böyle bir durumda kurum içi model, özel bulut, veri maskeleme veya yalnızca onaylanmış araçların kullanılması gerekebilir. Doğru çözüm sektöre, veriye ve tehdit modeline göre değişir.</p>

<p>Bu yüzden işe gizlice AI eklemek yerine güvenlik, hukuk, yönetim ve teknik ekiplerle açık bir model üzerinde anlaşmak gerekir. Agent’a verilen yetki de insan kullanıcının yetkisinden daha geniş olmamalıdır.</p>

<h2 id="bana-ne-kazandırdı">Bana ne kazandırdı?</h2>

<p>En görünür sonuç daha fazla iş üretmek değil, zamanımı geri kazanmak oldu. Sosyalleşmeye daha fazla vakit ayırabiliyorum. Oğlumla neredeyse her gün denize gidebiliyor, akşamları rahatça film izleyebiliyor ve baba-oğul zamanını daha verimli yaşayabiliyorum.</p>

<p>Eskiden bir etkinliğin ortasında zihnim hâlâ “bu teknik problemi nasıl çözerim?” sorusuyla meşgul olabiliyordu. Şimdi problemi kayıt altına alıp araştırma, alternatif üretme ve ilk uygulama adımlarını sisteme bırakabiliyorum. Ben yeniden karar noktasında sürece katılıyorum.</p>

<p>Bu, daha az sorumluluk anlamına gelmiyor. Operasyonları devrettim; sahipliği değil.</p>

<h2 id="bağımlılık-ve-yetkinlik-kaybı">Bağımlılık ve yetkinlik kaybı</h2>

<p>AI ile yürütülen işlerde güçlü bir bağımlılık oluşuyor. Servise erişemediğinizde süreç durabilir. Daha önemlisi, uzun süre elinizi işin içinden çekerseniz kullandığınız teknolojiyi anlama ve sorun çözme kasınız zayıflayabilir.</p>

<p>Junior geliştirici temel becerileri hiç kazanamayabilir; senior geliştirici ise kullanmadığı ayrıntıları zamanla unutabilir. Agent’ı yönetmek, sistemi anlamanın yerini aldığında mühendislik rolü kolayca yalnızca müşteri olmaya dönüşür. Bu riski <a href="/2025/11/25/yapay-zeka-dusunmenin-yerini-alabilir-mi.html">Yapay Zekâ ve İnsan Zihni: Düşünmenin Erozyonu, Kodun Çürümesi ve Kontrolün Kaybı</a> yazısında daha ayrıntılı ele almıştım.</p>

<p>Bu nedenle zaman zaman işi elle yapmak, üretilen komutu okuyabilmek, mimari kararı açıklayabilmek ve sistem arızalandığında AI olmadan müdahale edebilmek gerekiyor. Otomasyon, bilgi kaybı pahasına kazanılan hız olmamalı.</p>

<h2 id="sonuç-kararlar-bende-uygulama-aida">Sonuç: Kararlar bende, uygulama AI’da</h2>

<p>Geldiğim noktada AI benim için bir sohbet botu veya sınırsız yetkili sanal çalışan değil. Dokümanla beslenen, skill’lerle sınırlandırılan, gerektiğinde başka agent ve modeller tarafından kontrol edilen bir operasyon katmanı.</p>

<p>Başarılı delegasyon, AI’ın daha fazla karar vermesi değil; benim verdiğim kararları daha az tekrar, daha tutarlı kayıt ve daha düşük operasyon yüküyle uygulaması oldu. Böyle bir sistemin kurulması model seçiminden önce süreçlerin analiz edilmesini, kuruma özel yetki ve kontrol mekanizmalarının tasarlanmasını gerektiriyor. Teknoloji parçaları hazır olsa bile onları güvenilir bir çalışma modeline dönüştüren kısım bu tasarım emeği.</p>

<p>Son review hâlâ bende. Çünkü AI benim adıma işlem yapsa da yanlışlığın hesabını verecek olan model değil, benim.</p>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="yapay-zeka" /><category term="ai-agent" /><category term="otomasyon" /><category term="skill" /><category term="llm" /><summary type="html"><![CDATA[AI'ı bir sohbet botu yerine operasyon katmanı olarak nasıl kullandığımı; skill, dokümantasyon, bağımsız kontrol ve token optimizasyonu üzerinden anlatıyorum.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/ai-operations-decision-authority-cover.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/ai-operations-decision-authority-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en-US"><title type="html">Managing Dependencies: Where Should Microservice Boundaries Be Drawn?</title><link href="https://fmarslan.com/en/2026/08/04/managing-dependencies-and-microservice-boundaries.html" rel="alternate" type="text/html" title="Managing Dependencies: Where Should Microservice Boundaries Be Drawn?" /><published>2026-08-04T00:00:00+00:00</published><updated>2026-08-04T00:00:00+00:00</updated><id>https://fmarslan.com/en/2026/08/04/managing-dependencies-and-microservice-boundaries</id><content type="html" xml:base="https://fmarslan.com/en/2026/08/04/managing-dependencies-and-microservice-boundaries.html"><![CDATA[<p>No component in a software system is completely isolated. The important question is not whether a dependency exists, but how far a change, delay, or failure in one component can spread through the rest of the system.</p>

<p>We often say that microservices should be independent. Yet an order may require a payment, a payment belongs to a customer, and shipping depends on inventory. Absolute independence is neither realistic nor useful. The goal is to <strong>understand dependency boundaries and control their effects</strong>.</p>

<h2 id="what-is-a-dependency">What is a dependency?</h2>

<p>A component depends on another component when it needs that component’s availability, behavior, or data to perform its responsibility.</p>

<p>An order service may depend on:</p>

<ul>
  <li>a payment service to collect money,</li>
  <li>an inventory service to check availability,</li>
  <li>a customer service to obtain a delivery address,</li>
  <li>a message broker to publish events,</li>
  <li>or a particular database schema to process data.</li>
</ul>

<p>A dependency is not limited to one service making an HTTP request to another. Shared databases, common libraries, API and event contracts, deployment procedures, and recurring coordination between teams can all create dependencies.</p>

<h2 id="a-dependency-is-not-the-same-as-a-necessity">A dependency is not the same as a necessity</h2>

<p>Business needs and technical design decisions should be separated. Requiring payment before confirming an order may be a <strong>business necessity</strong>. Making the order service call the payment service synchronously is a <strong>technical design decision</strong>.</p>

<p>The distinction becomes clearer when the same example is classified:</p>

<table>
  <thead>
    <tr>
      <th>Situation</th>
      <th>Type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>An order cannot be confirmed before payment</td>
      <td>Business rule</td>
    </tr>
    <tr>
      <td>Transaction records must be retained</td>
      <td>Regulatory requirement</td>
    </tr>
    <tr>
      <td>A separate service handles the payment</td>
      <td>Service dependency</td>
    </tr>
    <tr>
      <td>The payment result is awaited in the same HTTP request</td>
      <td>Runtime dependency</td>
    </tr>
    <tr>
      <td>Order and payment services must be released together</td>
      <td>Deployment dependency</td>
    </tr>
  </tbody>
</table>

<p>We may not be able to remove a business or regulatory requirement. However, implementing it with a particular protocol, shared transaction, or shared database is usually an architectural choice. We can change that choice, reduce its impact, and design for failure.</p>

<h2 id="when-does-a-relationship-become-a-dependency">When does a relationship become a dependency?</h2>

<p>If the answer to any of the following questions is yes, there is a dependency worth managing:</p>

<ul>
  <li>Does my component stop working when the other component is unavailable?</li>
  <li>Does a change in the other component require a change in my code?</li>
  <li>Must the two components be released at the same time?</li>
  <li>Does my service need to know the other service’s internal data model?</li>
  <li>Do both services write to the same database tables?</li>
  <li>Does one service’s performance problem directly affect the other?</li>
  <li>Does one team repeatedly have to wait for another team to deliver a feature?</li>
</ul>

<p>These questions show that dependencies exist beyond source code. Runtime, data, contract, deployment, and organizational dependencies create different kinds of risk.</p>

<h3 id="runtime-dependency">Runtime dependency</h3>

<p>Service A waits for an immediate response from service B before completing an operation. If no fallback, cache, or deferred path has been defined, A cannot complete that flow while B is unavailable. As the call chain grows, latency and the probability of failure grow with it.</p>

<h3 id="data-dependency">Data dependency</h3>

<p>One service needs data managed by another. That need is normal, but direct access to the same tables turns every schema change into a shared release problem.</p>

<h3 id="contract-dependency">Contract dependency</h3>

<p>Services communicate through APIs, events, or message formats. An uncontrolled contract change can break consumers. Asynchronous communication does not remove this dependency; it changes its form.</p>

<h3 id="deployment-dependency">Deployment dependency</h3>

<p>If releasing one service requires several other services to be updated at the same time, the services may be physically separate but are not independently deployable.</p>

<h3 id="organizational-dependency">Organizational dependency</h3>

<p>When one team repeatedly waits for another team’s implementation or approval, a technical relationship has become an organizational bottleneck.</p>

<p>Shared libraries and infrastructure are not automatically harmful. They become a significant dependency when a shared change forces many services to upgrade together or prevents teams from working independently.</p>

<h2 id="where-should-the-microservice-boundary-be-drawn">Where should the microservice boundary be drawn?</h2>

<p>Microservice architecture is not an attempt to remove every relationship between services. Its purpose is to let each service make decisions within its business area without knowing the internal design of other services or inheriting every temporary failure they experience.</p>

<p>A healthy microservice should:</p>

<ul>
  <li>own a defined business capability,</li>
  <li>manage its own data,</li>
  <li>avoid direct access to another service’s database,</li>
  <li>expose explicit and backward-compatible contracts,</li>
  <li>be independently deployable where practical,</li>
  <li>tolerate temporary failures within defined limits,</li>
  <li>and avoid knowledge of other services’ implementation details.</li>
</ul>

<p>The acceptable dependency boundary can be evaluated through a service’s ability to <strong>change, deploy, and fail independently</strong>. If a small change requires many services to be updated together, the system is tightly coupled even if it is distributed across separate processes.</p>

<h2 id="how-synchronous-and-asynchronous-communication-change-dependency">How synchronous and asynchronous communication change dependency</h2>

<p>With synchronous communication, the caller waits for the other service. This model is direct and easy to understand, and it can be appropriate for short operations that require an immediate answer. However, latency and failures propagate through the call chain.</p>

<p>With asynchronous communication, a service publishes an event that other services process later. The services no longer need to be available at the same moment, but message ordering, retries, idempotency, observability, and eventual consistency must now be managed.</p>

<div class="mermaid">
flowchart LR
    subgraph S["Synchronous flow"]
      O1["Order"] --&gt;|"waits"| P1["Payment"]
      P1 --&gt;|"waits"| I1["Inventory"]
    end
    subgraph A["Asynchronous flow"]
      O2["Order"] --&gt; B(("Event broker"))
      B --&gt; P2["Payment"]
      B --&gt; I2["Inventory"]
    end
</div>

<p>Asynchronous communication does not eliminate dependency. When the message is reliably accepted and persisted, the order service no longer depends on the payment service being available at that exact moment, but it still depends on the event contract and messaging infrastructure. The dependency is loosened in time; the responsibility remains.</p>

<h2 id="useful-dependency-and-harmful-coupling">Useful dependency and harmful coupling</h2>

<p>Delegating payment processing to a service that owns that business capability can be a useful dependency. Requiring the order service to understand the payment service’s internal tables, classes, or release schedule is harmful coupling.</p>

<table>
  <thead>
    <tr>
      <th>Manageable dependency</th>
      <th>Harmful coupling</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Based on a clear business responsibility</td>
      <td>Responsibility boundaries are unclear</td>
    </tr>
    <tr>
      <td>Uses a stable contract</td>
      <td>Relies on internal implementation details</td>
    </tr>
    <tr>
      <td>Is one-way where practical</td>
      <td>Is bidirectional or cyclic</td>
    </tr>
    <tr>
      <td>Defines failure behavior</td>
      <td>Propagates failures through the system</td>
    </tr>
    <tr>
      <td>Allows independent deployment</td>
      <td>Forces coordinated releases</td>
    </tr>
    <tr>
      <td>Is observable and testable</td>
      <td>Becomes visible only in production</td>
    </tr>
  </tbody>
</table>

<p>Well-designed dependencies separate responsibilities, allow teams to specialize in business domains, and let components scale according to their own workload. Uncontrolled dependencies turn small changes into large coordination efforts, make test environments difficult to create, and tie system performance to the slowest component.</p>

<p>The result can be a <strong>distributed monolith</strong>: a system made of separate services that still have to be developed, tested, and released together. It combines the coupling of a monolith with the network and operational costs of a distributed system.</p>

<h2 id="how-should-dependencies-be-managed">How should dependencies be managed?</h2>

<h3 id="define-boundaries-around-business-capabilities">Define boundaries around business capabilities</h3>

<p>Design services around business responsibilities rather than technical layers. Generic services used by everyone can gradually become central bottlenecks.</p>

<h3 id="give-every-data-set-an-owner">Give every data set an owner</h3>

<p>The authoritative owner of the data and its write rules should be explicit. Other services may access it through defined APIs or events and keep controlled local copies when necessary, but they should not take ownership of the source’s write rules.</p>

<h3 id="evolve-contracts-compatibly">Evolve contracts compatibly</h3>

<p>API and event schemas are contracts between services. Contract tests and deliberate versioning help prevent a provider change from unexpectedly breaking its consumers.</p>

<h3 id="question-long-synchronous-call-chains">Question long synchronous call chains</h3>

<p>When a single user request calls many services in sequence, both latency and failure probability increase. If the user does not need the complete result immediately, part of the work may be handled asynchronously.</p>

<h3 id="treat-failure-as-a-normal-design-condition">Treat failure as a normal design condition</h3>

<p>Timeouts, bounded retries, circuit breakers, idempotency, queues, and appropriate fallback responses can contain the impact of a dependency. Unbounded retries deserve particular care because they can send even more traffic to a service that is already struggling.</p>

<h3 id="prevent-cyclic-dependencies">Prevent cyclic dependencies</h3>

<p>If service A depends on B and B also depends on A, revisit the service boundaries or ownership of the workflow. A cycle can be evidence that a separate business capability has not yet been recognized.</p>

<h3 id="make-dependencies-visible">Make dependencies visible</h3>

<p>Service maps, distributed traces, metrics, and correlated logs should reveal which services depend on one another. A relationship that is missing from the documentation does not disappear in production.</p>

<h2 id="before-introducing-a-new-dependency">Before introducing a new dependency</h2>

<p>The following questions form a useful review checklist before adding a service call, shared library, or event contract:</p>

<ol>
  <li>Is this relationship a genuine business requirement or a consequence of the current design?</li>
  <li>Does the user need the result immediately?</li>
  <li>What happens when the called service is unavailable?</li>
  <li>Can the operation continue with cached or slightly stale data?</li>
  <li>Can the work be completed later?</li>
  <li>Who owns the contract, and how will it evolve?</li>
  <li>Can the services still be deployed independently?</li>
  <li>Does the design require direct access to another service’s database?</li>
  <li>Is the dependency becoming bidirectional or cyclic?</li>
  <li>Can we observe the effects of failures, delays, and changes in production?</li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>The goal of microservices is not zero dependency; that would be unrealistic for a system whose parts collaborate. The goal is to make dependencies explicit, bounded, mostly one-way, resilient, and open to change.</p>

<p>One question provides a useful starting point:</p>

<blockquote>
  <p>Does a change, delay, or failure in one service unnecessarily affect the code, release schedule, or runtime behavior of other services?</p>
</blockquote>

<p>If the answer is repeatedly yes, the relationship is more than a natural business dependency: it is tight technical coupling. Good architecture does not hide dependencies or pretend to remove them. It makes them visible, draws deliberate boundaries around them, and prevents their effects from spreading through the system without control.</p>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="microservices" /><category term="dependencies" /><category term="software-architecture" /><category term="distributed-systems" /><category term="integration" /><summary type="html"><![CDATA[Microservices cannot eliminate dependencies. The practical goal is to contain how changes, latency, and failures propagate across service boundaries.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/microservice-dependency-boundaries.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/microservice-dependency-boundaries.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="tr-TR"><title type="html">Projelerde Bağımlılık: Mikroservislerde Sınır Nerede Başlar?</title><link href="https://fmarslan.com/tr/2026/08/04/projelerde-bagimlilik-ve-mikroservislerde-sinirlari.html" rel="alternate" type="text/html" title="Projelerde Bağımlılık: Mikroservislerde Sınır Nerede Başlar?" /><published>2026-08-04T00:00:00+00:00</published><updated>2026-08-04T00:00:00+00:00</updated><id>https://fmarslan.com/tr/2026/08/04/projelerde-bagimlilik-ve-mikroservislerde-sinirlari</id><content type="html" xml:base="https://fmarslan.com/tr/2026/08/04/projelerde-bagimlilik-ve-mikroservislerde-sinirlari.html"><![CDATA[<p>Yazılım projelerinde hiçbir bileşen bütünüyle yalnız değildir. Asıl mesele bağımlılığın varlığı değil; bu ilişkinin bir bileşendeki değişikliği, gecikmeyi veya hatayı sistemin geri kalanına ne ölçüde taşıdığıdır.</p>

<p>Mikroservis mimarisinde “servisler birbirinden bağımsız olmalıdır” cümlesini sıkça kullanıyoruz. Fakat siparişin ödemeye, ödemenin müşteriye, sevkiyatın stoğa ihtiyaç duyduğu bir sistemde mutlak bağımsızlık mümkün değildir. Bu nedenle hedef sıfır bağımlılık değil, <strong>bağımlılığın sınırlarını bilmek ve etkisini yönetmektir</strong>.</p>

<h2 id="bağımlılık-nedir">Bağımlılık nedir?</h2>

<p>Bir bileşen görevini yerine getirebilmek için başka bir bileşenin varlığına, davranışına veya ürettiği veriye ihtiyaç duyuyorsa aralarında bağımlılık vardır.</p>

<p>Örneğin bir sipariş servisi:</p>

<ul>
  <li>ödeme servisinden tahsilat yapıyorsa,</li>
  <li>stok servisinden ürün miktarını sorguluyorsa,</li>
  <li>kullanıcı servisinden teslimat adresini alıyorsa,</li>
  <li>bir mesaj kuyruğuna veri gönderiyorsa,</li>
  <li>belirli bir veritabanı şemasına göre işlem yapıyorsa</li>
</ul>

<p>bu bileşenlere farklı biçimlerde bağımlıdır.</p>

<p>Bağımlılık yalnızca bir servisin diğerine HTTP isteği göndermesi değildir. Paylaşılan veritabanları, ortak kütüphaneler, API ve olay sözleşmeleri, dağıtım süreçleri ve ekipler arasındaki sürekli koordinasyon ihtiyacı da bağımlılık oluşturabilir.</p>

<h2 id="bağımlılık-ile-zorunluluk-aynı-şey-değildir">Bağımlılık ile zorunluluk aynı şey değildir</h2>

<p>İş ihtiyacı ile teknik tasarımı birbirinden ayırmak gerekir. Bir siparişin tamamlanabilmesi için ödeme alınması bir <strong>iş zorunluluğu</strong> olabilir. Sipariş servisinin ödeme servisine senkron HTTP isteği göndermesi ise bir <strong>teknik tasarım kararıdır</strong>.</p>

<p>Aradaki farkı aynı örnek üzerinden açalım:</p>

<table>
  <thead>
    <tr>
      <th>Durum</th>
      <th>Türü</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ödeme alınmadan siparişin onaylanmaması</td>
      <td>İş kuralı</td>
    </tr>
    <tr>
      <td>İşlemin yasal kayıtlarının saklanması</td>
      <td>Yasal zorunluluk</td>
    </tr>
    <tr>
      <td>Ödemenin ayrı bir servis tarafından gerçekleştirilmesi</td>
      <td>Servis bağımlılığı</td>
    </tr>
    <tr>
      <td>Ödeme sonucunun aynı HTTP isteği içinde beklenmesi</td>
      <td>Çalışma zamanı bağımlılığı</td>
    </tr>
    <tr>
      <td>Sipariş ve ödeme servislerinin birlikte yayımlanması</td>
      <td>Dağıtım bağımlılığı</td>
    </tr>
  </tbody>
</table>

<p>İş veya mevzuat kaynaklı zorunluluğu ortadan kaldıramayabiliriz. Ancak bu zorunluluğun belirli bir protokolle, aynı işlem zincirinde veya ortak bir veritabanıyla uygulanması çoğu zaman mimari tercihtir. Bu tercihin etkisini azaltabilir, yönünü değiştirebilir ve hata durumlarına karşı dayanıklı hâle getirebiliriz.</p>

<h2 id="bir-ilişki-nerede-bağımlılığa-dönüşür">Bir ilişki nerede bağımlılığa dönüşür?</h2>

<p>Aşağıdaki sorulardan birine “evet” cevabı veriliyorsa dikkate alınması gereken bir bağımlılık vardır:</p>

<ul>
  <li>Diğer bileşen çalışmadığında benim bileşenim de çalışamaz mı?</li>
  <li>Diğer bileşendeki değişiklik benim kodumu değiştirmemi gerektiriyor mu?</li>
  <li>İki bileşeni aynı anda yayımlamak zorunda mıyım?</li>
  <li>Diğer servisin iç veri modelini bilmek zorunda mıyım?</li>
  <li>İki servis aynı veritabanı tablolarını kullanıyor mu?</li>
  <li>Bir servisin performans sorunu diğerini doğrudan etkiliyor mu?</li>
  <li>Bir özelliği geliştirmek için sürekli başka bir ekibi bekliyor muyum?</li>
</ul>

<p>Bu sorular bağımlılığın yalnızca kod düzeyinde olmadığını gösterir. Çalışma zamanı, veri, sözleşme, dağıtım ve organizasyon bağımlılıkları birbirinden farklı sonuçlar üretir.</p>

<h3 id="çalışma-zamanı-bağımlılığı">Çalışma zamanı bağımlılığı</h3>

<p>A servisi bir işlemi tamamlamak için B servisinden anlık cevap bekler. Alternatif bir akış, önbellek veya işlemi erteleme seçeneği tanımlanmamışsa B çalışmadığında A da bu akışı tamamlayamaz. Çağrı zinciri uzadıkça toplam gecikme ve hata olasılığı artar.</p>

<h3 id="veri-bağımlılığı">Veri bağımlılığı</h3>

<p>Bir servis başka bir servisin yönettiği veriye ihtiyaç duyar. Bu ihtiyaç normaldir; ancak iki servisin aynı tablolara doğrudan erişmesi, veri modelindeki her değişikliği ortak bir dağıtım problemine dönüştürür.</p>

<h3 id="sözleşme-bağımlılığı">Sözleşme bağımlılığı</h3>

<p>Servisler API, olay veya mesaj formatları üzerinden anlaşır. Bu sözleşmenin kontrolsüz değiştirilmesi tüketicileri bozabilir. Asenkron iletişim de bu bağımlılığı ortadan kaldırmaz; yalnızca sözleşmenin biçimini değiştirir.</p>

<h3 id="dağıtım-bağımlılığı">Dağıtım bağımlılığı</h3>

<p>Bir servisin yeni sürümünü yayımlamak için diğer servislerin de aynı anda güncellenmesi gerekiyorsa servisler fiziksel olarak ayrılmış olsa bile bağımsız değildir.</p>

<h3 id="organizasyonel-bağımlılık">Organizasyonel bağımlılık</h3>

<p>Bir ekip işini tamamlamak için sürekli başka bir ekibin geliştirme yapmasını veya onay vermesini bekliyorsa teknik ilişki organizasyonel darboğaza dönüşmüştür.</p>

<p>Ortak kütüphaneler ve altyapı bileşenleri tek başına zararlı değildir. Ancak ortak bileşendeki değişiklik birçok servisi aynı anda güncellemeye zorluyor veya ekiplerin bağımsız hareket etmesini engelliyorsa artık ayrıca yönetilmesi gereken bir bağımlılıktır.</p>

<h2 id="mikroservislerde-sınır-nerede-çizilir">Mikroservislerde sınır nerede çizilir?</h2>

<p>Mikroservis mimarisinin amacı servisler arasındaki bütün ilişkileri yok etmek değildir. Amaç, her servisin kendi iş alanında karar verebilmesi; başka servislerin iç yapısını bilmemesi ve onların geçici sorunlarından mümkün olduğunca az etkilenmesidir.</p>

<p>Sağlıklı bir mikroservis:</p>

<ul>
  <li>belirli bir iş yeteneğinin sahibi olmalı,</li>
  <li>kendi verisini yönetmeli,</li>
  <li>başka bir servisin veritabanına doğrudan erişmemeli,</li>
  <li>açık ve geriye uyumlu sözleşmeler sunmalı,</li>
  <li>mümkün olduğunda bağımsız yayımlanabilmeli,</li>
  <li>geçici hataları belirli ölçüde tolere edebilmeli,</li>
  <li>diğer servislerin uygulama ayrıntılarını bilmemelidir.</li>
</ul>

<p>Bağımlılığın kabul edilebilir sınırını, servisin <strong>bağımsız değişebilme, yayımlanabilme ve hata verebilme kabiliyeti</strong> üzerinden değerlendirebiliriz. Küçük bir değişiklik birçok servisin aynı anda güncellenmesini gerektiriyorsa sistem dağıtık olmasına rağmen sıkı bağlıdır.</p>

<h2 id="senkron-ve-asenkron-iletişim-bağımlılığı-nasıl-değiştirir">Senkron ve asenkron iletişim bağımlılığı nasıl değiştirir?</h2>

<p>Senkron iletişimde çağrıyı yapan servis karşı tarafın cevabını bekler. Yöntem basit ve anlaşılırdır; kullanıcıya anlık cevap verilmesi gereken kısa işlemler için uygun olabilir. Buna karşılık gecikme ve hata doğrudan çağrı zincirine yayılır.</p>

<p>Asenkron iletişimde servis bir olay yayımlar ve diğer servisler bunu daha sonra işler. Böylece servislerin aynı anda çalışması gerekmeyebilir. Bunun karşılığında mesaj sıralaması, tekrar işleme, idempotency, izlenebilirlik ve gecikmeli tutarlılık yönetilmelidir.</p>

<div class="mermaid">
flowchart LR
    subgraph S["Senkron akış"]
      O1["Sipariş"] --&gt;|"bekler"| P1["Ödeme"]
      P1 --&gt;|"bekler"| I1["Stok"]
    end
    subgraph A["Asenkron akış"]
      O2["Sipariş"] --&gt; B(("Olay aracısı"))
      B --&gt; P2["Ödeme"]
      B --&gt; I2["Stok"]
    end
</div>

<p>Asenkron iletişim bağımlılığı yok etmez. Mesajın güvenilir biçimde kabul edilip kalıcılaştırıldığı bir yapıda sipariş servisi, ödeme servisinin o anda erişilebilir olmasına bağımlı değildir; fakat olay sözleşmesine ve mesajlaşma altyapısına bağımlıdır. Yani bağımlılık zaman bakımından gevşer, sorumluluk ortadan kalkmaz.</p>

<h2 id="faydalı-bağımlılık-ile-zararlı-bağlılık">Faydalı bağımlılık ile zararlı bağlılık</h2>

<p>Sipariş servisinin ödeme işlemini bu alanda uzmanlaşmış bir ödeme servisine bırakması doğal ve faydalı olabilir. Zararlı olan, sipariş servisinin ödeme servisinin iç tablolarını, sınıflarını veya dağıtım takvimini bilmek zorunda kalmasıdır.</p>

<table>
  <thead>
    <tr>
      <th>Yönetilebilir bağımlılık</th>
      <th>Zararlı bağlılık</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Açık bir iş sorumluluğuna dayanır</td>
      <td>Sorumluluk sınırları belirsizdir</td>
    </tr>
    <tr>
      <td>Kararlı bir sözleşme üzerinden kurulur</td>
      <td>Diğer bileşenin iç yapısına dayanır</td>
    </tr>
    <tr>
      <td>Mümkün olduğunca tek yönlüdür</td>
      <td>Çift yönlü veya döngüseldir</td>
    </tr>
    <tr>
      <td>Hata davranışı tanımlıdır</td>
      <td>Hataları zincirleme biçimde yayar</td>
    </tr>
    <tr>
      <td>Bağımsız dağıtıma izin verir</td>
      <td>Servisleri birlikte yayımlamaya zorlar</td>
    </tr>
    <tr>
      <td>Gözlemlenebilir ve test edilebilirdir</td>
      <td>Etkisi ancak üretimde fark edilir</td>
    </tr>
  </tbody>
</table>

<p>Doğru kurulan bağımlılıklar sorumlulukların ayrılmasını, ekiplerin iş alanlarında uzmanlaşmasını ve bileşenlerin kendi yüklerine göre ölçeklenmesini sağlar. Kontrolsüz bağımlılıklar ise küçük değişiklikleri büyük koordinasyonlara dönüştürür, test ortamlarını zorlaştırır ve sistem performansını en yavaş bileşene bağlar.</p>

<p>Sonuçta mikroservislerden oluşan fakat birlikte geliştirilip birlikte yayımlanmak zorunda olan bir <strong>dağıtık monolit</strong> ortaya çıkar. Böyle bir sistem, monolitin sıkı bağlılık problemleriyle dağıtık sistemlerin ağ ve operasyon maliyetlerini aynı anda taşır.</p>

<h2 id="bağımlılıklar-nasıl-yönetilebilir">Bağımlılıklar nasıl yönetilebilir?</h2>

<h3 id="servis-sınırlarını-iş-yeteneklerine-göre-belirleyin">Servis sınırlarını iş yeteneklerine göre belirleyin</h3>

<p>Servisleri yalnızca teknik katmanlara göre bölmek yerine iş sorumluluklarına göre tasarlayın. Herkesin kullandığı genel amaçlı servisler zamanla merkezi darboğazlara dönüşebilir.</p>

<h3 id="her-verinin-bir-sahibi-olsun">Her verinin bir sahibi olsun</h3>

<p>Bir verinin otoritatif sahibi ve yazma kurallarından sorumlu servis açıkça belirlenmelidir. Diğer servisler bu veriye tanımlı API’ler veya olaylar üzerinden erişebilir, gerektiğinde kontrollü yerel kopyalarını tutabilir; ancak asıl kaynağın yazma kurallarını sahiplenmemelidir.</p>

<h3 id="sözleşmeleri-geriye-uyumlu-geliştirin">Sözleşmeleri geriye uyumlu geliştirin</h3>

<p>API ve olay şemaları servisler arasındaki sözleşmelerdir. Sözleşme testleri ve bilinçli sürümleme, bir sağlayıcıdaki değişikliğin tüketicileri beklenmedik biçimde bozmasını engeller.</p>

<h3 id="uzun-senkron-çağrı-zincirlerini-sorgulayın">Uzun senkron çağrı zincirlerini sorgulayın</h3>

<p>Tek bir kullanıcı isteğinin tamamlanması için çok sayıda servisin sırayla çağrılması hem gecikmeyi hem hata olasılığını artırır. Kullanıcının sonucu gerçekten aynı anda görmesi gerekmiyorsa işin bir bölümü asenkron yürütülebilir.</p>

<h3 id="hataları-tasarımın-doğal-parçası-kabul-edin">Hataları tasarımın doğal parçası kabul edin</h3>

<p>Timeout, sınırlı retry, circuit breaker, idempotency, kuyruklama ve uygun durumlarda alternatif cevap üretme gibi yöntemler bağımlılığın etkisini sınırlar. Özellikle kontrolsüz retry, zaten zorlanan bir servise daha fazla yük göndererek sorunu büyütebilir.</p>

<h3 id="döngüsel-bağımlılıkları-engelleyin">Döngüsel bağımlılıkları engelleyin</h3>

<p>A servisi B’ye, B de A’ya bağımlıysa sorumluluk sınırları veya iş akışının sahipliği yeniden değerlendirilmelidir. Döngü bazen yeni bir iş alanının henüz açıkça tanımlanmadığını gösterir.</p>

<h3 id="bağımlılıkları-görünür-kılın">Bağımlılıkları görünür kılın</h3>

<p>Servis haritaları, dağıtık izleme, metrikler ve ilişkilendirilmiş loglar hangi servisin hangisine bağımlı olduğunu göstermelidir. Dokümantasyonda görünmeyen bir ilişki, üretimde ortadan kaybolmaz.</p>

<h2 id="yeni-bir-bağımlılık-oluşturmadan-önce">Yeni bir bağımlılık oluşturmadan önce</h2>

<p>Bir servis çağrısı, ortak kütüphane veya olay sözleşmesi eklemeden önce şu sorular yararlı bir kontrol listesi oluşturur:</p>

<ol>
  <li>Bu ilişki gerçek bir iş zorunluluğu mu, yoksa mevcut tasarımın sonucu mu?</li>
  <li>Kullanıcı işlemin sonucunu anında almak zorunda mı?</li>
  <li>Çağrılan servis çalışmadığında ne olacak?</li>
  <li>Eski veya önbelleğe alınmış veriyle devam edilebilir mi?</li>
  <li>İşlem daha sonra tamamlanabilir mi?</li>
  <li>Sözleşmenin sahibi kim ve nasıl değiştirilecek?</li>
  <li>Servisler birbirinden bağımsız yayımlanabilecek mi?</li>
  <li>Bu ilişki başka bir servisin veritabanına erişmeyi gerektiriyor mu?</li>
  <li>Bağımlılık iki yönlü veya döngüsel hâle geliyor mu?</li>
  <li>Hata, gecikme ve değişiklik etkisini üretimde görebilecek miyiz?</li>
</ol>

<h2 id="sonuç">Sonuç</h2>

<p>Mikroservislerde hedef sıfır bağımlılık değildir; birlikte çalışan bir sistem için bu gerçekçi değildir. Hedef, bağımlılıkları açık, sınırlı, mümkün olduğunca tek yönlü ve değiştirilebilir hâle getirmektir.</p>

<p>Bir ilişkinin sağlıklı olup olmadığını anlamak için şu soruyla başlayabiliriz:</p>

<blockquote>
  <p>Bir serviste yapılan değişiklik, gecikme veya hata diğer servislerin kodunu, dağıtım zamanını ya da çalışma durumunu gereksiz yere etkiliyor mu?</p>
</blockquote>

<p>Cevap sürekli olarak “evet” ise ortada yalnızca doğal bir iş ilişkisi değil, sıkı bir teknik bağlılık vardır. İyi bir mimari bağımlılıkları gizlemez veya tamamen yok etmeye çalışmaz; onları görünür kılar, sınırlarını bilinçli biçimde çizer ve etkilerinin sistem boyunca kontrolsüz yayılmasını engeller.</p>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="mikroservis" /><category term="bağımlılık" /><category term="yazılım-mimarisi" /><category term="dağıtık-sistemler" /><category term="entegrasyon" /><summary type="html"><![CDATA[Mikroservislerde bağımlılığı yok etmek değil, değişiklik ve hata etkisini sınırlamak gerekir. Peki sağlıklı ilişkinin sınırı nerede çizilir?]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/microservice-dependency-boundaries.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/microservice-dependency-boundaries.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en-US"><title type="html">Designing a Dynamic Flow Engine: From Ready-Made Tools to a Custom Execution Model</title><link href="https://fmarslan.com/en/2026/08/01/designing-a-dynamic-flow-engine.html" rel="alternate" type="text/html" title="Designing a Dynamic Flow Engine: From Ready-Made Tools to a Custom Execution Model" /><published>2026-08-01T00:00:00+00:00</published><updated>2026-08-01T00:00:00+00:00</updated><id>https://fmarslan.com/en/2026/08/01/designing-a-dynamic-flow-engine</id><content type="html" xml:base="https://fmarslan.com/en/2026/08/01/designing-a-dynamic-flow-engine.html"><![CDATA[<p>A simple requirement to move data from one system to another can turn into a flow engine problem as transformation, validation, notification, and runtime configuration enter the picture. This article follows that change step by step and examines where ready-made tools end and a custom execution model begins.</p>

<h2 id="what-is-a-flow">What Is a Flow?</h2>

<p>Imagine that we are building a project whose purpose is to move data from System A to System B:</p>

<pre><code class="language-mermaid">flowchart LR
    A["System A"] --&gt; B["System B"]
</code></pre>

<p>At this stage, we are dealing mainly with a <strong>data integration</strong> or <strong>data pipeline</strong> problem. Tools such as Logstash, Azure Data Factory, and Apache NiFi can read data from different sources and move it to target systems. Nothing here requires us to build a custom flow engine.</p>

<p>In real projects, however, data is rarely moved without any intermediate work. Its format may need to be transformed, required fields must be validated, and invalid records have to be separated:</p>

<pre><code class="language-mermaid">flowchart LR
    A["Read data"] --&gt; T["Transform"]
    T --&gt; V["Validate"]
    V --&gt; D{"Valid?"}
    D --&gt;|Yes| B["Send to target"]
    D --&gt;|No| E["Separate invalid records"]
</code></pre>

<p>We are no longer describing only where the data comes from and where it goes. We are also defining the steps it passes through and the conditions that control its route. A flow is a structure that describes the steps of a process, the transitions between those steps, and the paths to follow for different outcomes.</p>

<h2 id="when-do-we-need-a-flow">When Do We Need a Flow?</h2>

<p>Not every sequence of operations needs a separate flow infrastructure. If the steps are few, stable, and changed only by developers, the process can remain in ordinary application code:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Read → Validate → Transform → Save
</code></pre></div></div>

<p>This is still a flow, but its definition is embedded in the codebase. Adding a step or changing the order requires a code change, testing, and another deployment. In this article, I will refer to this as a <strong>static flow</strong>.</p>

<p>A flow begins to emerge as a separate concern when the number of steps grows, conditional paths appear, failed work must be retried, or operators need to see the current stage of a process. The number of steps alone is not the deciding factor. A three-step process that waits two days for human approval may need stronger flow management than a short operation containing twenty consecutive method calls.</p>

<p>The need becomes clearer when we want to manage process state and failure paths independently of a single application call. Common signs include the following:</p>

<ul>
  <li>The process lasts longer than a single HTTP request or application process</li>
  <li>It waits for an external event, timer, or human decision</li>
  <li>It must continue from the failed step instead of starting again</li>
  <li>Operators need to see the current step of each instance</li>
  <li>The same process follows different paths for different customers or conditions</li>
</ul>

<p>None of these signs alone requires a custom engine. They help us distinguish between a flow that can stay in code, one that fits an existing execution tool, and one whose behavior has become a product concern.</p>

<h2 id="what-is-a-flow-engine">What Is a Flow Engine?</h2>

<p>A flow engine is the execution layer that decides which step of a defined flow should run and when. It reads the flow definition and determines the starting step, the next transition, and the completion condition. Depending on the requirements, it may also manage retries, timeouts, waits, parallel work, and execution state.</p>

<pre><code class="language-mermaid">flowchart LR
    D["Flow definition"] --&gt; E["Flow engine"]
    E --&gt; A["Step A"]
    A --&gt; B["Step B"]
    B --&gt; C["Step C"]
</code></pre>

<p>When we need to run many flows, recover after failure, or track long-running processes, collecting these responsibilities in an execution layer can be easier to reason about than distributing them throughout the application code.</p>

<h2 id="what-do-ready-made-flow-tools-do">What Do Ready-Made Flow Tools Do?</h2>

<p>Once a flow need appears, several options become available: code-based solutions, existing engines, managed services, and custom development. Mature tools already exist for different kinds of problems, but they do not all solve the same type of flow.</p>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>Main use</th>
      <th>Execution approach</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Logstash</td>
      <td>Log and event pipelines</td>
      <td>Input, filter, and output chain</td>
    </tr>
    <tr>
      <td>Azure Data Factory</td>
      <td>Data movement and ETL/ELT</td>
      <td>Managed pipelines and activities</td>
    </tr>
    <tr>
      <td>Apache NiFi</td>
      <td>Visual dataflow and system-to-system transfer</td>
      <td>Processors, connections, and queues</td>
    </tr>
    <tr>
      <td>Apache Airflow</td>
      <td>Scheduled data and batch orchestration</td>
      <td>Code-defined DAGs and tasks</td>
    </tr>
    <tr>
      <td>AWS Step Functions</td>
      <td>Application and service orchestration</td>
      <td>State machines and managed executions</td>
    </tr>
  </tbody>
</table>

<p>NiFi combines routing, transformation, queuing, back pressure, and data provenance for dataflows. I covered this model in more detail in <a href="/en/2025/08/05/data-flow-automation-with-apache-nifi.html">Data Flow Automation with Apache NiFi</a>.</p>

<p>Airflow represents work as tasks and dependencies in a DAG. Features such as schedules and backfills make it particularly useful for data pipelines. Step Functions uses state machines to run AWS services, Lambda functions, and external work within ordered and conditional application workflows.</p>

<p>These tools do more than connect boxes. They provide capabilities such as scheduling, retry handling, state tracking, execution history, and operational views, all of which are expensive to build and maintain. The question from <a href="/en/2026/01/07/should-i-write-everything-on-properly-leveraging-external-tools.html">Should I Write Everything Myself? On Using External Tools Properly</a> also applies here: does this infrastructure problem belong to our product, or has another team already spent years solving it?</p>

<h2 id="where-do-ready-made-tools-start-to-struggle">Where Do Ready-Made Tools Start to Struggle?</h2>

<p>Let us extend the example. The flow no longer transforms data and sends it directly to a destination:</p>

<pre><code class="language-mermaid">flowchart LR
    A["Receive data"] --&gt; C["Customer-specific operation"]
    C --&gt; P["Persist"]
    P --&gt; N["Send notification"]
    N --&gt; H["Wait for human approval"]
    H --&gt; B["Continue to next step"]
</code></pre>

<p>As nodes begin to contain product-specific behavior, the distance between our product model and the tool’s native model grows. NiFi, for example, includes processors that can run scripts, and compiled custom extensions can also be developed. However, placing core business rules in many scripts or tool-specific extensions can make testing, dependency management, CI/CD, and versioning harder than they are in a normal application codebase.</p>

<p>Airflow tasks can also execute custom code. Its main model, however, is closer to running workflows defined by developers than to serving as a general-purpose low-code product where end users add and remove nodes at runtime. If each new feature requires another adapter, plugin, or workaround, the tool may begin shaping the product model instead of reducing the problem.</p>

<p>There is also a reason why many flow tools use their own queue and state structures instead of, or alongside, Kafka and RabbitMQ. A message broker transports messages. A flow engine must additionally know which step is expected, which step has completed, how many retries have occurred, when a timeout expires, which flow version is running, and what the execution history contains. A broker does not provide all of this information by itself.</p>

<h2 id="when-does-a-custom-flow-engine-become-relevant">When Does a Custom Flow Engine Become Relevant?</h2>

<p>Now imagine that users can add or remove nodes through configuration or a visual designer:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Add node → Change connection → Define rule → Publish flow
</code></pre></div></div>

<p>The flow is no longer a fixed pipeline written in advance by a developer. It becomes product data interpreted at runtime. A combination of the following requirements can provide a strong reason to evaluate a custom engine:</p>

<ul>
  <li>Users create their own flows</li>
  <li>Nodes perform product-specific operations</li>
  <li>Flow definitions can change at runtime</li>
  <li>Tenants have different nodes, permissions, and limits</li>
  <li>Flow definitions require independent versioning</li>
  <li>Running executions must complete on the version with which they started</li>
  <li>The existing tool’s model must be bypassed for every new capability</li>
</ul>

<p>A custom user experience does not necessarily require a custom runtime. We can build our own designer and flow definition while using Step Functions or another engine behind it. A custom engine becomes a real option when both the flow language and its execution behavior become core product capabilities.</p>

<p>That is the requirement examined in this article: running a <strong>dynamic flow</strong> in which nodes can be added or removed and connections can be changed through configuration.</p>

<h2 id="core-concepts-of-a-dynamic-flow">Core Concepts of a Dynamic Flow</h2>

<p>Before discussing execution models, we need to separate a few concepts:</p>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Flow definition</td>
      <td>The model describing nodes and transitions</td>
    </tr>
    <tr>
      <td>Flow version</td>
      <td>A published, immutable version of a flow</td>
    </tr>
    <tr>
      <td>Flow instance</td>
      <td>One execution of a flow version</td>
    </tr>
    <tr>
      <td>Step execution</td>
      <td>One attempt to run a specific node</td>
    </tr>
    <tr>
      <td>Execution context</td>
      <td>Data and references carried between steps</td>
    </tr>
    <tr>
      <td>Transition</td>
      <td>Movement from a step result to the next step</td>
    </tr>
  </tbody>
</table>

<p>A simplified definition may look like this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"flowId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customer-registration"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
  </span><span class="nl">"startAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"validate"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"nodes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"validate"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"next"</span><span class="p">:</span><span class="w"> </span><span class="s2">"persist"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"persist"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"next"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notify"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"notify"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"end"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>A published flow definition should be treated as immutable. When a user changes it, the platform creates a new version instead of editing the existing one. Running instances can then continue with the version on which they started, while new instances use the latest version.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Flow v1 → Instance A, Instance B
Flow v2 → Instance C
Flow v3 → New instances
</code></pre></div></div>

<h2 id="what-guarantees-should-flow-execution-provide">What Guarantees Should Flow Execution Provide?</h2>

<p>Before choosing a message bus or database, we need to describe the engine’s behavior:</p>

<ul>
  <li>Can the same step run more than once?</li>
  <li>What happens when a message is lost or redelivered?</li>
  <li>Is ordering required across the whole system or only within one flow instance?</li>
  <li>What happens if an external call succeeds but the process stops before state is saved?</li>
  <li>Is the context carried in the message, or does the message contain only a reference?</li>
  <li>What happens if a retried step produces the same side effect again?</li>
</ul>

<p>Choosing a broker does not answer these questions automatically. Kafka, for example, maintains record order within a partition, not across an entire topic. Using <code class="language-plaintext highlighter-rouge">flowInstanceId</code> as the partition key can place related events in the same partition, but parallel consumer work, retries, and external-service latency can still change the order in which operations complete.</p>

<p>With RabbitMQ, a single queue and a single consumer can provide strong ordering, but this limits throughput. Once multiple consumers, redelivery, and requeue behavior enter the system, delivery order and completion order can diverge.</p>

<p>Ordering and delivery guarantees therefore tend to be designed together with mechanisms such as:</p>

<ul>
  <li>Sequence numbers scoped to a flow instance</li>
  <li>Expected-step validation</li>
  <li>Idempotency keys</li>
  <li>Optimistic concurrency</li>
  <li>Duplicate-event detection</li>
  <li>Outbox and inbox patterns</li>
</ul>

<h2 id="approach-1-central-orchestrator">Approach 1: Central Orchestrator</h2>

<p>In the first model, a central orchestrator makes all transition decisions:</p>

<pre><code class="language-mermaid">sequenceDiagram
    participant O as Orchestrator
    participant A as Step A Worker
    participant B as Step B Worker
    participant S as Execution Store

    O-&gt;&gt;A: Run Step A
    A--&gt;&gt;O: Step A completed
    O-&gt;&gt;S: Update state
    O-&gt;&gt;B: Run Step B
    B--&gt;&gt;O: Step B completed
    O-&gt;&gt;S: Complete flow
</code></pre>

<p>The orchestrator reads the flow definition, identifies the current step, sends work to the relevant worker, receives the result, updates state, and schedules the next step. Retry, timeout, notification, and authorization rules can be applied here as shared policies.</p>

<p>The main benefit is that the full flow remains visible through one execution model. Versioning, waits, parallel joins, and operational views can be handled in a controlled way. The trade-off is that every transition passes through the same logical component, so the orchestrator’s performance and availability become important.</p>

<p>A central orchestrator does not have to be a single process or pod. If execution state is stored in a shared durable store, the orchestrator can remain stateless and run as several replicas on Kubernetes. If one replica stops, another can continue scheduling. Centralization here means that transition decisions share one authority, not that only one physical instance exists.</p>

<h2 id="approach-2-choreography-based-distributed-flow">Approach 2: Choreography-Based Distributed Flow</h2>

<p>In the second model, a central component does not control every transition. A step publishes an event when it finishes, and the next step runs by listening for that event:</p>

<pre><code class="language-mermaid">flowchart LR
    S["FlowStarted"] --&gt; A["Step A"]
    A --&gt; EA["StepACompleted"]
    EA --&gt; B["Step B"]
    B --&gt; EB["StepBCompleted"]
    EB --&gt; C["Step C"]
    C --&gt; F["FlowCompleted"]
</code></pre>

<p>Operational functions such as audit, metrics, and notifications can listen to the same events independently:</p>

<pre><code class="language-mermaid">flowchart LR
    E["StepCompleted"] --&gt; N["Next step"]
    E --&gt; A["Audit function"]
    E --&gt; M["Metric function"]
    E --&gt; T["Notification function"]
</code></pre>

<p>This model allows steps to scale independently and new listeners to be added without changing the main flow. If one worker stops, other flows on the platform can continue running. The affected flow instance, however, cannot progress until the message is redelivered or the worker becomes available again.</p>

<p>As the central execution load decreases, coordination responsibilities become distributed. Selecting the next step, updating context, detecting duplicate events, handling retries, joining parallel branches, and respecting the flow version all become more difficult. Correlation, tracing, and replay are no longer optional operational features; they become part of the execution model. I explored the operational side of this problem in <a href="/en/2025/11/29/why-is-debug-not-enough-in-event-driven-architecture-on-tracing-replay-and-true-observability.html">Why Is Debugging Not Enough in Event-Driven Architecture?</a>.</p>

<h2 id="where-should-execution-state-be-stored">Where Should Execution State Be Stored?</h2>

<p>Carrying the entire flow context only inside Kafka or RabbitMQ messages may look attractive, but it creates problems around large payloads, sensitive data, queries, and historical access. The message can carry identity and transition information while the main state remains in a durable store:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"flowInstanceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"flow-123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"stepId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"validate"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"executionId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"exec-456"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sequence"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The execution store can keep information such as:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FlowInstance
- id
- definitionId
- definitionVersion
- currentStep
- status
- contextReference
- sequence
- createdAt
- updatedAt
</code></pre></div></div>

<p>A relational database can provide transactions and queryability, Redis can provide fast temporary state and locking, Kafka can serve as a replayable event log, and RabbitMQ can deliver tasks. Large payloads can remain in object storage while the flow context carries references. A real system does not have to force one of these tools to perform every role.</p>

<h2 id="a-hybrid-implementation-option">A Hybrid Implementation Option</h2>

<p>Central orchestration and distributed step execution can also be used together. In this model, a central component interprets the flow definition and version, while independent workers or services perform the actual work:</p>

<pre><code class="language-mermaid">flowchart TB
    D["Flow definition"] --&gt; O["Stateless orchestrator"]
    O --&gt; B["Message bus"]
    B --&gt; F1["Step worker A"]
    B --&gt; F2["Step worker B"]
    B --&gt; F3["Step worker C"]
    O &lt;--&gt; S["Execution store"]
    F1 --&gt; E["Flow events"]
    F2 --&gt; E
    F3 --&gt; E
    E --&gt; A["Audit / metric / notification"]
</code></pre>

<p>Flow decisions are centralized while work execution remains distributed. The orchestrator can scale as a stateless component, workers can grow independently according to their load profiles, and operational tasks can consume flow events. In return, the coordination cost of the central model and the messaging and observability costs of the distributed model must be managed in the same system.</p>

<p>This model is not free either. Once we build a custom engine, the team becomes responsible for retries, timeouts, idempotency, versioning, migrations, security, tenant isolation, and operational tooling. The long-term cost of these responsibilities must be considered alongside the cost of forcing an existing tool beyond its natural model.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Keeping a flow in code, executing it with an existing tool, or turning it into a custom engine are different responses to the same problem at different levels of scale and variability. The relevant factors are not limited to the number of nodes. They also include how dynamic the process is, how state is preserved, who changes the flow, and how important execution behavior is to the product itself.</p>

<p>With a custom model, every node does not have to use the same technology. As long as it follows a shared execution contract, a node can be a FaaS function, a service running in a container, an external SaaS integration, or a traditional application. The engine then depends on the node’s input, output, identity, and execution result rather than its programming language or deployment model. In cloud-native environments, this makes it possible to combine work with different sizes and load profiles in the same flow.</p>

<p>Each option carries a different cost. A ready-made tool may constrain the product model, while a custom engine leaves state, ordering, retries, idempotency, versioning, and operations with the product team. The purpose is therefore not to prescribe one correct approach, but to make visible the point at which changing requirements begin to call for a different execution model.</p>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><a href="https://nifi.apache.org/nifi-docs/overview.html">Apache NiFi — Overview</a></li>
  <li><a href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html">Apache Airflow — DAG concepts</a></li>
  <li><a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html">What is AWS Step Functions?</a></li>
  <li><a href="https://kafka.apache.org/design/">Apache Kafka — Design: ordering and partitioning</a></li>
</ul>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="workflow" /><category term="orchestration" /><category term="event-driven" /><category term="faas" /><category term="cloud-native" /><summary type="html"><![CDATA[How a data pipeline becomes a flow, where ready-made tools remain useful, and when a dynamic flow engine becomes a reasonable design option.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/dynamic-flow-engine-cover.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/dynamic-flow-engine-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="tr-TR"><title type="html">Dinamik Bir Flow Engine Tasarlamak: Hazır Araçtan Özel Yürütme Modeline</title><link href="https://fmarslan.com/tr/2026/08/01/dinamik-bir-flow-engine-tasarlamak.html" rel="alternate" type="text/html" title="Dinamik Bir Flow Engine Tasarlamak: Hazır Araçtan Özel Yürütme Modeline" /><published>2026-08-01T00:00:00+00:00</published><updated>2026-08-01T00:00:00+00:00</updated><id>https://fmarslan.com/tr/2026/08/01/designing-a-dynamic-flow-engine</id><content type="html" xml:base="https://fmarslan.com/tr/2026/08/01/dinamik-bir-flow-engine-tasarlamak.html"><![CDATA[<p>Bir noktadaki veriyi başka bir noktaya taşımakla başlayan basit bir ihtiyaç, dönüşüm, doğrulama, bildirim ve çalışma anında değişen kurallar eklendikçe bir flow engine problemine dönüşebilir. Bu yazıda bu dönüşümün nerede başladığını ve hazır bir araçtan özel bir yürütme modeline ne zaman geçilmesi gerektiğini aynı örnek üzerinden inceleyeceğim.</p>

<h2 id="flow-nedir">Flow Nedir?</h2>

<p>Bir proje geliştirdiğimizi ve amacımızın A sistemindeki veriyi B sistemine taşımak olduğunu düşünelim:</p>

<pre><code class="language-mermaid">flowchart LR
    A["Sistem A"] --&gt; B["Sistem B"]
</code></pre>

<p>Bu haliyle karşımızda daha çok bir <strong>veri entegrasyonu</strong> veya <strong>data pipeline</strong> problemi vardır. Logstash, Azure Data Factory ve Apache NiFi gibi araçlar farklı kaynaklardan veri okuyup hedef sistemlere taşıyabilir. Henüz özel bir flow engine geliştirmemizi gerektiren bir durum yoktur.</p>

<p>Gerçek projelerde veri çoğunlukla doğrudan taşınmaz. Önce formatı dönüştürülür, zorunlu alanları doğrulanır ve hatalı kayıtlar ayrılır:</p>

<pre><code class="language-mermaid">flowchart LR
    A["Veriyi oku"] --&gt; T["Dönüştür"]
    T --&gt; V["Doğrula"]
    V --&gt; D{"Geçerli mi?"}
    D --&gt;|Evet| B["Hedefe gönder"]
    D --&gt;|Hayır| E["Hatalı kayıtları ayır"]
</code></pre>

<p>Artık yalnızca verinin nereden nereye gittiğini değil, hangi adımlardan ve hangi koşullardan geçeceğini de tarif ediyoruz. Flow, bir işin adımlarını, bu adımlar arasındaki geçişleri ve farklı sonuçlarda izlenecek yolları tanımlayan yapıdır.</p>

<h2 id="flowa-ne-zaman-i̇htiyaç-duyulur">Flow’a Ne Zaman İhtiyaç Duyulur?</h2>

<p>Her sıralı işlem ayrı bir flow altyapısı gerektirmez. Adımlar az, sabit ve yalnızca geliştiriciler tarafından değiştiriliyorsa süreç normal uygulama kodu içinde yönetilebilir:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Oku → Doğrula → Dönüştür → Kaydet
</code></pre></div></div>

<p>Bu da bir flow’dur; ancak tanımı kod tabanının içindedir. Adım eklemek veya sırasını değiştirmek için kodu güncellemek, test etmek ve yeniden yayımlamak gerekir. Bu yazıda bunu <strong>statik flow</strong> olarak adlandıracağım.</p>

<p>Adımlar arttığında, koşullu yollar oluştuğunda, hata alan bir işin yeniden denenmesi veya sürecin hangi aşamada olduğunun izlenmesi gerektiğinde flow ayrı bir kavram haline gelmeye başlar. Buradaki ölçüt yalnızca adım sayısı değildir. Üç adımdan oluşan fakat iki gün insan onayı bekleyen bir süreç, yirmi metodun arka arkaya çağrıldığı kısa bir işlemden daha güçlü bir flow yönetimine ihtiyaç duyabilir.</p>

<p>Flow’a asıl ihtiyaç, sürecin ilerleme durumunu ve hata yollarını uygulama çağrısından bağımsız olarak yönetmek istediğimizde ortaya çıkar. Aşağıdaki belirtiler bu ihtiyacı görünür hale getirir:</p>

<ul>
  <li>Süreç tek bir HTTP isteğinin veya uygulama prosesinin yaşam süresini aşıyorsa</li>
  <li>Bir dış olay, zamanlayıcı veya insan kararı bekleniyorsa</li>
  <li>Hata sonrasında sürecin baştan değil kaldığı adımdan devam etmesi gerekiyorsa</li>
  <li>Operasyon ekibinin her instance’ın hangi adımda olduğunu görmesi isteniyorsa</li>
  <li>Aynı sürecin farklı müşteriler veya koşullar için farklı yollar izlemesi gerekiyorsa</li>
</ul>

<p>Bu ihtiyaçların hiçbiri tek başına özel bir engine geliştirmeyi zorunlu kılmaz. Önce flow’un kod içinde kalıp kalamayacağına, ardından hazır bir yürütme aracının süreci karşılayıp karşılamadığına bakılır.</p>

<h2 id="flow-engine-nedir">Flow Engine Nedir?</h2>

<p>Flow engine, tanımlanan akışın hangi adımının ne zaman çalışacağını yöneten yürütme katmanıdır. Flow tanımını okur; başlangıç adımını, sıradaki geçişi ve tamamlanma koşulunu belirler. İhtiyaca göre retry, timeout, bekleme, paralel çalışma ve execution state gibi davranışları da yönetir.</p>

<pre><code class="language-mermaid">flowchart LR
    D["Flow tanımı"] --&gt; E["Flow engine"]
    E --&gt; A["Step A"]
    A --&gt; B["Step B"]
    B --&gt; C["Step C"]
</code></pre>

<p>Birden fazla flow çalıştırmak, hatadan sonra kaldığı yerden devam etmek veya uzun süre bekleyen süreçleri takip etmek istiyorsak bu sorumlulukları uygulama kodu boyunca dağıtmak yerine bir yürütme katmanında toplamak anlamlı hale gelir.</p>

<h2 id="hazır-flow-araçları-ne-yapar">Hazır Flow Araçları Ne Yapar?</h2>

<p>Flow ihtiyacı ortaya çıktığında önümüzde kod tabanlı çözümler, hazır engine’ler ve özel geliştirme gibi farklı seçenekler bulunur. Farklı problem türleri için yıllardır kullanılan olgun araçlar vardır; ancak hepsi aynı tür akışı çözmez.</p>

<table>
  <thead>
    <tr>
      <th>Araç</th>
      <th>Temel kullanım alanı</th>
      <th>Çalışma yaklaşımı</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Logstash</td>
      <td>Log ve event pipeline’ları</td>
      <td>Input, filter ve output zinciri</td>
    </tr>
    <tr>
      <td>Azure Data Factory</td>
      <td>Veri taşıma ve ETL/ELT</td>
      <td>Yönetilen pipeline ve activity’ler</td>
    </tr>
    <tr>
      <td>Apache NiFi</td>
      <td>Görsel veri akışı ve sistemler arası aktarım</td>
      <td>Processor, connection ve kuyruklar</td>
    </tr>
    <tr>
      <td>Apache Airflow</td>
      <td>Zamanlanan veri ve batch işlerinin orkestrasyonu</td>
      <td>Kodla tanımlanan DAG ve task’lar</td>
    </tr>
    <tr>
      <td>AWS Step Functions</td>
      <td>Uygulama ve servis orkestrasyonu</td>
      <td>State machine ve yönetilen execution</td>
    </tr>
  </tbody>
</table>

<p>NiFi; routing, transformation, kuyruklama, back-pressure ve data provenance gibi veri akışı ihtiyaçlarını birlikte sunar. Bu yönünü <a href="/apache/nifi/2025/08/05/apache-nifi.html">Apache NiFi ile Veri Akışı Otomasyonu</a> yazısında daha ayrıntılı incelemiştim.</p>

<p>Airflow’da işler bir DAG içindeki task ve bağımlılıklar olarak tanımlanır. Schedule ve backfill gibi yetenekleri nedeniyle özellikle veri pipeline’larında güçlüdür. Step Functions ise state machine yaklaşımıyla AWS servislerini, Lambda fonksiyonlarını ve harici işleri belirli bir sıra ve karar yapısı içinde çalıştırır.</p>

<p>Bu araçlar yalnızca kutuları birbirine bağlamaz. Scheduling, retry, state takibi, execution history ve operasyon ekranı gibi geliştirmesi ve işletmesi pahalı yetenekleri de sağlar. Bu nedenle daha önce <a href="/2026/01/07/harici-araclardan-dogru-sekilde-faydalanma.html">Harici Araçlardan Doğru Şekilde Faydalanma</a> yazısında ele aldığım soru burada da geçerlidir: Bu problem gerçekten bizim ürünümüze mi ait, yoksa yıllardır başka ekiplerin çözdüğü bir altyapı problemi mi?</p>

<h2 id="hazır-araçlar-nerede-zorlanmaya-başlar">Hazır Araçlar Nerede Zorlanmaya Başlar?</h2>

<p>Örneğimizi biraz daha büyütelim. Flow artık yalnızca veriyi dönüştürüp hedef sisteme göndermiyor:</p>

<pre><code class="language-mermaid">flowchart LR
    A["Veriyi al"] --&gt; C["Müşteriye özel işlem"]
    C --&gt; P["Kaydet"]
    P --&gt; N["Bildirim gönder"]
    N --&gt; H["İnsan onayı bekle"]
    H --&gt; B["Sonraki adıma geç"]
</code></pre>

<p>Node’ların içine ürüne özgü davranışlar girmeye başladığında hazır aracın doğal çalışma modeliyle aramızdaki mesafe artar. Örneğin NiFi içinde script çalıştıran processor’lar vardır. Gerektiğinde derlenmiş özel extension’lar da geliştirilebilir. Ancak temel iş kurallarını çok sayıda script veya araca özel extension içine yerleştirmek; test, bağımlılık yönetimi, CI/CD ve versiyonlama süreçlerini normal bir uygulama kod tabanına göre zorlaştırabilir.</p>

<p>Airflow task’ları içinde de özel kod çalıştırabiliriz. Fakat Airflow’un ana çalışma modeli, son kullanıcıların runtime sırasında node ekleyip çıkardığı genel amaçlı bir low-code ürün olmaktan çok, geliştiriciler tarafından tanımlanan workflow’ların yürütülmesine yakındır. Hazır bir aracı kullanmak için sürekli adapter, plugin ve workaround geliştiriyorsak araç artık problemi azaltmak yerine ürün modelimizi şekillendiriyor olabilir.</p>

<p>Birçok flow aracının Kafka veya RabbitMQ yerine ya da bunların yanında kendi queue ve state yapılarını kullanmasının da bir nedeni vardır. Bir message broker mesajı taşır; fakat bir flow engine ayrıca hangi adımın beklendiğini, hangisinin tamamlandığını, retry sayısını, timeout’u, flow versiyonunu ve execution geçmişini bilmek zorundadır. Broker bu bilgilerin tamamını tek başına sağlamaz.</p>

<h2 id="özel-flow-engine-ne-zaman-gündeme-gelir">Özel Flow Engine Ne Zaman Gündeme Gelir?</h2>

<p>Kullanıcıların bir konfigürasyon veya görsel tasarım ekranı üzerinden node ekleyip çıkarabildiğini düşünelim:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node ekle → Bağlantıyı değiştir → Kural tanımla → Flow'u yayımla
</code></pre></div></div>

<p>Bu noktada flow, geliştiricinin önceden kodladığı sabit bir pipeline olmaktan çıkar ve ürünün çalışma anında yorumladığı bir veriye dönüşür. Aşağıdaki ihtiyaçların birkaçının birlikte bulunması özel bir flow engine için güçlü bir gerekçe oluşturur:</p>

<ul>
  <li>Kullanıcılar kendi flow’larını oluşturuyorsa</li>
  <li>Node’lar ürüne özgü işler gerçekleştiriyorsa</li>
  <li>Akış tanımı runtime sırasında değişebiliyorsa</li>
  <li>Tenant’a göre farklı node, yetki ve limitler bulunuyorsa</li>
  <li>Flow tanımı bağımsız olarak versiyonlanacaksa</li>
  <li>Devam eden execution’ların başladıkları versiyonla tamamlanması gerekiyorsa</li>
  <li>Hazır aracın modeli her yeni özellikte aşılmaya çalışılıyorsa</li>
</ul>

<p>Özel bir kullanıcı deneyimine ihtiyaç duymak, mutlaka özel bir runtime geliştirmeyi gerektirmez. Kendi flow tanımımızı ve designer’ımızı oluşturup arka tarafta Step Functions veya başka bir engine kullanabiliriz. Ancak hem flow dili hem de yürütme davranışı ürünün temel yeteneğine dönüşmüşse custom engine gerçek bir seçenek haline gelir.</p>

<p>Bu yazıdaki ihtiyacımız da budur: Node’ların eklenebildiği, çıkarılabildiği ve bağlantıların konfigürasyon üzerinden değiştirilebildiği <strong>dinamik bir flow</strong> çalıştırmak.</p>

<h2 id="dinamik-flowun-temel-kavramları">Dinamik Flow’un Temel Kavramları</h2>

<p>Execution modelini seçmeden önce birkaç kavramı ayırmamız gerekir:</p>

<table>
  <thead>
    <tr>
      <th>Kavram</th>
      <th>Anlamı</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Flow definition</td>
      <td>Node’ları ve geçişleri tanımlayan model</td>
    </tr>
    <tr>
      <td>Flow version</td>
      <td>Yayımlanmış, değişmez flow sürümü</td>
    </tr>
    <tr>
      <td>Flow instance</td>
      <td>Bir flow sürümünün tekil çalışması</td>
    </tr>
    <tr>
      <td>Step execution</td>
      <td>Belirli bir node’un tek çalışma denemesi</td>
    </tr>
    <tr>
      <td>Execution context</td>
      <td>Adımlar arasında taşınan veri ve referanslar</td>
    </tr>
    <tr>
      <td>Transition</td>
      <td>Bir step sonucundan sonraki step’e geçiş</td>
    </tr>
  </tbody>
</table>

<p>Basitleştirilmiş bir tanım şöyle olabilir:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"flowId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customer-registration"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
  </span><span class="nl">"startAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"validate"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"nodes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"validate"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"next"</span><span class="p">:</span><span class="w"> </span><span class="s2">"persist"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"persist"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"next"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notify"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"notify"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"end"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Yayımlanmış flow tanımı değişmez kabul edilmelidir. Kullanıcı değişiklik yaptığında mevcut tanımı güncellemek yerine yeni bir versiyon üretilir. Böylece çalışan instance’lar başladıkları sürümle devam ederken yeni instance’lar son sürümü kullanabilir.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Flow v1 → Instance A, Instance B
Flow v2 → Instance C
Flow v3 → Yeni instance'lar
</code></pre></div></div>

<h2 id="flow-execution-hangi-garantileri-vermeli">Flow Execution Hangi Garantileri Vermeli?</h2>

<p>Message bus veya veritabanı seçmeden önce engine’in davranışını tanımlamalıyız:</p>

<ul>
  <li>Aynı step birden fazla kez çalışabilir mi?</li>
  <li>Mesaj kaybolduğunda veya yeniden teslim edildiğinde ne olacak?</li>
  <li>Sıralama bütün sistemde mi, yalnızca aynı flow instance içinde mi gerekli?</li>
  <li>Başarılı bir dış çağrıdan sonra state kaydedilemeden proses kapanırsa ne yapılacak?</li>
  <li>Context mesajla mı taşınacak, yoksa yalnızca bir referans mı gönderilecek?</li>
  <li>Retry edilen bir step yan etkiyi ikinci kez üretirse sonuç ne olacak?</li>
</ul>

<p>Bir broker seçmek bu soruların kendiliğinden cevaplanmasını sağlamaz. Örneğin Kafka kayıt sırasını topic genelinde değil partition içinde korur. Aynı <code class="language-plaintext highlighter-rouge">flowInstanceId</code> değerini partition key olarak kullanmak ilgili event’lerin aynı partition’a gitmesini sağlayabilir; fakat paralel consumer işlemleri, retry veya dış servis gecikmeleri sonuçların aynı sırada tamamlanacağını garanti etmez.</p>

<p>RabbitMQ’da tek queue ve tek consumer güçlü bir sıra oluşturabilir, ancak throughput’u sınırlar. Birden fazla consumer, redelivery ve requeue devreye girdiğinde teslim sırası ile işlemlerin tamamlanma sırası birbirinden ayrılabilir.</p>

<p>Bu nedenle ordering ve teslimat garantileri genellikle şu mekanizmalarla birlikte tasarlanır:</p>

<ul>
  <li>Flow instance bazlı sequence number</li>
  <li>Beklenen step kontrolü</li>
  <li>Idempotency key</li>
  <li>Optimistic concurrency</li>
  <li>Duplicate event kontrolü</li>
  <li>Outbox ve inbox pattern’leri</li>
</ul>

<h2 id="yaklaşım-1-merkezi-orchestrator">Yaklaşım 1: Merkezi Orchestrator</h2>

<p>İlk yaklaşımda bütün geçiş kararlarını merkezi bir orchestrator verir:</p>

<pre><code class="language-mermaid">sequenceDiagram
    participant O as Orchestrator
    participant A as Step A Worker
    participant B as Step B Worker
    participant S as Execution Store

    O-&gt;&gt;A: Step A'yı çalıştır
    A--&gt;&gt;O: Step A tamamlandı
    O-&gt;&gt;S: State'i güncelle
    O-&gt;&gt;B: Step B'yi çalıştır
    B--&gt;&gt;O: Step B tamamlandı
    O-&gt;&gt;S: Flow'u tamamla
</code></pre>

<p>Orchestrator flow tanımını okur, mevcut step’i belirler, işi ilgili worker’a gönderir, sonucu alır ve state’i güncelledikten sonra sıradaki step’i planlar. Retry, timeout, bildirim ve yetki kontrolleri ortak politikalar olarak burada uygulanabilir.</p>

<p>Bu yaklaşımın güçlü tarafı, flow’un tamamını tek bir execution modeli üzerinden görebilmemizdir. Versiyonlama, bekleme, paralel kolların birleşmesi ve operasyon ekranları daha kontrollü tasarlanabilir. Buna karşılık bütün geçişler aynı mantıksal bileşenden geçtiği için orchestrator’ın performansı ve erişilebilirliği önem kazanır.</p>

<p>Merkezi orchestrator, tek bir proses veya tek bir pod olmak zorunda değildir. Execution state ortak ve kalıcı bir store’da tutulursa orchestrator stateless tasarlanıp Kubernetes üzerinde birden fazla replica ile çalıştırılabilir. Bir replica kapandığında diğerleri planlamaya devam edebilir. Buradaki merkezilik fiziksel olarak tek instance bulunması değil, geçiş kararlarının aynı otorite tarafından verilmesidir.</p>

<h2 id="yaklaşım-2-choreography-tabanlı-dağıtık-akış">Yaklaşım 2: Choreography Tabanlı Dağıtık Akış</h2>

<p>İkinci yaklaşımda merkezi bir bileşen her geçişi yönetmez. Bir step işini tamamladığında event yayımlar; sıradaki step bu event’i dinleyerek çalışır:</p>

<pre><code class="language-mermaid">flowchart LR
    S["FlowStarted"] --&gt; A["Step A"]
    A --&gt; EA["StepACompleted"]
    EA --&gt; B["Step B"]
    B --&gt; EB["StepBCompleted"]
    EB --&gt; C["Step C"]
    C --&gt; F["FlowCompleted"]
</code></pre>

<p>Audit, metric ve notification gibi operasyonel fonksiyonlar da aynı event’leri bağımsız olarak dinleyebilir:</p>

<pre><code class="language-mermaid">flowchart LR
    E["StepCompleted"] --&gt; N["Next step"]
    E --&gt; A["Audit function"]
    E --&gt; M["Metric function"]
    E --&gt; T["Notification function"]
</code></pre>

<p>Bu model step’lerin bağımsız ölçeklenmesine ve yeni dinleyicilerin ana flow değiştirilmeden eklenmesine izin verir. Bir worker’ın kapanması platformdaki diğer flow’ların çalışmasını durdurmaz. Ancak ilgili flow instance, mesaj yeniden teslim edilene veya worker yeniden erişilebilir olana kadar ilerleyemez.</p>

<p>Merkezi execution yükü azalırken koordinasyon sorumluluğu dağıtılır. Sıradaki step’in belirlenmesi, context’in güncellenmesi, duplicate event’ler, retry, paralel join ve flow versiyonu daha zor hale gelir. Akışın tamamını görebilmek için correlation, tracing ve replay artık yardımcı özellik değil temel ihtiyaçtır. Bu problemin operasyon tarafını <a href="/2025/11/29/event-driven-mimaride-debug-neden-yetmez.html">Event-Driven Mimaride Debug Neden Yetmez?</a> yazısında ele almıştım.</p>

<h2 id="execution-state-nerede-tutulmalı">Execution State Nerede Tutulmalı?</h2>

<p>Flow context’inin tamamını yalnızca Kafka veya RabbitMQ mesajlarında taşımak cazip görünür; fakat büyük payload, hassas veri, sorgulama ve geçmişe erişim gibi sorunlar ortaya çıkar. Mesaj çoğu zaman kimlik ve geçiş bilgisini taşırken asıl state kalıcı bir store’da tutulabilir:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"flowInstanceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"flow-123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"stepId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"validate"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"executionId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"exec-456"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sequence"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Execution store ise aşağıdaki türde bilgileri saklar:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FlowInstance
- id
- definitionId
- definitionVersion
- currentStep
- status
- contextReference
- sequence
- createdAt
- updatedAt
</code></pre></div></div>

<p>İlişkisel veritabanı transaction ve sorgulanabilirlik, Redis hızlı geçici state ve lock, Kafka replay edilebilir event log, RabbitMQ ise task delivery için kullanılabilir. Büyük payload’lar object storage üzerinde tutulup flow context içinde yalnızca referansları taşınabilir. Gerçek bir sistemde bu araçlardan biri diğerlerinin bütün görevlerini üstlenmek zorunda değildir.</p>

<h2 id="hibrit-bir-uygulama-seçeneği">Hibrit Bir Uygulama Seçeneği</h2>

<p>Merkezi orchestration ile dağıtık step execution birlikte de kullanılabilir. Bu modelde flow tanımı ve versiyonu merkezi bir bileşen tarafından yorumlanırken gerçek işler bağımsız worker veya servislere bırakılır:</p>

<pre><code class="language-mermaid">flowchart TB
    D["Flow definition"] --&gt; O["Stateless orchestrator"]
    O --&gt; B["Message bus"]
    B --&gt; F1["Step worker A"]
    B --&gt; F2["Step worker B"]
    B --&gt; F3["Step worker C"]
    O &lt;--&gt; S["Execution store"]
    F1 --&gt; E["Flow events"]
    F2 --&gt; E
    F3 --&gt; E
    E --&gt; A["Audit / metric / notification"]
</code></pre>

<p>Bu hibrit modelde flow kararları merkezi, işin yürütülmesi dağıtıktır. Orchestrator stateless ölçeklenebilir; worker’lar kendi yük profillerine göre bağımsız büyüyebilir; audit ve notification gibi operasyonel işler event’leri dinleyebilir. Bunun karşılığında merkezi modelin koordinasyon yükü ile dağıtık modelin mesajlaşma ve gözlemlenebilirlik maliyeti aynı sistemde birlikte yönetilir.</p>

<p>Yine de bu model bedelsiz değildir. Özel engine geliştirdiğimiz anda retry, timeout, idempotency, versiyonlama, migration, security, tenant izolasyonu ve operasyon ekranlarının sahibi de oluruz. Hazır aracı zorlamanın maliyeti ile bu sorumlulukların uzun vadeli maliyeti birlikte değerlendirilmelidir.</p>

<h2 id="sonuç">Sonuç</h2>

<p>Flow’un kod içinde kalması, hazır bir araçla yürütülmesi veya özel bir engine’e dönüştürülmesi aynı problemin farklı ölçek ve değişkenlik seviyelerindeki karşılıklarıdır. Seçimi belirleyen yalnızca node sayısı değil; sürecin ne kadar dinamik olduğu, state’in nasıl korunacağı, flow’u kimin değiştireceği ve execution davranışının ürünün ne kadar önemli bir parçası olduğudur.</p>

<p>Özel bir model kullanıldığında node’ların tamamı aynı teknolojiyle geliştirilmek zorunda değildir. Ortak execution sözleşmesine uyduğu sürece bir node FaaS fonksiyonu, container içinde çalışan servis, harici bir SaaS entegrasyonu veya klasik bir uygulama olabilir. Böylece flow engine, node’un programlama diline veya dağıtım biçimine değil; girdisine, çıktısına, kimliğine ve çalışma sonucuna bağlı kalır. Bu esneklik özellikle cloud-native ortamlarda farklı boyut ve yük profiline sahip işleri aynı flow içinde bir araya getirmeyi mümkün kılar.</p>

<p>Bu seçeneklerin her biri farklı bir maliyet taşır. Hazır araç ürün modelini sınırlandırabilir; özel engine ise state, ordering, retry, idempotency, versiyonlama ve operasyon sorumluluklarını ekibe bırakır. Bu nedenle amaç tek bir doğru yaklaşım önermek değil, ihtiyaçların hangi noktada farklı bir execution modeline dönüştüğünü görünür hale getirmektir.</p>

<h2 id="daha-fazla-okuma">Daha Fazla Okuma</h2>

<ul>
  <li><a href="https://nifi.apache.org/nifi-docs/overview.html">Apache NiFi — Overview</a></li>
  <li><a href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html">Apache Airflow — DAG kavramları</a></li>
  <li><a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html">AWS Step Functions nedir?</a></li>
  <li><a href="https://kafka.apache.org/design/">Apache Kafka — Design: ordering ve partition yaklaşımı</a></li>
</ul>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="workflow" /><category term="orchestration" /><category term="event-driven" /><category term="faas" /><category term="cloud-native" /><summary type="html"><![CDATA[Bir veri hattının ne zaman flow'a dönüştüğünü, hazır araçların nerede yeterli kaldığını ve dinamik bir flow engine'in nasıl tasarlanabileceğini inceliyorum.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/dynamic-flow-engine-cover.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/dynamic-flow-engine-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en-US"><title type="html">What Are Kubernetes, Kustomize, Helm Charts, and GitOps?</title><link href="https://fmarslan.com/en/2026/07/28/what-are-kubernetes-kustomize-helm-charts-and-gitops.html" rel="alternate" type="text/html" title="What Are Kubernetes, Kustomize, Helm Charts, and GitOps?" /><published>2026-07-28T00:00:00+00:00</published><updated>2026-07-28T00:00:00+00:00</updated><id>https://fmarslan.com/en/2026/07/28/what-are-kubernetes-kustomize-helm-charts-and-gitops</id><content type="html" xml:base="https://fmarslan.com/en/2026/07/28/what-are-kubernetes-kustomize-helm-charts-and-gitops.html"><![CDATA[<p>Soon after learning Kubernetes, we encounter another set of terms: YAML manifests, Helm Charts, Kustomize, GitOps, Argo CD, and Flux. Each solves a different problem, but learning them in isolation can make their relationship harder to see.</p>

<p>The goal of this article is not to turn every command into a tutorial. It is to build a useful mental model: what Kubernetes runs, what Helm packages, what Kustomize customizes, and how GitOps changes the way the resulting configuration reaches a cluster.</p>

<h2 id="start-with-the-core-problem-what-does-kubernetes-do">Start with the Core Problem: What Does Kubernetes Do?</h2>

<p>Kubernetes is an orchestration platform for running and managing containerized applications. We can describe how many copies of an application should run, how it should be reached, which configuration it should use, and how it should recover when a process fails.</p>

<p>We usually express this desired state through YAML manifests:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">orders-api</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">orders-api</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">registry.example.com/orders-api:1.4.0</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<p>This file tells Kubernetes to run two Pods from the <code class="language-plaintext highlighter-rouge">orders-api:1.4.0</code> image and expose port 8080 inside the container.</p>

<p>Kubernetes runs the workload and continuously tries to preserve that declared state. It does not, by itself, decide how our team should package a group of manifests, remove duplication between environments, review a production change, or synchronize a Git repository with a cluster. Those responsibilities belong to other layers.</p>

<h2 id="what-happens-as-the-number-of-manifests-grows">What Happens as the Number of Manifests Grows?</h2>

<p>A real application rarely consists of one <code class="language-plaintext highlighter-rouge">Deployment</code>. Over time, it may also need:</p>

<ul>
  <li>Services</li>
  <li>ConfigMaps</li>
  <li>Secret references</li>
  <li>Ingress or Gateway resources</li>
  <li>HorizontalPodAutoscalers</li>
  <li>ServiceAccounts and RBAC rules</li>
  <li>NetworkPolicies</li>
  <li>environment-specific replicas and resource limits</li>
</ul>

<p>Development, test, and production then introduce small but important differences. Development may need one replica while production needs four. Domains, image tags, resource limits, and integrations can also vary.</p>

<p>Copying every YAML file into an environment directory works at first, but each copy becomes another configuration that can drift. Helm and Kustomize approach this management problem from different directions.</p>

<h2 id="what-is-helm">What Is Helm?</h2>

<p>Helm is commonly described as a package manager for Kubernetes. It turns the Kubernetes resources needed by an application into a reusable and versioned package called a <strong>Chart</strong>.</p>

<p>A small Chart usually looks like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>orders-api/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    └── ingress.yaml
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Chart.yaml</code> contains package metadata such as its name and version.</li>
  <li><code class="language-plaintext highlighter-rouge">values.yaml</code> defines configurable defaults.</li>
  <li><code class="language-plaintext highlighter-rouge">templates/</code> contains Kubernetes manifest templates that consume those values.</li>
</ul>

<p>The replica count and container image, for example, can be placed in <code class="language-plaintext highlighter-rouge">values.yaml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">replicaCount</span><span class="pi">:</span> <span class="m">2</span>

<span class="na">image</span><span class="pi">:</span>
  <span class="na">repository</span><span class="pi">:</span> <span class="s">registry.example.com/orders-api</span>
  <span class="na">tag</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1.4.0"</span>
</code></pre></div></div>

<p>The Deployment template refers to them:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.replicaCount</span> <span class="pi">}}</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">.Values.image.repository</span><span class="nv"> </span><span class="s">}}:{{</span><span class="nv"> </span><span class="s">.Values.image.tag</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>

<p>The same Chart can now be installed with a different values file for each environment:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade <span class="nt">--install</span> orders-api ./orders-api <span class="se">\</span>
  <span class="nt">--values</span> values-prod.yaml
</code></pre></div></div>

<p>Helm is particularly useful when:</p>

<ul>
  <li>the same application must be installed for several customers or clusters,</li>
  <li>the package needs an independent version and distribution lifecycle,</li>
  <li>users need to configure many supported options,</li>
  <li>an existing product such as PostgreSQL, Prometheus, or cert-manager is distributed as a Chart,</li>
  <li>Charts will be shared through a repository or OCI registry.</li>
</ul>

<p>There is a cost to this flexibility. As conditions and parameters accumulate, a Chart can become harder to understand than the manifests it generates. Kubernetes YAML gradually turns into a program written in a template language. Making every field configurable is therefore not a sign of good Chart design.</p>

<h2 id="what-is-kustomize">What Is Kustomize?</h2>

<p>Kustomize customizes Kubernetes YAML without requiring placeholders or a template language. Its typical model keeps shared resources in a <strong>base</strong> and applies environment-specific changes through <strong>overlays</strong>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k8s/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    └── prod/
        └── kustomization.yaml
</code></pre></div></div>

<p>The base contains the common definition. A production overlay describes only what differs:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kustomize.config.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Kustomization</span>

<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">../../base</span>

<span class="na">images</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">registry.example.com/orders-api</span>
    <span class="na">newTag</span><span class="pi">:</span> <span class="s">1.4.0</span>

<span class="na">replicas</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
    <span class="na">count</span><span class="pi">:</span> <span class="m">4</span>
</code></pre></div></div>

<p>We can inspect the generated manifests with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl kustomize k8s/overlays/prod
</code></pre></div></div>

<p>And apply them with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-k</span> k8s/overlays/prod
</code></pre></div></div>

<p>Kustomize is a good fit when:</p>

<ul>
  <li>environments have a small set of explicit differences,</li>
  <li>we want the source to remain recognizable as ordinary Kubernetes YAML,</li>
  <li>dev, test, and production derive from one shared base,</li>
  <li>patch-based customization is enough,</li>
  <li>using the Kustomize support built into <code class="language-plaintext highlighter-rouge">kubectl</code> is useful.</li>
</ul>

<p>Kustomize is not a package manager. It does not provide a Chart repository, Chart dependencies, or the same release abstraction that Helm provides. Its primary job is composing and customizing resources.</p>

<h2 id="helm-or-kustomize">Helm or Kustomize?</h2>

<p>The question often assumes that one tool must replace the other. Their strongest use cases, however, are not identical.</p>

<table>
  <thead>
    <tr>
      <th>Requirement</th>
      <th>Helm</th>
      <th>Kustomize</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Package an application</td>
      <td>Strong fit</td>
      <td>Not its purpose</td>
    </tr>
    <tr>
      <td>Expose many supported parameters</td>
      <td>Strong fit</td>
      <td>Better for limited variation</td>
    </tr>
    <tr>
      <td>Manage environment differences</td>
      <td>Values files</td>
      <td>Bases and overlays</td>
    </tr>
    <tr>
      <td>Keep source manifests directly readable</td>
      <td>Templates add indirection</td>
      <td>Usually easier</td>
    </tr>
    <tr>
      <td>Package versions and dependencies</td>
      <td>Supported</td>
      <td>Not supported</td>
    </tr>
    <tr>
      <td>Built into <code class="language-plaintext highlighter-rouge">kubectl</code></td>
      <td>Separate CLI</td>
      <td>Supported by <code class="language-plaintext highlighter-rouge">kubectl</code></td>
    </tr>
  </tbody>
</table>

<p>A practical rule is:</p>

<ul>
  <li>Use Helm when the output is a reusable, distributable application package.</li>
  <li>Use Kustomize when you own the manifests and mainly need a small number of environment variations.</li>
  <li>Combine them when a packaged application still needs controlled, site-specific adjustments.</li>
</ul>

<p>The right choice depends less on tool popularity than on the kind of variability we need to manage.</p>

<h2 id="what-is-gitops">What Is GitOps?</h2>

<p>Helm and Kustomize produce or customize manifests. GitOps is an operating model for managing the desired state represented by those manifests.</p>

<p>In a GitOps workflow, a Git repository is the source of truth for the system’s intended configuration. Instead of connecting to production and running a command directly, a developer or platform engineer proposes a Git change. The change can be reviewed and approved before a controller applies it to the cluster.</p>

<pre><code class="language-mermaid">flowchart LR
    D["Developer"] --&gt;|Pull Request| G["Git repository"]
    G --&gt;|Desired state| C["Argo CD or Flux"]
    C --&gt;|Synchronization| K["Kubernetes cluster"]
    K -. "Live state" .-&gt; C
</code></pre>

<p>Git is no longer just storage for YAML files. It becomes the versioned record of the desired operational state.</p>

<h2 id="what-do-argo-cd-and-flux-do">What Do Argo CD and Flux Do?</h2>

<p>Argo CD and Flux are widely used controllers for implementing GitOps on Kubernetes. They observe a source, generate or read the desired manifests, compare them with the live cluster, and report the difference. Depending on policy, they can also correct that difference automatically.</p>

<p>If Git declares four replicas but someone manually changes the live Deployment to two, the controller detects the drift. With automated synchronization or self-healing enabled, it can restore the Git-defined state.</p>

<p>This continuous comparison and correction is a <strong>reconciliation loop</strong>. It is more important than the presence of a Git repository alone; storing manifests in Git but deploying them through untracked manual commands does not give us the same operating model.</p>

<p>GitOps can provide:</p>

<ul>
  <li>Pull Request review for operational changes,</li>
  <li>an audit trail of who changed what and when,</li>
  <li>detection of manual configuration drift,</li>
  <li>reproducible environment definitions,</li>
  <li>rollback by restoring a known Git revision,</li>
  <li>reduced need to distribute cluster credentials to developer machines or CI jobs.</li>
</ul>

<p>It does not make an unsafe configuration safe. A bad manifest merged into Git can still be delivered very efficiently. Secret management, tests, approvals, access control, rollout health, and recovery policies remain separate design concerns.</p>

<h2 id="how-is-gitops-different-from-cicd">How Is GitOps Different from CI/CD?</h2>

<p>In a traditional CI/CD pipeline, the pipeline builds the application, pushes a container image, and often deploys directly to Kubernetes. The pipeline therefore needs credentials that allow it to change the cluster.</p>

<p>A GitOps design can separate these responsibilities:</p>

<ol>
  <li>CI tests the source code.</li>
  <li>CI builds the container image and pushes it to a registry.</li>
  <li>The image tag in the deployment repository is updated.</li>
  <li>Argo CD or Flux detects the new desired state.</li>
  <li>The controller reconciles the cluster with that state.</li>
</ol>

<p>CI still exists, but it no longer has to perform the deployment itself. It produces an artifact and records the requested version in Git; a controller operating near or inside the cluster pulls and applies the change. This is why GitOps is often described as a pull-based delivery model.</p>

<h2 id="how-do-these-layers-work-together">How Do These Layers Work Together?</h2>

<p>We can position the tools in one flow:</p>

<pre><code class="language-mermaid">flowchart LR
    H["Helm: packaging"] --&gt; M["Kubernetes manifests"]
    Z["Kustomize: environment adaptation"] --&gt; M
    M --&gt; G["Git: desired state"]
    G --&gt; A["Argo CD / Flux: reconciliation"]
    A --&gt; K["Kubernetes: live state"]
</code></pre>

<p>A project might use that model as follows:</p>

<ol>
  <li>Application source code lives in its own Git repository.</li>
  <li>CI tests the code and publishes <code class="language-plaintext highlighter-rouge">orders-api:1.4.0</code>.</li>
  <li>The application is packaged as a Helm Chart.</li>
  <li>Environment-specific values or Kustomize overlays live in a deployment repository.</li>
  <li>A Pull Request updates the production image tag to <code class="language-plaintext highlighter-rouge">1.4.0</code>.</li>
  <li>Argo CD detects the change and synchronizes the cluster.</li>
  <li>The team observes rollout health through Argo CD and its monitoring stack.</li>
</ol>

<p>This is an example, not a mandatory repository layout. A small team may not benefit from separating application and deployment repositories. A larger or regulated organization may need that boundary for access control, approval, and auditability.</p>

<h2 id="does-every-project-need-all-of-them">Does Every Project Need All of Them?</h2>

<p>No. Adding tools is not the same as increasing maturity.</p>

<p>A small application may be well served by a few clear YAML files and <code class="language-plaintext highlighter-rouge">kubectl apply</code>. Kustomize becomes useful when environment differences appear. Helm becomes valuable when the application needs to be packaged and reused. GitOps starts to pay for itself as the number of applications, environments, teams, and audit requirements grows.</p>

<table>
  <thead>
    <tr>
      <th>Scenario</th>
      <th>Reasonable starting point</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>One application, one environment, few resources</td>
      <td>Plain Kubernetes YAML</td>
    </tr>
    <tr>
      <td>One application, several environments, small differences</td>
      <td>Kustomize</td>
    </tr>
    <tr>
      <td>Reusable or externally distributed application</td>
      <td>Helm Chart</td>
    </tr>
    <tr>
      <td>Many applications and environments with auditable delivery</td>
      <td>GitOps with Argo CD or Flux</td>
    </tr>
    <tr>
      <td>Packaging and site-specific customization both matter</td>
      <td>Helm + Kustomize + GitOps</td>
    </tr>
  </tbody>
</table>

<p>This table is not a universal prescription. Team experience, security requirements, operational load, and the ownership model can change the decision.</p>

<h2 id="common-mistakes">Common Mistakes</h2>

<p>Several mistakes appear repeatedly across these tools:</p>

<ul>
  <li>copying all YAML files for every environment,</li>
  <li>turning every Helm field into a parameter,</li>
  <li>using Kustomize overlays that replace so much of the base that the base has little meaning,</li>
  <li>adopting GitOps while keeping routine manual cluster changes,</li>
  <li>deploying mutable tags such as <code class="language-plaintext highlighter-rouge">latest</code>,</li>
  <li>storing unencrypted secret values in Git,</li>
  <li>applying generated manifests without rendering and validating them first,</li>
  <li>losing the traceable link between a Git change and the running image,</li>
  <li>enabling automatic synchronization before defining health checks and recovery behavior.</li>
</ul>

<p>Regardless of the tools, the generated configuration should remain understandable, each change should be traceable, and a running version should be reproducible.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Kubernetes, Helm, Kustomize, and GitOps are not competing answers to one question. They operate at different layers of the delivery process.</p>

<p>Kubernetes runs the application. Helm packages its resources. Kustomize adapts manifests to an environment. Git records the intended state. Argo CD or Flux continuously reconciles that state with the cluster.</p>

<p>We do not need to introduce every layer on day one. We first need to identify where the current complexity lives: packaging, environment variation, review and audit, or the gap between Git and the live cluster. The useful tool is the one that reduces that specific complexity without creating a larger operational burden elsewhere.</p>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><a href="https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/">Kubernetes — Declarative Management with Kustomize</a></li>
  <li><a href="https://helm.sh/docs/intro/introduction/">Helm — Introduction to Helm</a></li>
  <li><a href="https://argo-cd.readthedocs.io/en/stable/">Argo CD — Declarative GitOps CD for Kubernetes</a></li>
  <li><a href="https://argo-cd.readthedocs.io/en/latest/user-guide/ci_automation/">Argo CD — Automation from CI Pipelines</a></li>
  <li><a href="/en/2024/09/13/helm-and-kubernetes-rendering-and-managing-helm-charts-locally.html">Rendering and Managing Helm Charts Locally</a></li>
  <li><a href="/en/2025/11/26/versioning-and-releasing-management-on-kubernetes.html">Versioning and Release Management on Kubernetes</a></li>
</ul>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="kubernetes" /><category term="helm" /><category term="kustomize" /><category term="gitops" /><category term="devops" /><summary type="html"><![CDATA[Kubernetes, Helm, Kustomize, and GitOps manage different layers of delivery. I explain what each one solves and how they work together.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/kubernetes-delivery-layers-cover.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/kubernetes-delivery-layers-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="tr-TR"><title type="html">Kubernetes, Kustomize, Helm Chart ve GitOps Nedir? Birlikte Nasıl Çalışırlar?</title><link href="https://fmarslan.com/tr/2026/07/28/kubernetes-kustomize-helm-chart-ve-gitops-nedir.html" rel="alternate" type="text/html" title="Kubernetes, Kustomize, Helm Chart ve GitOps Nedir? Birlikte Nasıl Çalışırlar?" /><published>2026-07-28T00:00:00+00:00</published><updated>2026-07-28T00:00:00+00:00</updated><id>https://fmarslan.com/tr/2026/07/28/kubernetes-kustomize-helm-chart-ve-gitops-nedir</id><content type="html" xml:base="https://fmarslan.com/tr/2026/07/28/kubernetes-kustomize-helm-chart-ve-gitops-nedir.html"><![CDATA[<p>Kubernetes öğrenmeye başlayanların karşısına kısa süre içinde yeni kavramlar çıkar: YAML manifestleri, Helm Chart, Kustomize, GitOps, Argo CD ve Flux. Bu kavramların her biri farklı bir problemi çözer. Ancak çoğu kaynak onları tek tek anlattığı için araçların birbirleriyle ilişkisini görmek zorlaşabilir.</p>

<p>Bu yazının amacı bütün komutları ezberletmek değil, büyük resmi kurmaktır: Kubernetes neyi çalıştırır, Helm neyi paketler, Kustomize neyi özelleştirir ve GitOps bu süreci nasıl yönetir?</p>

<h2 id="önce-temel-problem-kubernetes-ne-yapar">Önce temel problem: Kubernetes ne yapar?</h2>

<p>Kubernetes, container olarak paketlenmiş uygulamaları çalıştıran ve yöneten bir orkestrasyon platformudur. Bir uygulamanın kaç kopya çalışacağını, hangi porttan erişileceğini, hangi konfigürasyonu kullanacağını ve hata durumunda nasıl yeniden başlatılacağını tanımlayabiliriz.</p>

<p>Bu istekleri Kubernetes’e genellikle YAML manifestleriyle bildiririz.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">orders-api</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">orders-api</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">registry.example.com/orders-api:1.4.0</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<p>Bu dosya Kubernetes’e kabaca şunu söyler: <code class="language-plaintext highlighter-rouge">orders-api:1.4.0</code> imajından iki pod çalıştır ve container’ın 8080 portunu kullan.</p>

<p>Kubernetes burada uygulamayı çalıştırır ve tanımlanan durumu korumaya çalışır. Fakat manifestleri farklı ortamlar için nasıl yöneteceğimiz, tekrarları nasıl azaltacağımız veya değişiklikleri kümeye kimin uygulayacağı Kubernetes’in tek başına çözdüğü sorunlar değildir.</p>

<h2 id="manifest-sayısı-arttığında-ne-olur">Manifest sayısı arttığında ne olur?</h2>

<p>Gerçek bir uygulama çoğunlukla yalnızca bir <code class="language-plaintext highlighter-rouge">Deployment</code> dosyasından oluşmaz. Zamanla aşağıdaki kaynaklar eklenir:</p>

<ul>
  <li>Service</li>
  <li>ConfigMap</li>
  <li>Secret referansları</li>
  <li>Ingress veya Gateway</li>
  <li>HorizontalPodAutoscaler</li>
  <li>ServiceAccount ve RBAC kuralları</li>
  <li>NetworkPolicy</li>
  <li>ortam bazlı kaynak limitleri ve replica sayıları</li>
</ul>

<p>Bir de geliştirme, test ve production ortamları varsa aynı dosyaların küçük farklarla kopyalandığı bir yapı oluşabilir. Örneğin geliştirme ortamında tek replica yeterliyken production ortamında dört replica gerekebilir. Kullanılan domain, image tag’i ve kaynak limitleri de değişebilir.</p>

<p>Helm ve Kustomize bu yönetim probleminin farklı taraflarına yaklaşır.</p>

<h2 id="helm-nedir">Helm nedir?</h2>

<p>Helm, Kubernetes için paket yöneticisi olarak tanımlanır. Bir uygulamaya ait Kubernetes kaynaklarını yeniden kullanılabilir ve versiyonlanabilir bir paket hâline getirir. Bu pakete <strong>Chart</strong> denir.</p>

<p>Bir Helm Chart genel olarak şu yapıya sahiptir:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>orders-api/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    └── ingress.yaml
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Chart.yaml</code>, paketin adı ve versiyonu gibi bilgileri tutar.</li>
  <li><code class="language-plaintext highlighter-rouge">values.yaml</code>, değiştirilebilir varsayılan değerleri içerir.</li>
  <li><code class="language-plaintext highlighter-rouge">templates/</code>, bu değerleri kullanan Kubernetes manifest şablonlarını barındırır.</li>
</ul>

<p>Örneğin replica sayısı <code class="language-plaintext highlighter-rouge">values.yaml</code> içinde tanımlanabilir:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">replicaCount</span><span class="pi">:</span> <span class="m">2</span>

<span class="na">image</span><span class="pi">:</span>
  <span class="na">repository</span><span class="pi">:</span> <span class="s">registry.example.com/orders-api</span>
  <span class="na">tag</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1.4.0"</span>
</code></pre></div></div>

<p>Deployment şablonu ise bu değerleri kullanır:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.replicaCount</span> <span class="pi">}}</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">.Values.image.repository</span><span class="nv"> </span><span class="s">}}:{{</span><span class="nv"> </span><span class="s">.Values.image.tag</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>

<p>Bu sayede aynı Chart farklı değer dosyalarıyla farklı ortamlara kurulabilir.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade <span class="nt">--install</span> orders-api ./orders-api <span class="se">\</span>
  <span class="nt">--values</span> values-prod.yaml
</code></pre></div></div>

<p>Helm özellikle şu durumlarda güçlüdür:</p>

<ul>
  <li>Aynı uygulama farklı müşterilere veya kümelere kurulacaksa</li>
  <li>Paket versiyonlamak ve dağıtmak gerekiyorsa</li>
  <li>Çok sayıda ayarın kullanıcı tarafından değiştirilebilmesi isteniyorsa</li>
  <li>PostgreSQL, Prometheus veya cert-manager gibi hazır çözümler kurulacaksa</li>
  <li>Uygulama bir OCI registry üzerinden Chart olarak paylaşılacaksa</li>
</ul>

<p>Ancak şablon sayısı ve koşullar arttıkça Chart’ın okunması zorlaşabilir. Kubernetes YAML dosyaları zamanla bir şablon programlama diline dönüşebilir. Bu nedenle her değişkeni parametre hâline getirmek iyi bir tasarım değildir.</p>

<h2 id="kustomize-nedir">Kustomize nedir?</h2>

<p>Kustomize, mevcut Kubernetes YAML dosyalarını şablon diline dönüştürmeden özelleştirmeyi sağlar. Temel yaklaşımı, ortak kaynakları bir <strong>base</strong> altında tutmak ve ortama özel değişiklikleri <strong>overlay</strong> olarak uygulamaktır.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k8s/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    └── prod/
        └── kustomization.yaml
</code></pre></div></div>

<p>Base, bütün ortamların ortak tanımını içerir. Production overlay’i ise yalnızca farkı belirtir:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kustomize.config.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Kustomization</span>

<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">../../base</span>

<span class="na">images</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">registry.example.com/orders-api</span>
    <span class="na">newTag</span><span class="pi">:</span> <span class="s">1.4.0</span>

<span class="na">replicas</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">orders-api</span>
    <span class="na">count</span><span class="pi">:</span> <span class="m">4</span>
</code></pre></div></div>

<p>Ortaya çıkacak manifesti görmek için:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl kustomize k8s/overlays/prod
</code></pre></div></div>

<p>Uygulamak için:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-k</span> k8s/overlays/prod
</code></pre></div></div>

<p>Kustomize özellikle şu durumlarda uygundur:</p>

<ul>
  <li>Ortamlar arasında az ve belirgin farklar varsa</li>
  <li>Kubernetes manifestlerinin sade YAML olarak kalması isteniyorsa</li>
  <li>Ortak bir base üzerinden dev, test ve prod varyasyonları üretilecekse</li>
  <li>Yeni bir şablon dili kullanmadan patch tabanlı değişiklik yapılacaksa</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl</code> ile yerleşik gelen bir çözüm tercih ediliyorsa</li>
</ul>

<p>Kustomize bir paket yöneticisi değildir. Chart bağımlılıkları, Chart repository’si veya release geçmişi gibi Helm özelliklerini sağlamaz. Onun temel işi mevcut manifestleri düzenlemek ve birleştirmektir.</p>

<h2 id="helm-mi-kustomize-mı">Helm mi, Kustomize mı?</h2>

<p>Bu soru çoğu zaman iki araçtan yalnızca biri seçilmek zorundaymış gibi sorulur. Oysa araçların güçlü olduğu alanlar farklıdır.</p>

<table>
  <thead>
    <tr>
      <th>İhtiyaç</th>
      <th>Helm</th>
      <th>Kustomize</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Uygulamayı paketlemek</td>
      <td>Güçlü</td>
      <td>Amaç bu değil</td>
    </tr>
    <tr>
      <td>Çok sayıda parametre sunmak</td>
      <td>Güçlü</td>
      <td>Sınırlı kullanım için uygun</td>
    </tr>
    <tr>
      <td>Ortam farklarını yönetmek</td>
      <td>Values dosyalarıyla</td>
      <td>Base ve overlay ile</td>
    </tr>
    <tr>
      <td>Manifestlerin doğrudan okunması</td>
      <td>Şablonlar nedeniyle zorlaşabilir</td>
      <td>Genellikle daha kolay</td>
    </tr>
    <tr>
      <td>Paket versiyonu ve bağımlılık</td>
      <td>Var</td>
      <td>Yok</td>
    </tr>
    <tr>
      <td>Kubernetes’e yerleşik kullanım</td>
      <td>Ayrı CLI gerekir</td>
      <td><code class="language-plaintext highlighter-rouge">kubectl</code> içinde bulunur</td>
    </tr>
  </tbody>
</table>

<p>Basit bir karar kuralı şöyle kurulabilir:</p>

<ul>
  <li>Yeniden kullanılabilir ve dağıtılabilir bir uygulama paketi üretiyorsak Helm güçlü bir adaydır.</li>
  <li>Kendi uygulamamızın birkaç ortamdaki küçük farklarını yönetiyorsak Kustomize daha sade kalabilir.</li>
  <li>İhtiyaç varsa Helm ile üretilen kaynakların üzerine Kustomize değişiklikleri de uygulanabilir.</li>
</ul>

<p>Önemli olan aracı alışkanlıkla değil, değişkenliğin türüne göre seçmektir.</p>

<h2 id="gitops-nedir">GitOps nedir?</h2>

<p>Helm ve Kustomize manifest üretme veya özelleştirme araçlarıdır. GitOps ise sistemin nasıl yönetileceğine ilişkin bir çalışma modelidir.</p>

<p>GitOps yaklaşımında Git deposu, sistemin olması gereken durumunu tutar. Bir geliştirici veya operasyon ekibi doğrudan production kümesine bağlanıp komut çalıştırmak yerine Git üzerinde değişiklik yapar. Değişiklik incelenir, onaylanır ve bir GitOps controller tarafından kümeye uygulanır.</p>

<p>Basitleştirilmiş akış şöyledir:</p>

<pre><code class="language-mermaid">flowchart LR
    D["Geliştirici"] --&gt;|Pull Request| G["Git deposu"]
    G --&gt;|İstenen durum| C["Argo CD veya Flux"]
    C --&gt;|Senkronizasyon| K["Kubernetes kümesi"]
    K -. "Mevcut durum" .-&gt; C
</code></pre>

<p>Burada Git yalnızca dosyaların saklandığı yer değildir; <strong>istenen durumun kayıt altına alındığı kaynak</strong> hâline gelir.</p>

<h2 id="argo-cd-ve-flux-ne-yapar">Argo CD ve Flux ne yapar?</h2>

<p>Argo CD ve Flux, GitOps yaklaşımını Kubernetes üzerinde uygulayan yaygın araçlardır. Git deposundaki manifestleri izler, bunları kümedeki mevcut durumla karşılaştırır ve aradaki farkı görünür hâle getirir. Yapılandırmaya bağlı olarak farkları otomatik olarak da giderebilirler.</p>

<p>Örneğin Git’te replica sayısı dört olarak tanımlanmışken kümede elle ikiye düşürülürse controller bu sapmayı tespit eder. Otomatik senkronizasyon açıksa sistemi tekrar Git’teki duruma getirebilir.</p>

<p>Bu çalışma şekline <strong>reconciliation</strong>, yani uzlaştırma döngüsü denir.</p>

<p>GitOps’un sağlayabileceği başlıca avantajlar şunlardır:</p>

<ul>
  <li>Değişikliklerin Pull Request üzerinden incelenmesi</li>
  <li>Kimin, neyi, ne zaman değiştirdiğinin izlenebilmesi</li>
  <li>Kümedeki manuel ve kayıt dışı değişikliklerin tespit edilmesi</li>
  <li>Önceki Git revizyonuna dönerek geri alma sürecinin sadeleşmesi</li>
  <li>Ortamların aynı yöntemle tekrar üretilebilmesi</li>
  <li>Deploy yetkisinin geliştirici makineleri veya klasik CI sistemleri yerine sınırlı bir controller’da tutulması</li>
</ul>

<p>GitOps bütün operasyon problemlerini kendiliğinden çözmez. Hatalı manifest Git’e alındığında hata da kontrollü biçimde dağıtılabilir. Secret yönetimi, testler, onay mekanizmaları, erişim kontrolü ve geri dönüş stratejisi ayrıca tasarlanmalıdır.</p>

<h2 id="cicd-ile-gitops-arasındaki-fark-nedir">CI/CD ile GitOps arasındaki fark nedir?</h2>

<p>Klasik bir CI/CD akışında pipeline uygulamayı derler, container imajını üretir ve çoğu zaman doğrudan kümeye deploy eder. Bunun için pipeline’ın Kubernetes erişim bilgilerine sahip olması gerekir.</p>

<p>GitOps modelinde sorumluluk ayrılabilir:</p>

<ol>
  <li>CI kodu test eder.</li>
  <li>CI container imajını üretip registry’ye gönderir.</li>
  <li>Dağıtım deposundaki image tag’i güncellenir.</li>
  <li>Argo CD veya Flux değişikliği görür.</li>
  <li>GitOps controller yeni durumu kümeye uygular.</li>
</ol>

<p>Yani CI hâlâ vardır; fakat Kubernetes’e doğrudan deploy etmek zorunda değildir. CI bir artifact üretir, CD tarafında ise kümenin içindeki controller Git’ten değişikliği çeker. Bu nedenle GitOps çoğunlukla <strong>pull tabanlı deployment</strong> modeli olarak anılır.</p>

<h2 id="bu-kavramlar-birlikte-nasıl-çalışır">Bu kavramlar birlikte nasıl çalışır?</h2>

<p>Araçları tek cümleyle konumlandırırsak:</p>

<pre><code class="language-mermaid">flowchart LR
    H["Helm: paketleme"] --&gt; M["Kubernetes manifestleri"]
    Z["Kustomize: ortam uyarlaması"] --&gt; M
    M --&gt; G["Git: istenen durum"]
    G --&gt; A["Argo CD / Flux: uzlaştırma"]
    A --&gt; K["Kubernetes: çalışma durumu"]
</code></pre>

<p>Örnek bir projede süreç şu şekilde kurulabilir:</p>

<ol>
  <li>Uygulama kaynak kodu kendi Git deposunda tutulur.</li>
  <li>CI testlerden sonra <code class="language-plaintext highlighter-rouge">orders-api:1.4.0</code> imajını üretir.</li>
  <li>Uygulamanın kurulumu bir Helm Chart olarak paketlenir.</li>
  <li>Ortama özel values dosyaları veya Kustomize overlay’leri dağıtım deposunda saklanır.</li>
  <li>Production image tag’i Pull Request ile <code class="language-plaintext highlighter-rouge">1.4.0</code> olarak güncellenir.</li>
  <li>Argo CD değişikliği algılar ve Kubernetes kümesiyle senkronize eder.</li>
  <li>Dağıtım sağlığı Argo CD ve gözlemlenebilirlik araçları üzerinden takip edilir.</li>
</ol>

<p>Bu yalnızca örnek bir modeldir. Küçük bir ekip için ayrı kod ve deployment depoları gereksiz olabilir. Büyük veya regüle bir yapıda ise yetki ayrımı, onay süreçleri ve denetlenebilirlik nedeniyle ayrı depolar anlamlı olabilir.</p>

<h2 id="her-projede-hepsine-ihtiyaç-var-mı">Her projede hepsine ihtiyaç var mı?</h2>

<p>Hayır. Araç sayısının artması her zaman olgunluk göstergesi değildir.</p>

<p>Küçük bir uygulama için birkaç sade YAML dosyası ve <code class="language-plaintext highlighter-rouge">kubectl apply</code> yeterli olabilir. Ortam farkları oluştuğunda Kustomize eklenebilir. Uygulama tekrar kullanılabilir bir paket hâline geldiğinde Helm tercih edilebilir. Dağıtım sayısı, ekip büyüklüğü ve denetim ihtiyacı arttığında Argo CD veya Flux ile GitOps yaklaşımına geçilebilir.</p>

<p>Kabaca şöyle düşünebiliriz:</p>

<table>
  <thead>
    <tr>
      <th>Senaryo</th>
      <th>Muhtemel başlangıç noktası</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tek uygulama, tek ortam, az sayıda kaynak</td>
      <td>Düz Kubernetes YAML</td>
    </tr>
    <tr>
      <td>Aynı uygulama, birkaç ortam, küçük farklar</td>
      <td>Kustomize</td>
    </tr>
    <tr>
      <td>Tekrar kullanılabilir veya dışarı dağıtılan uygulama</td>
      <td>Helm Chart</td>
    </tr>
    <tr>
      <td>Çok sayıda uygulama ve ortam, denetlenebilir deployment</td>
      <td>GitOps + Argo CD/Flux</td>
    </tr>
    <tr>
      <td>Paketleme ve ortam özelleştirmesi birlikte gerekli</td>
      <td>Helm + Kustomize + GitOps</td>
    </tr>
  </tbody>
</table>

<p>Buradaki tablo kesin bir reçete değildir. Ekip deneyimi, güvenlik beklentileri, uygulama sayısı ve operasyon yükü kararı değiştirebilir.</p>

<h2 id="sık-yapılan-hatalar">Sık yapılan hatalar</h2>

<p>Bu araçlar kullanılırken bazı hatalar tekrar eder:</p>

<ul>
  <li>Ortam başına bütün YAML dosyalarını kopyalamak</li>
  <li>Helm içindeki her satırı parametre hâline getirmek</li>
  <li>Kustomize overlay’lerinde base yapısını tamamen değiştirecek kadar patch üretmek</li>
  <li>GitOps kullanırken kümeye elle müdahale etmeyi normal operasyon şekli olarak sürdürmek</li>
  <li><code class="language-plaintext highlighter-rouge">latest</code> gibi değişken image tag’leri kullanmak</li>
  <li>Secret değerlerini şifrelemeden Git’e koymak</li>
  <li>Render edilen manifesti deployment öncesinde doğrulamamak</li>
  <li>Git’teki değişiklik ile çalışan container imajı arasında izlenebilir bağ kurmamak</li>
  <li>Otomatik senkronizasyonu geri dönüş ve sağlık kontrolü tasarlamadan açmak</li>
</ul>

<p>Araçlardan bağımsız temel ilke şudur: üretilecek manifest anlaşılabilir, değişiklik izlenebilir ve çalışan sürüm yeniden üretilebilir olmalıdır.</p>

<h2 id="sonuç">Sonuç</h2>

<p>Kubernetes, Helm, Kustomize ve GitOps aynı problemin rakip çözümleri değildir. Bir dağıtım sürecinin farklı katmanlarında görev alırlar.</p>

<p>Kubernetes uygulamayı çalıştırır. Helm uygulamayı paketler. Kustomize manifestleri belirli bir ortama uyarlar. Git istenen durumu kayıt altına alır. Argo CD veya Flux ise bu durumun Kubernetes kümesinde korunmasını sağlar.</p>

<p>İyi bir başlangıç için bütün araçları aynı anda sisteme eklemek gerekmez. Önce mevcut problemin hangi katmanda olduğunu belirlemek gerekir: paketleme mi, ortam farklılığı mı, değişikliklerin denetlenmesi mi, yoksa kümeyle Git arasındaki tutarlılık mı?</p>

<p>Doğru araç, en popüler olan değil; sistemdeki gerçek karmaşıklığı azaltan araçtır.</p>

<h2 id="daha-fazla-okuma">Daha Fazla Okuma</h2>

<ul>
  <li><a href="https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/">Kubernetes — Kustomize ile bildirimsel nesne yönetimi</a></li>
  <li><a href="https://helm.sh/docs/intro/introduction/">Helm — Introduction to Helm</a></li>
  <li><a href="https://argo-cd.readthedocs.io/en/stable/">Argo CD — Declarative GitOps CD for Kubernetes</a></li>
  <li><a href="https://argo-cd.readthedocs.io/en/latest/user-guide/ci_automation/">Argo CD — CI pipeline otomasyonu</a></li>
  <li><a href="/helm/2024/09/13/helm-chart-local-render.html">Helm Chart’ları Localde Render Etme ve Yönetme</a></li>
  <li><a href="/2025/11/26/kubernetes-uzerinde-versioning-ve-releasing-yonetimi.html">Kubernetes Üzerinde Versioning ve Releasing Yönetimi</a></li>
</ul>]]></content><author><name>Fatih Mehmet ARSLAN</name><email>contact@fmarslan.com</email></author><category term="kubernetes" /><category term="helm" /><category term="kustomize" /><category term="gitops" /><category term="devops" /><summary type="html"><![CDATA[Kubernetes, Helm, Kustomize ve GitOps aynı dağıtım sürecinin farklı katmanlarını yönetir. Hangi aracın neyi çözdüğünü ve birlikte nasıl çalıştıklarını inceliyorum.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fmarslan.com/assets/img/kubernetes-delivery-layers-cover.png" /><media:content medium="image" url="https://fmarslan.com/assets/img/kubernetes-delivery-layers-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>