ASP.NET Core Serisi 03: RESTful API’ı Containerize Edip Azure Container Service ile Kubernetes’e Deploy Etmek

Merhaba .NET Core severler.

Daha önce ASP.NET Core serisinin 1. bölümünde bir RESTful API geliştirip, Azure App Services’e deploy işlemini gerçekleştirmiştik. Şimdi bu 3. bölümünde ise, daha önce geliştirmiş olduğumuz bu RESTful API‘ı, nasıl containerize edebileceğimizi ve ardından Azure Container Service ile Kubernetes‘e nasıl deploy edebiliriz gibi konulara, çalışmakta olduğum firmada .NET Core transformation sürecinde elde edebildiğim tecrübeler doğrultusunda değinmeye çalışacağım.

Konu başlıkları sırasıyla:

  1. Docker Image’i Oluşturma ve Docker Hub Üzerine Push Etmek
  2. Azure Container Service Kullanarak Kubernetes Cluster’ı Oluşturmak
    1. SSH RSA Public Key Oluşturma
    2. Service Principal Client ID and Secret Oluşturma
    3. Kubernetes Cluster’ı Oluşturma
    4. Kubernetes Cluster’ına Bağlanmak
    5. Kubernetes Cluster’ı İçerisindeki Bir Pod’da ASP.NET Core RESTful API’ını Çalıştırma
    6. Kubernetes Service ile Pod’u Expose Etmek
    7. Kubernetes Deployment Controller’ı Oluşturma ve Expose Etme

NOT: Bu makale içerisinde detaylı olarak Docker nedir, neden ihtiyaç duyarız gibi konulara değinmeyeceğim. Bu makale için Docker hakkında biraz bilgiye sahip olmanız gerekmektedir.

Bildiğimiz gibi container image’i lightweightstand-alone ve her an çalışmaya hazır application parçalarıdır. Container’lar ile uygulamalarımız, aynı infrastructure altında birbirlerinden izole bir şekilde  çalışabilmektedir. Böylece development ve staging ortam farklılıkları birbirlerinden ayrılabileceği için, oluşabilecek conflict’lerin azaltılmasına da yardımcı olmaktadır.

Containerizing işlemi için “ASP.NET Core Serisi 01: Dapper ile RESTful API Tasarlama ve Azure App Services’e Deploy” isimli makale içerisinde geliştirdiğimiz örnek projeyi kullanacağız. Örnek projeyi GitHub üzerinden download edelim ve masaüstüne unzip işlemini gerçekleştirelim.

1. Bölüm – Docker Image’i Oluşturma ve Docker Hub Üzerine Push Etmek

NOT: Gerçekleştireceğim işlemler için, Windows platformu üzerinde DockerToolbox kullanmaktayım.

Bu aşamada ilk olarak, bir image build edebilmemiz için unzip yaptığımız projenin root dizininde, aşağıdaki gibi bir Dockerfile oluşturalım.

FROM microsoft/aspnetcore:1.1
ENTRYPOINT ["dotnet", "aspnetcore-rest-api-with-dapper.dll"]
ARG source=.
WORKDIR /app
EXPOSE 5000
COPY $source .

Dockerfile içerisinde base olarak offical compiled ASP.NET Core image’ini kullanacağımızı ve “5000” port’u üzerinden ise expose edeceğimizi tanımladık. Entrypoint olarak ise, projeyi publish ettikten sonra elde edecek olduğumuz “aspnetcore-rest-api-with-dapper.dll” dosyasını belirttik.  Şimdi projenin root dizinine herhangi bir command tool’u ile girelim ve projeyi aşağıdaki gibi build edelim.

dotnet build

NOT: “dotnet restore” komutu ile package’ları restore etmeyi unutmayın.

Build işleminin başarıyla gerçekleşmesinden sonra, aşağıdaki komutu kullanarak projeyi publish edelim.

dotnet publish -c release

Artık, publish işleminin ardından Dockerfile’ı kullanarak bir image build edebiliriz. Image build edebilmemiz için aşağıdaki komutu çalıştırmamız yeterli olacaktır.

docker build bin\Release\netcoreapp1.1\publish -t aspnetcorerestapionlinux

Publish sonrası elde ettiğimiz “bin\Release\netcoreapp1.1\publish” path’ini kullanarak, “aspnetcorerestapionlinux” isminde bir image build ettik. Aşağıdaki komutu kullanarak, oluşturulan image’leri görebiliriz.

docker images

Yukarıdaki resimde, “aspnetcorerestapionlinux” ismi ile image’in oluştuğunu görebiliriz.

Şimdi, yaptığımız işlemlerin doğru gittiğinden emin olabilmek için oluşturmuş olduğumuz image’i, bir container içerisinde aşağıdaki gibi çalıştıralım.

docker run -d -p 5000:5000 aspnetcorerestapionlinux

Oluşturmuş olduğumuz “aspnetcorerestapionlinux” image’ini, bir container içerisinde “5000” port’u üzerinden expose ederek çalıştırdık. Şimdi browser üzerinden “http://192.168.99.100:5000/swagger” URL’ine girelim ve projenin çalıştığından emin olalım.

Tadaa! Oluşturmuş olduğumuz image, container içerisinde başarılı bir şekilde çalışmaktadır. Buraya kadar olan kısımda, ASP.NET Core ile geliştirmiş olduğumuz RESTful bir API’ın containerizing işlemini tamamladık.

Şimdi ise oluşturmuş olduğumuz bu “aspnetcorerestapionlinux” image’ini, Docker Hub üzerine push edeceğiz. Bu sayede Azure Container Service içerisinde bu image’i pull ederek, Kubernetes cluster’ı içerisinde çalıştıracağız. Öncelikle push işlemini gerçekleştirebilmemiz için, Docker Hub account’ına sahip olmamız gerekmektedir. Eğer account’a sahip değilsek, buradan oluşturabiliriz.

İlk olarak oluşturmuş olduğumuz image’e, aşağıdaki gibi bir tag eklememiz gerekmektedir.

docker tag aspnetcorerestapionlinux gokgokalp/aspnetcorerestapionlinux

NOT: “gokgokalp” olan kısmı, kendi Docker Hub kullanıcı adınız ile değiştirmeyi unutmayın.

Şimdi ise Docker Hub üzerine login olmamız gerekmektedir.

docker login

Login olma işlemi başarıyla gerçekleştikten sonra aşağıdaki gibi oluşturmuş olduğumuz image’i, Docker Hub üzerine push edelim.

docker push gokgokalp/aspnetcorerestapionlinux

2. Bölüm – Azure Container Service Kullanarak Kubernetes Cluster’ı Oluşturmak

Azure Container Service, containerize hale getirdiğimiz uygulamalar için basit ve hızlı bir şekilde sanal makineler oluşturabilmemizi, yapılandırabilmemizi ve cluster yapımızı yönetebilmemizi kolaylaştırmak için çözümler sunmaktadır. Microsoft‘un bir kaç ay önce Kubernetes container orchestration service’ini de tamamen available hale getirmesiyle beraber, efficiently bir şekilde containerize edilmiş uygulamalarımızı deploy ve on the fly olarak scale edebilmek kolay bir hal almıştır.

Şimdi Azure Container Service‘ini kullanarak, basic bir şekilde Kubernetes cluster’ı oluşturacağız. Cluster oluşturabilmek için öncelikle aşağıdaki maddelere ihtiyaç duymaktayız:

  • Azure subscription – “ASP.NET Core Serisi 01” makalesinde, nasıl elde edeceğimizden bahsetmiştim.
  • SSH RSA public key – Virtual machine’lere authenticate olabilmek için oluşturmamız gerekmektedir.
  • Service Principal client ID and secret – Orchestrator olarak Kubernetes kullanacağımız için “Azure Active Directory service principal client ID” ye ve “secret” a ihtiyacımız var.

Haydi ozaman başlayalım.

2.1 SSH RSA Public Key Oluşturma

Windows üzerinde oluşturabilmek için, Git for Windows‘un kurulu olması gerekmektedir. Eğer kurulu değilse: https://git-for-windows.github.io/

Git Bash‘i açalım ve aşağıdaki komut satırı ile “openssl.exe” yi kullanarak, “myPrivateKey” ve “myCert” certificate’ini oluşturalım.

openssl.exe req -x509 -nodes -days 365 -newkey rsa:2048 \-keyout myPrivateKey.key -out myCert.pem

Bu işlem sırasında size “Country Name, State, E-mail, Organization Name” gibi bir kaç bilgi soracaktır. Gerekli bilgileri girdikten sonra, “myPrivateKey” ve “myCert” certificate’i, ilgili path altında oluşacaktır.

Şimdi ise aşağıdaki komutu kullanarak, oluşturmuş olduğumuz private key ile “myPublicKey” adında bir public key oluşturacağız.

openssl.exe rsa -pubout -in myPrivateKey.key -out myPublicKey.key

PuTTY SSH client’ını kullanabilmemiz için, ek bir key daha oluşturmamız gerekmektedir. Bunun için:ddddddd

openssl rsa -in ./myPrivateKey.key -out myPrivateKey_rsa

Bununla birlikte “myPrivateKey_rsa” isminde bir private key daha elde ettik.

Şimdi oluşturmuş olduğumuz bu “myPrivateKey_rsa” key’ini kullanarak, Azure portal üzerinde Linux VM yaratırken kullanacağımız public key’i oluşturacağız. Bunun için öncelikle buradanPuTTY ‘i indirelim ve kuralım. Kurulum işleminin ardından “PuTTYgen” uygulamalasını çalıştıralım ve “Load” kısmından oluşturmuş olduğumuz “myPrivateKey_rsa” key’ini seçelim.

Yukarıdaki resimde gördüğümüz gibi “ssh-rsa” prefix’i ile başlayan bir public key daha elde etmiş olduk. Makalenin ilerleyen bölümünde kullanabilmek için, bu key bilgisini de kaydedelim.

2.2 Service Principal Client ID and Secret Oluşturma

Bu noktada Azure AD (Active Directory) service principal oluşturma işlemini, portal üzerindeki terminal’den gerçekleştireceğiz. Azure portal‘a login olduktan sonra, sağ üst menüde bulunan “Cloud Shell” i açalım.

Cloud Shell” i açtıktan sonra, öncelikle aşağıdaki komut ile “aspnetcore-test-restapi” adında bir resource group oluşturalım.

az group create -n "aspnetcore-test-restapi" -l "westeurope"

Resource group’u oluşturmanın ardından, aşağıdaki komut içerisindeki “mySubscriptionID” bölümüne kendi subscription ID bilgimizi girelim çalıştıralım.

az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/mySubscriptionID/resourceGroups/aspnetcore-test-restapi"

Bu işlemin ardından aşağıdaki gibi bir output elde edeceğiz.

Buradaki “appId” yi principal client ID olarak, “password” u ise secret olarak kullanacağız.

2.3 Kubernetes Cluster’ı Oluşturma

  1. Azure portal üzerine login olalım.
  2. Sol üst menüden “New” kısmına tıklayalım.
  3. Açılan menüden aşağıdaki gibi “Containers > Azure Container Service” kısmını seçelim.

  4. Basics” kısmında, resource group altındaki “Use existing” butonuna tıklayalım ve daha önce oluşturmuş olduğumuz resource group olan “aspnetcore-test-restapi” ı seçelim ve diğer alanları da aşağıdaki gibi dolduralım. (Sizler de kendinize uygun doldurabilirsiniz)
  5. Şimdi ise “Master configuration” kısmından orchestrator gibi önemli bilgileri tanımlayacağız. “Orchestrator” olarak “Kubernetes” i seçelim ve ardından “DNS name prefix” gibi field’ları aşağıdaki gibi dolduralım.
    Devamında ise “SSH public key” ve “Service principal” bölümlerini yukarıda elde ettiğimiz bilgiler doğrultusunda tamamlayalım.
  6. Agent configuration” bölümü üzerinden de demo olduğu için “Agent count” olarak “1” ve “Standard DS2” seçiyorum.

Artık bir Kubernetes cluster’ına sahibiz.

2.4 Kubernetes Cluster’ına Bağlanmak

Local makinemizden Kubernetes cluster’ına bağlanabilmemiz için, “kubectl” i kurmamız gerekmektedir. Bunun için buradan, ilgili işletim sistemimize göre Azure CLI‘ı indirelim ve kuralım. Kurulum işleminin ardından, command tool açalım ve aşağıdaki komut ile “kubectl” in kurulumunu gerçekleştirelim.

az acs kubernetes install-cli

Şimdi ise Kubernetes cluster configuration bilgilerini download etmemiz gerekmektedir. Öncelikle aşağıdaki komut ile, portal bilgilerimizi kullanarak Azure üzerine login olalım.

az login

Login olduktan sonra, aşağıdaki komut üzerinde gerekli değişiklikleri yapalım ve çalıştıralım.

az acs kubernetes get-credentials --resource-group=aspnetcore-test-restapi --name=aspnetcore-container-service --ssh-key-file=C:\Users\GOKGOKALP\myPrivateKey_rsa

Cluster configuration bilgilerinin başarıyla download edildiğinden emin olabilmek için, aşağıdaki komutu da çalıştıralım ve node listesini görelim.

kubectl get nodes

2.5 Kubernetes Cluster’ı İçerisindeki Bir Pod’da ASP.NET Core RESTful API’ını Çalıştırma

Dokümantasyonunda da olduğu gibi, Pod’u tek bir cümle ile tanımlamak gerekirse eğer:

Kubernetes içerisindeki küçük, deployable computing birimleridir.

Pod, bir veya birden fazla application container’larını içerebilmektedir. Şimdi Kubernetes cluster’ı içerisinde bir pod oluşturabilmemiz için, projenin root klasörü altında “aspnetcore-rest-api-pod.yaml” adında aşağıdaki gibi bir YAML file’ı tanımlayalım.

apiVersion: v1
kind: Pod
metadata:
 name: aspnetcore-restapi
 labels:
   app: aspnetcore-restapi
spec:
 containers:
 - name: aspnetcore-container
   image: "gokgokalp/aspnetcorerestapionlinux"
   ports:
   - containerPort: 5000

Yukarıda tanımlamış olduğumuz YAML file’ı içerisindeki “image” kısmına, Docker repo’ya push yaptığımız image’i belirtiyoruz. Hatırlarsak API‘ı container içerisinde de “5000” port’u üzerinden expose etmiştik.

Şimdi aşağıdaki komutu, projenin root dizini altında command tool’u üzerinden çalıştıralım.

kubectl create -f aspnetcore-rest-api-pod.yaml

Gördüğümüz gibi pod’u başarıyla oluşturduk. Şimdi aşağıdaki komut ile oluşturmuş olduğumuz pod’a bir bakalım.,

kubectl get pods

Yukarıdaki resimde oluşturmuş olduğumuz “aspnetcore-restapi” pod’unun, “running” statüs’ünde çalıştığını görebiliriz. Bu pod içerisinde, demo projemiz olan ASP.NET Core RESTful API’ı çalışmaktadır. Artık yapmamız gereken tek şey, bu pod’u dışarıya Kubernetes Service ile expose etmek olacaktır.

2.6 Kubernetes Service ile Pod’u Expose Etmek

Kubernetes Service, logical pod’lar seti tanımlayabilmek ve policy’ler oluşturabilmek için olan bir abstraction’dır. Detaylı bilgiye ise, buradan ulaşabilirsiniz.

Şimdi Kubernetes Service için “aspnetcore-rest-api-ks.yaml” adında bir YAML file daha oluşturalım.

apiVersion: v1
kind: Service
metadata:
  name: aspnetcore-restapi-service
  labels:
   app: aspnetcore-restapi-service
spec:
  selector:
    app: aspnetcore-restapi
  ports:
    - port: 80
      targetPort: 5000
      protocol: TCP
  type: LoadBalancer

Yukarıdaki YAML file’ı ile “aspnetcore-restapi-service” adında, target TCP port’u “5000” olan bir Kubernetes Service‘i oluşturacağız. Ayrıca “selector” kısmında bulunan “app: aspnetcore-restapi” değerinin, oluşturmuş olduğumuz pod’un label’ı ile aynı olduğuna dikkat edelim. Şimdi service’i oluşturabilmek için, aşağıdaki komutu çalıştıralım.

kubectl create -f aspnetcore-rest-api-ks.yaml

Service oluşturma işleminin ardından, aşağıdaki komut ile de service listesine bir bakalım.

kubectl get services

Yukarıdaki resme dikkat edersek “aspnetcore-restapi-service” adlı service’in, otomatik olarak assign edilen “52.232.119.105” external IP‘si ile expose olduğunu görebiliriz.

NOTIP assign edilme işlemi, bir kaç dakika sürebilir.

Browser üzerinden eriştiğimizde ise, aşağıdaki gibi Swagger UI karşımıza gelecektir.

Şimdi geriye sadece Deployment Controller‘ı oluşturmak kaldı.

2.7 Kubernetes Deployment Controller’ı Oluşturma ve Expose Etme

Bu noktaya kadar Azure Container Service‘i kullanarak bir Kubernetes cluster’ı oluşturduk ve içerisinde RESTful API’ımızın container’ının çalıştığı bir pod tanımladık. Daha sonra bir Kubernetes Service‘i tanımlayarak, Load Balancer ile “80” port’u üzerinden dışarıya expose ettik.

Buraya kadar her şey yolunda. Eğer pod’larla ilgili buradan Durability of pods (or lack thereof)” başlığını okursak: pod’ların durable entities olmadıklarını ve scheduling failures, node failures gibi durumlarda yeniden kullanılamadıklarını görebiliriz. Buda demek oluyor ki ilgili pod öldüğünde, ilgili service ulaşılamaz olacaktır. Production ready olup bu gibi durumlara maruz kalmamamız için, Kubernetes Controller‘ları kullanmalıyız. Kubernetes içerisinde, Deployments ve Replication Controller gibi controller’lar mevcuttur. Biz burada Replication Controller‘ın yeni nesli olan Deployments‘ı kullanacağız. Deployments genel olarak self-healing, rollout management, scaling gibi önemli işlemleri sağlamaktadır.

Şimdi, aşağıdaki gibi “aspnetcore-rest-api-deployment.yaml” adında bir deployment YAML file’ı tanımlayalım.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: aspnetcore-restapi-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: aspnetcore-restapi
  template:
    metadata:
      labels:
        app: aspnetcore-restapi
    spec:
      containers:
      - name: aspnetcore-container
        image: "gokgokalp/aspnetcorerestapionlinux"
        ports:
        - containerPort: 5000

Dikkat edersek “replicas” bölümünü “3” olarak tanımladık ve ardından “containers” bölümü altında ise tıpkı pod oluşturma YAML file’ında olduğu gibi, ilgili image bilgilerini ve port bilgilerini belirttik. Artık bu template doğrultusunda Deployment, scheduling failures ve node failures gibi durumlar karşısında bizim için “3” adet replika’nın var olduğundan emin olacaktır.

Aşağıdaki komut ile deployment’ı oluşturmadan önce, daha önce oluşturmuş olduğumuz “aspnetcore-rest-api-pod.yaml” pod’unu ve “aspnetcore-rest-api-ks.yaml” service’ini silelim.

kubectl delete -f aspnetcore-rest-api-pod.yaml

kubectl delete -f aspnetcore-rest-api-ks.yaml

Şimdi deployment’ı oluşturabiliriz. Bunun için:

kubectl create -f aspnetcore-rest-api-deployment.yaml

ve deployment oluşmuş durumda. Hemen oluşan pod’lara bir bakalım:

kubectl get pods

Yukarıda gördüğümüz gibi deployment, 3 adet “running” statüs’ünde pod oluşturmuş durumdadır. Ayrıca deployment ile daha fazla yükü otomatik olarak scale edebilmekte mümkündür. Bunun için detaylı bilgiye, buradan erişebilirsiniz.

Şimdi ise geriye kalan tek şey, bir service oluşturarak bu 3 pod’u dışarıya tekrardan expose etmek olacaktır. Bu işlem için daha önce kullanmış olduğumuz “aspnetcore-rest-api-ks.yaml” file’ını tekrardan kullanacağız. Hatırlarsak deployment controller ile pod’ları oluşturabilmek için service’i silmiştik.

apiVersion: v1
kind: Service
metadata:
  name: aspnetcore-restapi-service
  labels:
   app: aspnetcore-restapi-service
spec:
  selector:
    app: aspnetcore-restapi
  ports:
    - port: 80
      targetPort: 5000
      protocol: TCP
  type: LoadBalancer

Bu service “app: aspnetcore-restapi” label’ına sahip olan 3 pod’u, “80” port’u üzerinden dışarıya expose edecektir.

kubectl create -f aspnetcore-rest-api-ks.yaml

Sonunda hazırız. 🙂 Yukarıdaki resimler görebildiğimiz gibi bu 3 pod’u dışarıya bir Load Balancer service’i ile expose ettik. Hangi endpoint’leri expose ettiğine bakmak gerekirse eğer, aşağıdaki komutu çalıştırmamız yeterli olacaktır.

kubectl describe services aspnetcore-restapi-service

Yukarıdaki komutu çalıştırmanın ardından Load Balancer service’inin “10.244.1.7:5000“, “10.244.1.8:5000” ve “10.244.1.9:5000” pod’larını “http://52.232.119.105” URL’i üzerinden expose ettiğini görebiliriz.

Biraz uzun bir makale oldu ama gerçekten zevkli ve önemli bir konu. Şuan çalışmakta olduğum firmada, bazı projeler için .NET Core transformation’ı ile birlikte Containerizing ve Kubernetes konuları üzerinde yoğun bir şekilde çalışmaktayım. Bu makale umarım ihtiyacı olanlara yardımcı olur.

Bir sonraki .NET Core makale serisinde ise, Jenkins kullanarak CI pipeline süreçlerine nasıl dahil edebiliriz konusunu ele almaya çalışacağım.

Takip de kalın!

https://github.com/GokGokalp/containerizing-and-kubernetes-sample-files

Bazı Referanslar ve Kaynaklar

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/container-service/container-service-tutorial-kubernetes-prepare-app.md
https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/container-service/container-service-tutorial-kubernetes-prepare-acr.md
https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/container-service/container-service-tutorial-kubernetes-deploy-cluster.md
https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/container-service/container-service-tutorial-kubernetes-deploy-application.md
https://www.docker.com/what-container
https://docs.microsoft.com/tr-tr/azure/container-service/container-service-intro
https://kubernetes.io/docs/concepts/overview/what-is-kubernetes
https://docs.microsoft.com/en-us/azure/container-service/container-service-deployment
https://docs.microsoft.com/en-us/azure/virtual-machines/linux/ssh-from-windows
https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough
http://www.c-sharpcorner.com/blogs/containerizing-a-net-core-application-using-docker-acs-and-kubernetespart-4
https://kubernetes.io/docs/concepts/services-networking/service/

Gökhan Gökalp

View Comments

Recent Posts

Event-Driven Architecture’larda Conditional Claim-Check Pattern’ı ile Event Boyut Sınırlarının Üstesinden Gelmek

{:en}In today’s technological age, we typically build our application solutions on event-driven architecture in order…

2 ay ago

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

9 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago