Selamlar! Azure App Service’te tek instance olarak çalışan uygulamamızı Azure DevOps işlem hatlarını kullanarak dağıtırken büyük olasılıkla birkaç saniyelik kesinti yaşanacaktır. Çünkü tek instance olarak çalışan uygulamanın yeni sürüme güncellenmesi için yeniden başlatılması gerekecektir. En kötü senaryoda, uygulama sürümlerimizin geçişinde bir hata oluşması durumunda geri alma nedeniyle kesinti süresi uzayacaktır.

Bu özel sorunu app servisi için deployment slot özelliğini kullanarak çözebiliriz. Bu özellikle, genellikle ayrı örnekler olarak aynı app servis planında çalışan iki farklı örnek, genellikle staging ve production uygulamaları, birbirinden ayrı örnekler olarak çalışır ve production ortamına geçiş yapıldığında app servisi tarafından değiştirme işlemi yönetilir ve kesinti yaşanmaz.

Bu yazıda, çok basit bir Dotnet Web API projesi oluşturacak, Azure DevOps Repository kullanarak barındıracak, Azure DevOps pipeline ile CI/CD pipeline oluşturarak Azure App Service’e dağıtacak ve deployment slot özelliği ile kesintisiz deployment yapacağız. Aşağıdaki adımları takip ederek süreci 5 adımda tamamlayacağız. Başlayalım.

  1. Uygulamanın Oluşturulması
  2. Azure DevOps Repo
  3. Azure DevOps build pipeline
  4. Azure App service
  5. Zero downtime testing

## Uygulamanın Oluşturulması

Deploy etmek için basit bir Dotnet Web API projesi oluşturuyoruz. Bunun için aşağıdaki komutları kullanıyoruz.

mkdir backend  
cd ./backend  
dotnet new sln -n Slot  
dotnet new webapi -n Slot.API  
dotnet sln add ./Slot.API/

properties/launchSetting.json dosyasındaki profiles.http özelliğini aşağıdaki gibi güncelliyoruz. Burada yalnızca applicationUrl ve launchBrowser özelliklerini güncelledik.

// ...  
  "profiles": {  
    "http": {  
      "commandName": "Project",  
      "dotnetRunMessages": true,  
      "launchBrowser": false,  
      "launchUrl": "swagger",  
      "applicationUrl": "http://localhost:5050",  
      "environmentVariables": {  
        "ASPNETCORE_ENVIRONMENT": "Development"  
      }  
    },  
// ...

Aşağıdaki güncellemeyi Program.cs dosyasında yaparak, uygulamanın erişilebilir ve /health uç noktasına yapılan isteklere yanıt verebileceğini test edeceğiz. Uygulamanızda bir veritabanı kullanıyorsanız, AddDbContextCheck yöntemi ile veritabanına erişimde bir sorun olup olmadığını da test edebilirsiniz.

var builder = WebApplication.CreateBuilder(args);  
  
// ...  
  
builder.Services.AddHealthChecks();  
  
var app = builder.Build();  
  
// ...  
  
// app.UseHttpsRedirection();  
  
app.MapControllers();  
  
app.UseHealthChecks("/health");  
  
app.Run();

Uygulamamız için bu kadar! Şimdi şu komutları çalıştırarak localhost:5050/health adresine erişebiliriz.

cd ./Slot.API  
dotnet run  
curl -4 http://localhost:5050/health  
# Healthy

Daha sonra uygulamamızı dağıtmak için Docker kullanacağız, bu yüzden Dockerfile dosyamızı sln dosyamızla aynı dizine koyuyoruz.

# Build Stage  
FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine AS base  
WORKDIR /app  
EXPOSE 8080  
  
  
# Publish Stage  
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build  
COPY ["Slot.API/Slot.API.csproj", "Slot.API/"]  
RUN dotnet restore "Slot.API/Slot.API.csproj"  
COPY . .  
WORKDIR "/Slot.API"  
RUN dotnet build "Slot.API.csproj" -c Release -o /app/build  
  
FROM build AS publish  
RUN dotnet publish "Slot.API.csproj" -c Release -o /app/publish /p:UseAppHost=false  
  
FROM base AS final  
WORKDIR /app  
COPY --from=publish /app/publish .  
ENV ASPNETCORE_URLS=http://*:8080  
ENV ASPNETCORE_ENVIRONMENT=Production  
ENTRYPOINT ["dotnet", "Slot.API.dll"]

Aşağıdaki iki komutu kullanarak imajı başarıyla oluşturup container’ı build edip çalıştırabiliriz.

docker build -t deployment-slots-demo .  
docker run -it -p 80:8080 deployment-slots-demo -n deployment-slots-demo-container

Uygulamanın Çalışıyor Olduğunu Doğrulayın

Önceki adımda sağlık kontrolü özelliği ile uygulamamızı oluşturduktan sonra, bu adrese her saniye bir istek yaparak uygulamanın erişilebilir olduğunu doğrulayabiliriz. Bu şekilde, Azure’a dağıtıldığında erişilebilir olduğunu doğrulamış olacağız.

Bu bölümde, uygulamanın her 50 milisaniyede bir istek yaparak erişilebilir olup olmadığını doğrulamak için basit bir Node.js script’i yazacağız. Bunun için aşağıdaki komutları kullanıyoruz.

mkdir health-check  
npm init -y  
touch index.js  
npm install node-fetch

Aşağıdaki kısmı package.json dosyasına ekliyoruz.

// ...  
"scripts": {  
  "start": "node index.js"  
},  
"type": "module",  
// ...

index.js dosyamızı aşağıdaki gibi oluşturabiliriz. Bu kod ile belirtilen url’e her 50 milisaniyede bir istek yapacak ve yanıtı konsola yazacaktır.

import fetch from "node-fetch";  
  
const check = async (url) => {  
  try {  
    const response = await fetch(url, {  
      method: "GET",  
    });  
    const result = await response.text();  
  
    if (result !== "Healthy") {  
      console.log(`${new Date().toISOString()}, ${url} result is not OK`);  
    }  
  } catch (error) {  
    console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`);  
  }  
};  
  
(() => {  
  setInterval(() => {  
    check("http://localhost:5050/health");  
  }, 50);  
  setInterval(() => {  
    check("http://localhost:5050/health");  
  }, 50);  
})();

API projemizde UseHttpsRedirection middleware’ini kapatmazsanız, geçersiz bir SSL sertifikası hatası alabilirsiniz. Bunu aşağıdaki gibi düzeltebilirsiniz.

import fetch from "node-fetch";  
import https from "https";  
  
const httpsAgent = new https.Agent({  
  rejectUnauthorized: false,  
});  
  
const check = async (url) => {  
  try {  
    const response = await fetch(url, {  
      method: "GET",  
      agent: httpsAgent,  
    });  
    const result = await response.text();  
  
    if (result !== "Healthy") {  
      console.log(`${new Date().toISOString()}, ${url} result is not OK`);  
    }  
  } catch (error) {  
    console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`);  
  }  
};  
  
(() => {  
  setInterval(() => {  
    check("https://localhost:5051/health");  
  }, 50);  
  setInterval(() => {  
    check("https://localhost:5051/health");  
  }, 50);  
})();

Azure DevOps Repo

Azure DevOps hesabımıza giriş yapıyoruz ve aşağıdaki gibi yeni bir repo oluşturuyoruz.

Azure deployment
Azure deployment

Bu repo’yu bilgisayarımıza klonluyoruz, yazdığımız kodu bu repo klasörüne taşıyoruz ve kodları origin’e iletiyoruz.

Azure deployment
git add .  
git commit -m "inital commit"  
git push origin
Azure deployment

Azure DevOps build pipeline

Pipeline ekranında, en sağ üstteki New pipeline düğmesine tıklıyoruz. Ardından aşağıdaki adımları izleyerek docker dosyasını oluşturup ve Azure Container Registry’e iteleriyoruz.

Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment

Şimdi Azure Container Registry’de image deployment işlemimizin başarılı olduğunu kontrol ediyoruz.

Azure deployment
Azure deployment
Azure deployment

Görüldüğü gibi, boru hattı çalıştırıldığında, Docker image’i başarıyla Azure Container Registry’de oluşturulmuş ve kullanılmak üzere bizi bekliyor. Bu ayarların ardından, main branch’de /backend dizininde yapılan her değişiklik ile tetiklenerek yeni bir Docker image’i oluşturulacak.

Azure App service

Azure App servisi, güvenlik, yük dengeleme, otomatik ölçeklendirme gibi özellikleriyle Azure tarafından yönetilen web uygulamalarını dağıtmamıza olanak tanır. Azure app servisleri ekranı üzerinden yeni bir app servisi oluşturabiliriz.

Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment

App servisini oluşturduktan sonra, Container Registry şifresini yapılandırma sekmesinden güncellemeniz gerekebilir.

## Azure DevOps release pipeline

release pipeline ile, build pipeline tarafından oluşturulan Docker image’i kullanarak App servisine deploy edilir ve uygulamamızı yayınlamış oluruz.

Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment

Staging dağıtımına girdiğimizde, staging versiyonumuz için dağıtım alırız ve Docker image’inde yaptığımız değişikliklerin canlı hale gelmesini sağlamak için uygulama servisimizi yeniden başlatırız.

Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment
Azure deployment

production aşamasında, app service’de bir deployment yapmıyoruz, bunun yerine staging aşamasıyla değişim işlemini gerçekleştiriyoruz ve bu şekilde, production erişiminde herhangi bir kesinti yaşamadan staging’de çalışan uygulamamızı production ile değiştirdiğimiz için production uygulamamıza erişebiliyoruz.

Azure deployment
Azure deployment
Azure deployment
Azure deployment

Şimdi, main bracnh’te yapılan herhangi bir değişiklik önce build pipeline (CI) ile geçecek, ardından release pipeline (CD) geçecek ve release, staging ortamına yapılacak. Production ortamına geçmek isterseniz, production deployment işlemi manuel olarak release ekranından tetiklenmelidir.

Azure deployment

Zero downtime testing

Uygulamamız için gerekli pipeline’ları başarıyla oluşturduk, bundan sonra release sırasında herhangi bir kesinti olup olmadığını test etmemiz gerekecek. Bunun için health-check/index.js dosyamızı aşağıdaki gibi güncelliyorum ve uygulamayı npm run start komutuyla çalıştırıp pipeline’ı tetikliyorum. Ardından, konsolda herhangi bir hata mesajı almadan, yani herhangi bir kesinti olmadan deployment sürecini tamamlıyorum!

import fetch from "node-fetch";  
  
const check = async (url) => {  
  try {  
    const response = await fetch(url, {  
      method: "GET",  
    });  
    const result = await response.text();  
  
    if (result !== "Healthy") {  
      console.log(`${new Date().toISOString()}, ${url} result is not OK`);  
    }  
  } catch (error) {  
    console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`);  
  }  
};  
  
(() => {  
  setInterval(() => {  
    check("https://deployment-slot-demo.azurewebsites.net/health");  
  }, 50);  
  setInterval(() => {  
    check("https://deployment-slot-demo-staging.azurewebsites.net/health");  
  }, 50);  
})();

Benzer bir işlemi deployment slots özelliği uygulamayan bir App Service ile denersek, aşağıdaki gibi bir hata alırız.

2023-11-14T12:28:39.506Z, [some-url]/health error is request to [some-url]/health failed, reason: connect ETIMEDOUT 100.100.100.100:443

Sonuç

Okuduğunuz için teşekkürler! 🎉 Yazılım geliştirme alanındaki araştırmalarımı kaçırmamak için @berkslv adresinden takipte kalabilirsiniz.