Spring Boot Deployment

Multi-module Spring Boot projesini Docker ve GHCR ile EC2'ye CI/CD kurmak

Dört servisli bir yapıda gateway dahil tüm modülleri tek repoda tutup, Docker image üretip GitHub Container Registry'ye push eden ve AWS EC2 üzerinde güvenli şekilde ayağa kaldıran akışı sıfırdan anlatan pratik rehber.

EDGE
api-gateway

Spring Cloud Gateway ile dış dünyadan gelen istekleri route eden giriş kapısı.

CORE
user-service

Kullanıcı, auth, profil ve temel account akışları.

CORE
order-service

Sipariş, iş kuralları ve transaction tarafı.

CORE
catalog-service

Ürün veya domain verisinin okunması ve sorgulanması.

Repo
multi-module Dockerfile compose
CI
build test push ghcr.io
CD
ssh ec2 docker login docker compose up

1. Kurmak istediğimiz sistem: tek repo, dört servis, bir gateway, tek EC2 hedefi

Bu senaryoda elimizde tek bir GitHub reposu var. Repo içinde dört servis tutuyoruz: `api-gateway`, `user-service`, `order-service`, `catalog-service`. İstekler dışarıdan önce gateway'e geliyor, oradan iç servislere yönleniyor.

CI/CD hedefimiz şu: GitHub'a push yaptığımızda servisler build edilsin, image'lar `ghcr.io` altına gitsin, sonra EC2 makinesinde yeni image'lar çekilip Docker Compose ile servisler ayağa kalksın.

Neden tek repo?

  • Servisler arası ortak versiyon yönetimi kolaylaşır.
  • Gateway ve backend modüllerini birlikte değiştirmek pratiktir.
  • Ortak library, DTO ve build mantığı paylaşılabilir.

Neden EC2?

  • Basit ve doğrudan kontrol sağlar.
  • Kubernetes kadar ağır değildir.
  • Küçük ve orta ölçekli projede maliyet yönetimi nettir.

2. Multi-module Spring Boot yapısı nasıl kurulur?

Spring'in multi-module yaklaşımında kökte bir parent proje bulunur; servisler alt modül olarak tanımlanır. Ortak kod gerekiyorsa ayrıca `common` gibi bir modül açılır. Burada her servis ayrı çalıştırılabilir Spring Boot uygulamasıdır.

Örnek klasör yapısıMaven
commerce-platform/
├── pom.xml
├── common/
│   └── pom.xml
├── api-gateway/
│   └── pom.xml
├── user-service/
│   └── pom.xml
├── order-service/
│   └── pom.xml
└── catalog-service/
    └── pom.xml
Root pom.xmlAggregator
<project>
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.5</version>
    </parent>

    <groupId>com.dogankaya</groupId>
    <artifactId>commerce-platform</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <modules>
        <module>common</module>
        <module>api-gateway</module>
        <module>user-service</module>
        <module>order-service</module>
        <module>catalog-service</module>
    </modules>
</project>
Önemli fikir

Root `pom.xml` uygulama değil, yöneticidir. Asıl runnable Spring Boot uygulamaları alt modüllerdir. Her servis kendi `spring-boot-starter-web` veya gateway dependency setine sahip olur.

Gateway bağımlılığıapi-gateway/pom.xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>

Spring Cloud Gateway, Spring Framework 7 ve Spring Boot 4 tabanlı route, security, monitoring ve resiliency katmanını gateway üzerinde toplamak için uygundur. Böylece iç servislerde her yerde aynı cross-cutting işi tekrar tekrar yazmazsın.

3. Docker tarafı: her servis için image üretmek ve Compose ile bir araya getirmek

Her Spring Boot servisi için ayrı bir image üreteceğiz. En temiz yaklaşım her modülün kendi Dockerfile dosyasına sahip olmasıdır. Build sonrası çıkan jar dosyası image içine kopyalanır ve servis tek bir `ENTRYPOINT` ile ayağa kalkar.

user-service/DockerfileTemel image
FROM eclipse-temurin:21-jre

WORKDIR /app

COPY target/user-service-1.0.0.jar app.jar

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "/app/app.jar"]
docker-compose.prod.ymlEC2 üzerinde çalışma
services:
  api-gateway:
    image: ghcr.io/kayadogan1/commerce-platform-api-gateway:latest
    ports:
      - "80:8080"
    depends_on:
      - user-service
      - order-service
      - catalog-service

  user-service:
    image: ghcr.io/kayadogan1/commerce-platform-user-service:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod

  order-service:
    image: ghcr.io/kayadogan1/commerce-platform-order-service:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod

  catalog-service:
    image: ghcr.io/kayadogan1/commerce-platform-catalog-service:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod
Secret kuralı

`application-prod.yml`, `.env` veya compose içinde asla gerçek AWS access key, database password veya GitHub token yazma. Bu yazıda da özellikle placeholder isimler kullanıyoruz: `AWS_ACCESS_KEY_ID`, `DB_PASSWORD`, `GHCR_PAT` gibi.

4. GitHub Container Registry neden kullanılır ve nasıl etiketlenir?

GitHub'ın eski Docker registry yapısı yerini `ghcr.io` tabanlı Container Registry'ye bıraktı. Burada her servis image'ını GitHub repo ile ilişkili şekilde saklayabilir, GitHub Actions içinden `GITHUB_TOKEN` ile publish edebilirsin.

Image isimlendirme örneğiGHCR
ghcr.io/kayadogan1/commerce-platform-api-gateway:latest
ghcr.io/kayadogan1/commerce-platform-user-service:latest
ghcr.io/kayadogan1/commerce-platform-order-service:latest
ghcr.io/kayadogan1/commerce-platform-catalog-service:latest
Repo ile package ilişkisinin kurulması GitHub dokümanlarında önerildiği gibi image'ı repo ile bağlamak önemlidir; aksi halde workflow erişim izinleri karışabilir.
`GITHUB_TOKEN` tercih et Aynı repo içinden build/push yapıyorsan çoğu durumda personal token yerine workflow token kullanmak daha doğru ve daha güvenlidir.

5. AWS EC2 hazırlığı: Ubuntu, Docker kurulumu ve deploy kullanıcı düzeni

EC2 üzerinde en pratik yaklaşım Ubuntu tabanlı bir instance açmak, Docker Engine kurmak ve uygulama klasörünü tek bir deploy kullanıcısı altında yönetmektir. Burada asıl önemli nokta şudur: server'a girebilmek için SSH key kullanırsın, ama workflow içine hiçbir zaman gerçek private key'i dosya olarak commit etmezsin.

Docker kurulumuUbuntu EC2
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo ${UBUNTU_CODENAME:-$VERSION_CODENAME}) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Sunucu klasör yapısı/opt/apps
/opt/apps/commerce-platform/
├── docker-compose.prod.yml
├── .env
└── deploy.sh
Burada özellikle yapmıyoruz

AWS access key'i EC2 içine düz metin gömmüyoruz. EC2 zaten kendi rolüyle AWS servislerine erişecekse IAM role tercih edilir. GitHub Actions için de `EC2_HOST`, `EC2_USER`, `EC2_SSH_KEY` gibi secret alanları kullanılır; gerçek değer yazılmaz.

6. GitHub Actions CI/CD akışı: build, test, image push ve EC2 deploy

Pipeline iki mantık taşır. Birinci adım CI: Maven build ve test. İkinci adım CD: Docker image üret, GHCR'ye push et, sonra EC2'ye SSH ile bağlanıp yeni image'ları çek ve compose ile yeniden başlat.

.github/workflows/deploy.ymlÖrnek workflow
name: Build and Deploy

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - name: Build
        run: mvn clean verify

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push gateway
        uses: docker/build-push-action@v6
        with:
          context: ./api-gateway
          push: true
          tags: ghcr.io/kayadogan1/commerce-platform-api-gateway:latest

      - name: Build and push user-service
        uses: docker/build-push-action@v6
        with:
          context: ./user-service
          push: true
          tags: ghcr.io/kayadogan1/commerce-platform-user-service:latest

      - name: Build and push order-service
        uses: docker/build-push-action@v6
        with:
          context: ./order-service
          push: true
          tags: ghcr.io/kayadogan1/commerce-platform-order-service:latest

      - name: Build and push catalog-service
        uses: docker/build-push-action@v6
        with:
          context: ./catalog-service
          push: true
          tags: ghcr.io/kayadogan1/commerce-platform-catalog-service:latest

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd /opt/apps/commerce-platform
            echo "${{ secrets.GHCR_PULL_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose -f docker-compose.prod.yml pull
            docker compose -f docker-compose.prod.yml up -d
            docker image prune -f

CI tarafında

  • `mvn clean verify` ile testler çalışır.
  • Her servis için image build edilir.
  • Image'lar GHCR'ye `latest` veya SHA tag ile pushlanır.

CD tarafında

  • EC2'ye SSH ile bağlanılır.
  • Yeni image'lar `docker compose pull` ile çekilir.
  • Servisler `up -d` ile yeniden ayağa kaldırılır.

7. Secret ve güvenlik best practice'leri: key sızdırmadan deploy kurmak

Bu tip yazılarda en büyük risk, eğitim amaçlı örnek verirken gerçek secret gösterilmesidir. Burada özellikle tüm alanları placeholder bıraktık. Gerçek hayatta GitHub Actions `Secrets and variables` ekranını kullanırsın ve değerleri oradan çekersin.

Gerçek AWS key yazma Mümkünse EC2 için IAM role kullan. Uygulama içinden S3, SES, SQS erişimi gerekiyorsa makinenin rolüyle çöz.
SSH private key'i repoya koyma `EC2_SSH_KEY` sadece GitHub Secret olarak dursun; repo içinde dosya olarak tutulmasın.
GHCR pull için minimum izin Pull yapan token sadece `read:packages` yetkisine sahip olmalı.
`latest` yanında immutable tag kullan Sadece `latest` ile gitme; commit SHA veya release tag ekle ki rollback mümkün olsun.
En doğru sonraki adım

Bu akış ilk aşama için yeterli. Bir sonraki olgunlaştırma adımı health check, reverse proxy, zero-downtime rollout, image digest pinning ve environment bazlı compose override dosyaları olur.

Kaynakça

Bu yazıdaki multi-module yapı, gateway tercihi, GHCR davranışı, Docker kurulumu ve GitHub Actions deployment akışı aşağıdaki resmi dokümanlar temel alınarak derlendi.