Environment & Katmanlı Mimarı — Docker #S1B5

Container ayağa kaldırırken herhangi bir ortam belirlemezsek, container varsayılan olarak “Production” ortamda çalışacaktır.

Cihat Solak
lTunes Tribe
6 min readDec 16, 2021

--

Volume — Docker #S1B4 bölümünden sonra monolitik mimaride projemizi nasıl dockerize ederiz? Nasıl environment değerler belirleriz? Konusu ele alalım. Monolitik mimariler birden fazla katman içeren (User Interface, Business, DataAccess, Entities vb.) tek bir kod tabanına sahip yapılardır.

.Net Framework projesini nasıl dockerize ederiz?

Daha önceki yazılarımda da ifade ettiğim üzere, container içerisinde projeyi çalıştırdığımız zaman varsayılan ortam “Production” dır. Bunu değiştirebilir miyiz? Tabii ki de Evet! Bunun için aşağıdaki Dockerfile dosyasını inceleyelim.

FROM mcr.microsoft.com/dotnet/sdk:5.0 as sdkbuild
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish NetCoreDockerizeWebApp.csproj -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=sdkbuild /app/out .
ENV ASPNETCORE_URLS="http://*:2500"
ENTRYPOINT ["dotnet", "NetCoreDockerizeWebApp.dll"]

.Net CLI dotnet restore ve dotnet publish komutlarını kullanabilmem için ilk olarak SDK image’ından faydalandım. Net CLI komutları SDK’ya ihtiyaç duyar, Runtime ile anlaşamazlar. Bu işlemleri tamamladıktan sonra yani CLI komutları ile işim bittikten sonra docker hub üzerinden Runtime image’ını pull ediyoruz. SDK image’ı ile publish ettiğim dosyaları yeni oluşturduğum Runtime image’ına kopyalıyoruz. Şimdi oluşturduğumuz Dockerfile dosyası ile container ayağa kaldıralım.

powershell — komut arayüzü ile environment belirlemek

Contanier run ederken --env parametresiyle beraber environment olarak “Development” belirledik. Container içerisinde çalışan projemizi görüntülediğimizde ise aşağıdaki gibi bizleri karşılamaktadır.

Environment — Development

Docker için ortamımızı “Development” belirledik. Eğer sabit değişkenlerimizi environment değerleri olarak belirlersek (örn: veri tabanı yolu) appsettings.{environmentName}.json içerisinde tekrar belirlememe ihtiyacımız kalmayacak. Tabii ki bunun birden fazla yolu var. Eğer container ayağa kaldırırken veri tabanı yolunu environment olarak belirlemek istersek docker run -p 5000:2000 --env ConnectionStrings:Default:'Production veri tabanı yolu' --env Username='Test UserName' --name envcontainer 5d6 komut şemasını kullanabiliriz. Birden fazla environment tanımı için container ayağa kaldırırken environment belirlemek meşakkatli bir iş olacağı için Dockerfile dosyası içerisinde environment değerleri belirlemek daha basit olacaktır. Ayrıca her container ayağa kaldırdığımda da tekrar tekrar environment değerlerini belirtmek zorunda kalmayacağım. Aşağıdaki Dockerfile dosyasında birden fazla environment değerleri belirledim.

FROM mcr.microsoft.com/dotnet/sdk:5.0 as sdkbuild
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish NetCoreDockerizeWebApp.csproj -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=sdkbuild /app/out .
ENV ASPNETCORE_URLS="http://*:2500"
ENV ASPNETCORE_ENVIRONMENT = "Production"
ENV ConnectionStrings:Default = "Production veri tabanı yolu"
ENV Username = "Test UserName"
ENTRYPOINT ["dotnet", "NetCoreDockerizeWebApp.dll"]
ConnectionStrings — Docker Environment

HomeController sınıfının index metodunda veri tabanı bağlantı yolunu yazarak yaptığımız işlemleri test edeceğim.

ConnectionStrings — Docker Environment
ConnectionStrings — Docker Environment

Katmanlı Mimari Projesi İle Dockerize

Container içerisinde local diye bir kavram bulunmamaktadır. Canlı ortam gibi düşünmeliyiz. Bundan mütevellit docker içerisinde çalıştırmak istediğimiz uygulamanın localden bağımsız olacağını bildiğimiz için örneğin proje veri tabanının uzak sunucuda olması gerektiğini bilmeli local veri tabanını görmeyeceğini de unutmamalıyız. Yok ben ille de local veri tabanım ile çalışacağım diyorsanız ve bu bir MSSQL olduğunu varsayarak konuşuyorum. MSSQL’i bir container içerisinde ayağa kaldırıp bu container’ın portunu da projemize bildiririz.

MSSQL sadece basit bir örnekti. NoSQL taraftarı arkadaşlar beni topa tutmazsa sevinirim. Gerçi geçenlerde Developer Days’de MongoDB’den bahsettik. Bunun şerefine MongoDB image’ini de linke bıraktım. İsterseniz MongoDB ile de çalışabilirsiniz.

FROM mcr.microsoft.com/dotnet/sdk:5.0 as sdkbuild
#image içerisine app isminde klasör oluşturuyorum.
WORKDIR /app
#layerları app klasörü içerisine kopyalıyorum
COPY ./NewsPortal.Core/*.csproj ./NewsPortal.Core/
COPY ./NewsPortal.Data/*.csproj ./NewsPortal.Data/
COPY ./NewsPortal.Service/*.csproj ./NewsPortal.Service/
COPY ./NewsPortal.Web/*.csproj ./NewsPortal.Web/
#sln dosyasını app klasörünün direkt içerisine kopyalıyorum
COPY *.sln .
#.csprojların paketlerini gerekirse ekleyip, güncelliyorum
RUN dotnet restore
#Kalan dosyaları app içerisine kopyalıyorum. (örneğin ana projedeki controller, wwwroot vb.)
COPY . .
#app klasörüyle aynı dizinde olan publish klasörüne publish et
RUN dotnet publish ./NewsPortal.Web/*.csproj -o /publish/
FROM mcr.microsoft.com/dotnet/runtime:5.0
WORKDIR /app
#sdkbuild image'inin içerisinde bulunan publish klasörünü app klasörünün içerisine kopyala
COPY --from=sdkbuild /publish .
ENV ASPNETCORE_URLS="http://*:3200"
ENV ASPNETCORE_ENVIRONMENT = "Production"
ENTRYPOINT ["dotnet", "NewsPortal.Web.dll"]

Katmanlı mimari de dockerize işlemi yapacağımızdan ötürü tüm projeleri katman katman COPY komutuyla belirleyeceğim klasörün içerine kopyaladım.

Peki neden katman katman kopyaladım?

Çünkü, her bir satır bir katman oluşturur. Örneğin projemde sadece servis katmanında bir değişiklik yaptığım zaman dockerize işleminde (bir image oluşturduğum zaman) CORE, DATA, WEB katmanlarında değişiklik olmadıysa docker bunları sıfırdan oluşturmak yerine cache üzerinden getirecektir. Bundan ötürü build işlemiyle beraber image oluşturma süresi de kısalacaktır.

Neden COPY . . demedikte projeleri tek tek app klasörü içerisine kopyaladık?

Çünkü bu komut tüm projeleri tek seferde tek bir katmana kopyalar ve 1 adet layer oluşturur. Bir adet layer oluşturmasının sonucunda eğer sadece CORE katmanında bir değişiklik yapıldıysa tüm proje için sıfırdan katman oluşturulması gerekecek yani her seferinde tüm projeyi image içerisine kopyalayacaktır. Fakat COPY . . yerine projeleri ayrı şekilde belirtirsem, service, data katmanında bir değişiklik olmadığı takdirde cache üzerinden image içerisine aktarım yapacaktır. Bu da bizim build süremizi ciddi şekilde düşürecektir.

Unit Test İçeren MVC Projesini Nasıl Dockerize Ederim?

Unit test projelerini dotnet cli üzerinden dotnet test komutuyla run edebilir, test sonucunu görüntüleyebiliriz. dotnet test komutunu Dockerfile içerisinde kullanarak image oluşturmadan yani publish etmeden önce test projesini çalıştıralım. Sonuç başarısız olursa bir sonraki adıma geçemeyeceğinden dolayı image’de oluşmaz. Bu bizim için avantajlıdır. Çünkü Dockerfile içerisinde image oluştururken testide dahil edersek (Dockerfile içerisinde RUN dotnet test komutunu kullanırsak) imagelarımız testten geçmiş olan projeleri içereceğinden hem güvenli hem de best practices açısından uygun yol olacaktır.

FROM mcr.microsoft.com/dotnet/sdk:5.0 as sdkbuild#image içerisine app isminde klasör oluşturuyorum.
WORKDIR /app
#tüm layerları app klasörü içerisinde, aynı isimde oluşturduğum klasörlere kopyalıyorum
COPY ./NewsPortal.Test/*.csproj ./NewsPortal.Test/
COPY ./NewsPortal.Core/*.csproj ./NewsPortal.Core/
COPY ./NewsPortal.Data/*.csproj ./NewsPortal.Data/
COPY ./NewsPortal.Service/*.csproj ./NewsPortal.Service/
COPY ./NewsPortal.Web/*.csproj ./NewsPortal.Web/
#sln dosyasını app klasörünün direkt içerisine kopyalıyorum
COPY *.sln .
#ilgili .csproj dosyalarının paketlerini ekleyip, güncelliyorum
RUN dotnet restore
#dizinde bulunan tüm dosyaları image içerisindeki app'e kopyalıyorum. (örneğin ana projedeki controller, wwwroot vb.)
COPY . .
#test projesinin csproj dosyasını bul ve çalıştır
RUN dotnet test ./NewsPortal.Test/*.csproj
#app klasörüyle aynı dizinde olan publish klasörüne publish et
RUN dotnet publish ./NewsPortal.Web -o /publish/
FROM mcr.microsoft.com/dotnet/runtime:5.0
WORKDIR /app
#sdkbuild image'inin içerisinde bulunan publish klasörünü app klasörünün içerisine kopyala
COPY --from=sdkbuild /publish .
ENV ASPNETCORE_URLS="http://*:2750"
ENTRYPOINT ["dotnet", "NewsPortal.Web.dll"]

Dockerfile içerisindeki “.” (Nokta), bulunduğu klasörü yani o anda çalıştığı klasörü ifade etmektedir.

Visual Studio Docker Ortamı

Docker CLI komutlarına geçmeden visual studio içerisinden container üzerinde işlemler (Silme, Durdurma, Ayağa kaldırma gibi.) yapabiliriz. Bunun yanı sıra yeni bir proje oluştururken “Enable Docker” onay kutusunu işaretlersek bizim için hazır Dockerfile ve içerisinde image oluşturmasıyla ilgili hazır kodları da barındıracaktır.

Ek Bilgiler — Docker Support

Enable Docker onay kutusunu işaretledikten sonra Docker OS üzerinden Linux & Windows container tercihini yapıyoruz.

Windows containerların image boyutları Linux containerlara göre büyüktür. Bu da bizim için bir dezavantajdır. Fakat cross platform uygulama geliştirmiyorsanız mutlaka windows container seçmelisiniz!

Projeyi oluşturduktan sonra hazır (ready) olana denk output penceresine göz atarsanız container tools içerisindeki Dockerfile ile yapılan işlemleri takip edebilirsiniz.

Ve bizlere hazır sunulan Dockerfile dosyasını incelediğimizde,

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"]
RUN dotnet restore "WebApplication1/WebApplication1.csproj"
COPY . .
WORKDIR "/src/WebApplication1"
RUN dotnet build "WebApplication1.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "WebApplication1.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication1.dll"

Expose genellikle containerların birbiri ile haberleşmesi için kullanılan komuttur. İki container ayakta ve birbiriyle haberleşmekteyse bu containerlar kendi portlarını dışarı açarlar. Örneğin yukarıdaki Dockerfile dosyasında uygulama 80 ve 443 portunu dışarı açtığı için bir başka container 80 veya 403 portu üzerinden buradaki containerla haberleşebilir.

Gerçek Hayat Senaryosu: Microservis mimarisi üzerine düşünelim. İki adet containera sahibiz ve bunları ayağı kaldırıyoruz. Bir containerda Net Core projemiz diğerindeyse Redis Cache bulunuyor. Net Core projesinin bulunduğu container’ın Redis Cache’in bulunduğu container’a erişebilmesi için redis container’ın kendi portunu expose etmesi gereklidir. expose komutu containerların kendi içlerinde haberleşmesi için kullanılan portlardır. Yani 443 ya da 80 dediğimde benim uygulamam 403 ya da 80 portundan kalkacak anlamına gelmemektedir.

--

--