Cloud Run Servisini Cloud SQL Örneğine Bağlamak

Merhabalar, bu yazımda Cloud Run üstünde koşan bir servisi Cloud SQL örneğine nasıl bağladığımıza bakacağız. Konu kâğıt üstünde basit duruyor ama ilk denediğinizde 'connection refused' veya 'too many connections' hatasıyla karşılaşmamak için bilmeniz gereken birkaç ufak detay var. Lafı çok uzatmadan başlayayım.

Cloud Run, GCP'nin sunucusuz container platformu. Trafik geldiğinde sıfırdan N instance'a ölçekleniyor, gelmeyince yine sıfıra iniyor. Bu güzel ama veritabanı tarafında bir gerçeklik var: Cloud SQL örneğinin sınırlı sayıda bağlantısı var ve siz patlayan instance sayısıyla doğru orantılı bağlantı açarsanız hızla duvara toslarsınız. O yüzden bağlantı şeklini ve havuzu doğru kurmak işin yarısı.

Bağlantı için iki yol var

Pratikte iki ana yaklaşım kullanılıyor:

  • Yerleşik Cloud SQL connector + Unix socket: Servisi deploy ederken --add-cloudsql-instances parametresini geçiyorsunuz, Cloud Run container içine /cloudsql/<INSTANCE_CONNECTION_NAME> yolunda bir Unix socket bind ediyor. Public IP veya private IP fark etmiyor, akış GCP'nin kendi proxy'si üzerinden geçiyor.
  • Cloud SQL Connector kütüphaneleri: Python için cloud-sql-python-connector, Java için postgres-socket-factory gibi paketler. IAM Authentication ve TLS'i programatik olarak yönetiyorlar. IAM tabanlı auth kullanmak istiyorsanız genelde tercih edilen yol.

Bence ilk projede socket yaklaşımı yeter. Connector kütüphaneleri IAM auth veya çok özel ihtiyaçlar varsa devreye girer; basit bir CRUD servisinde fazladan bağımlılık.

IAM ve servis hesabı

İlk yapılacak iş Cloud Run servisinin koştuğu service account'a roles/cloudsql.client rolünü vermek. Cloud SQL Admin API'sinin de açık olması lazım; aksi halde proxy bağlantı kuramıyor.

Connection name'i almakla başlayalım:

gcloud sql instances describe my-postgres \
  --format='value(connectionName)'
# ornegin: my-project:europe-west1:my-postgres

Bu üçlü project:region:instance ileride deploy ederken sürekli işimize yarayacak. Sonrasında deploy:

gcloud run deploy payment-api \
  --image=europe-west1-docker.pkg.dev/my-project/apps/payment-api:1.4.0 \
  --region=europe-west1 \
  --service-account=payment-api-sa@my-project.iam.gserviceaccount.com \
  --add-cloudsql-instances=my-project:europe-west1:my-postgres \
  --set-env-vars=DB_HOST=/cloudsql/my-project:europe-west1:my-postgres,DB_USER=payment,DB_NAME=payments \
  --set-secrets=DB_PASS=db-password:latest \
  --max-instances=20

Burada dikkat etmenizi istediğim iki şey var. Birincisi şifreyi --set-env-vars ile değil --set-secrets ile geçiyoruz; düz metin parola env'e asla yazılmaz, Secret Manager bunun için var. İkincisi --max-instances koymak. Cloud Run kendi haline bırakılırsa yüzlerce instance açabilir, her biri bağlantı tutar, Cloud SQL'in max_connections limiti kısa sürede dolar.

Uygulama tarafı

Python tarafında SQLAlchemy ile bağlanırken host olarak /cloudsql/... socket yolunu veriyoruz. URL'de host yerine query parametresinde belirtmek gerekiyor, çünkü TCP gibi davranmıyor:

import os
from sqlalchemy import create_engine
from sqlalchemy.engine.url import URL

instance = os.environ['DB_HOST']  # /cloudsql/proj:region:instance

url = URL.create(
    drivername='postgresql+psycopg2',
    username=os.environ['DB_USER'],
    password=os.environ['DB_PASS'],
    database=os.environ['DB_NAME'],
    query={'host': instance},
)

engine = create_engine(
    url,
    pool_size=3,
    max_overflow=2,
    pool_pre_ping=True,
    pool_recycle=1800,
)

pool_size=3 ve max_overflow=2 belki size düşük gelebilir. Ama hesabı yapın: 20 instance × 5 bağlantı = 100. Cloud SQL küçük bir tier'daysa default max_connections zaten 100-200 civarında. Pool'u büyütmeden önce instance sayısını sınırlamak daha sağlıklı.

pool_pre_ping=True da kritik. Cloud Run instance'ları cold start sonrası uyuyabiliyor, bağlantı kopuk olabiliyor; pre-ping olmazsa ilk istek hata yiyor.

Private IP isteyenler için kısa not

Eğer Cloud SQL örneği sadece private IP ile erişilebiliyorsa, Cloud Run'ın VPC'ye girmesi lazım. Bunun için Serverless VPC Access connector oluşturuyoruz ve deploy sırasında --vpc-connector ile bağlıyoruz. Bu yazıda detaya girmeyeceğim ama unutmayın: socket yöntemi public IP yokken bile çalışır, çünkü Cloud SQL Auth Proxy GCP içinden konuşuyor. Yani private IP zorunluluğu güvenlik gerekçesiyle değilse, socket genelde yeterli.

Sık karşılaşılan tuzaklar

  • connection refused hatası: Çoğunlukla --add-cloudsql-instances adı yanlış yazılmış veya servis hesabında roles/cloudsql.client rolü yok. Bir de Cloud SQL Admin API'nin açık olduğunu kontrol edin.
  • too many connections: Pool size'ı küçültün, --max-instances koyun. Gerekirse Cloud SQL tarafında max_connections flag'ini artırın ama bu ikinci adım; önce uygulama tarafını disipline edin.
  • Connection timeout: Cold start sırasında Cloud SQL Auth Proxy'nin ayağa kalkması bir-iki saniye sürebiliyor. Container'ın --timeout değerini ve client tarafındaki connect timeout'u makul tutun, pool_pre_ping zaten bayat soketleri eler.
  • Plaintext parola: --set-env-vars=DB_PASS=... yazma alışkanlığı çok yaygın. Loglara, revision history'ye sızar. Secret Manager'a 30 saniye veriyorsunuz, ömür boyu rahat ediyorsunuz.

Kapanış

Cloud Run + Cloud SQL ikilisi doğru kurulduğunda çok rahat çalışıyor; iş, ölçeklenmenin veritabanı tarafında yarattığı baskıyı ciddiye almakta. Bana sorarsanız küçük pool, sıkı max-instances ve Secret Manager üçlüsü neredeyse her senaryoya yetiyor; gerisi konfor. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.