İngilizce versiyonu için;
Achieving Zero Downtime: Azure App Service Deployment using Azure DevOps and Deployment SlotsZero downtime ile Deployment: Azure DevOps Deployment Slots Kullanarak Azure App Service Deployment işlemi
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.
- Uygulamanın Oluşturulması
- Azure DevOps Repo
- Azure DevOps build pipeline
- Azure App service
- 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.
Bu repo’yu bilgisayarımıza klonluyoruz, yazdığımız kodu bu repo klasörüne taşıyoruz ve kodları origin’e iletiyoruz.
git add .
git commit -m "inital commit"
git push origin
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.
Şimdi Azure Container Registry’de image deployment işlemimizin başarılı olduğunu kontrol ediyoruz.
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.
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.
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.
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.
Ş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.
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.