Docker Multistage Build Örneği: Caddy Web Sunucusunun Dockerlaştırılması

Docker’ın şu an(Haziran 2017) sadece son birkaç CE Edge sürümünde yer alan (Güncelleme) 17.06 CE’den itibaren stabil olarak açıklanan multistage build özelliği, imaj boyutlarının küçülmesi ve build işleminin kısalması/optimizasyonu için kolaylık  sağlıyor. Bu yazıda, Caddy Web sunucusunun Docker imajını, eski ve yeni yöntemler ile gerçekleştirip, multistage build kavramını karşılaştırmalı olarak inceleyeceğiz.

1. Giriş

Caddy Web sunucusu, Go diliyle yazılan, TLS aktif olarak gelen, Let’s Encrypt üzerinden sertifikası olmayan alan adları için otomatik sertifika alan, HTTP/2 desteği direkt olarak aktif ve şu anlık (Haziran 2017) deneysel QUIC desteği ile oldukça ilgi çekici bir Web sunucusu. Caddy aynı zamanda Apache-2.0 lisansına sahip.

Caddy sunucusuna ayar dosyası olarak Caddyfile isminde bir dosya sağlıyoruz. Bu dosyanın formatı olabildiğince basit tutulmaya çalışılmış. Aslında hiçbir ayar dosyası sağlamasak bile, bulunduğu dizindeki dosyaları sunmaya başlaması için sadece çalıştırmamız yeterli oluyor.

Caddy’nin özellikleri konumuzun kapsamı dışında olduğu için, kısaca bahsettik. Bunlardan birkaç tanesi birazdan deneme yaparken işimize yarayacak.

2. Ön Bilgi

Öncelikle, bu yazının amacının Docker’daki multistage build olduğunu tekrarlayalım. Caddy’nin web sitesi üzerinden derlenmiş halde dağıtımı yapılıyor. Bu şekilde indirip direkt olarak çalıştırılabilir bir haline erişmiş oluyorsunuz. Haliyle, ilk olarak bu şekilde nasıl yapacağımızdan bahsedelim ve karşılaşılabilecek bazı sorunlara bakalım.

3. Derlenmiş, Çalıştırılabilir Caddy’nin İndirilmesi

Bir web sitesinden nasıl program indirilebileceğinden bahsetmekten ziyade, bu başlıkta Caddy’nin indirme ekranı seçeneklerinden bahsedeceğiz. Caddy’nin web sitesine https://caddyserver.com adresinden ulaşabilirsiniz. İndirme sayfasında, bize kullanacağımız mimariyi, işletim sistemini ve eklentileri seçme şansı veriyor. Öyle ki Raspberry Pi’de çalıştırabileceğimiz versiyonu bile mevcut.

Bu sayfada önemli olan, seçim yaparken kendi bilgisayarınızın mimarisi için değil, Docker konteynırlarının çalışacağı mimariye göre seçim yapmak. Bu yüzden şu an Linux 64-bit yazılı olanı indirmemiz gerekiyor. Bunu yaparken, indirmek için kullandığınız linki de bir kenara not edin. Bende gözüken haliyle(ki muhtemelen sizde de aynı olacak), şöyle bir link:

https://caddyserver.com/download/linux/amd64

Not: Mac OS kullanıcılarının bu kısımda dikkatli olmasını öneririm, zira kendi bilgisayarlarında doğrudan çalıştırdıkları ikili dosyaları indirirlerse, Linux konteynırlarında bu dosyalar çalışmayacak. Bu yüzden platformu doğru seçtiğinizden emin olun.

Şu anlık bir eklenti kullanma planımız yok. İhtiyacınız olanları dokümantasyonlarına ve açıklamalarına bularak kolayca ekleyebilirsiniz. Şimdilik bu kısımla zaman kaybetmeden, herhangi bir eklenti olmadan ilerleyelim ve dosyayı indirelim.

İndirdiğiniz dosya gzip ile sıkıştırılmış tar arşivi(tar.gz) formatında, haliyle açarak başlamak gerekiyor. Dosyayı açınca içerisinde caddy adında çalıştırılabilir bir dosya bulacaksınız. Bu dosya, Go programlama dilinde yazılmış Caddy web sunucusunun statik linklenmiş versiyonunu içeriyor. Bunun bize sağladığı avantaj ise son durumda elde edeceğimiz imajın bağımlılıkları az oluyor ve haliyle imaj boyutu küçük oluyor. Bu dosyanın çalıştırılabilir olduğundan emin olalım. Aslında tüm bunlar için baştan sona şunları yazmak yeterli:

$ wget https://caddyserver.com/download/linux/amd64
$ tar xzf amd64
$ ls -l caddy
-rwxr-xr-x  1 guray  guray  16235640 25 May 08:10 caddy

Çalıştırılabilir olduğunu da bildiğimize göre, denemek için kendi bilgisayarımızda ufak bir sunucuyu hemen kurabiliriz. Bunun için bu dosyayı çalıştırmak yeterli:

root@84c6057a8605:/# ./caddy
Activating privacy features... done.
http://:2015

Bu noktadan sonra tarayıcınızdan localhost:2015 adresine girerek size bir 404 gelmesini beklemeniz gerekiyor. Veya hazır terminalde çalışırken curl ile deneyebiliriz:

$ curl localhost:2015
404 Not Found

Caddy varsayılanda bulunduğu dizini sunmaya başladığı için isterseniz hemen bir HTML dosyası oluşturup ona ulaşmaya çalışabilirsiniz. Bu, şu an konu dışında kaldığı için geçiyoruz.

Bu aşamaya kadar, normal şartlarda Caddy’i herhangi bir otomasyona tabii tutmadan indirdik. Artık devam edebiliriz.

4. Caddy’nin bir Docker konteynırında çalıştırılması – 1. Deneme

Normal şartlarda yapılabilecek ilk işlem, bu indirilen programın bir konteynır içine aktarılarak çalıştırılması olarak akla gelebilir. Şimdi bunu deneyelim, çalıştıralım ve iyi/kötü yanlarına bakalım. Basit bir Dockerfile oluşturarak içerisine şunları yazalım:

$ cat Dockerfile
FROM ubuntu:latest
COPY ./caddy /caddy
ENTRYPOINT ["/caddy"]

Şimdi bu Dockerfile iles imajı oluşturalım:

$ docker build -t caddyv1:0.1 .
Sending build context to Docker daemon  108.1MB
Step 1/3 : FROM ubuntu:latest
 ---> d355ed3537e9
Step 2/3 : COPY caddy /caddy
 ---> Using cache
 ---> 2a455495b889
Step 3/3 : ENTRYPOINT ['/caddy']
 ---> Running in 86019059a70d
 ---> 980c504862c1
Removing intermediate container 86019059a70d
Successfully built 980c504862c1
Successfully tagged caddyv1:0.1

Oluşturduğumuz imajı çalıştırırsak, Caddy web sunucusunun başladığını görebiliriz:

$ docker run -it --rm -p 2015:2015 caddyv1:0.1
Activating privacy features... done.
http://:2015

Kontrol etmek için yine cURL kullanırsak(yapılan isteği ve gelen cevabı detaylıca görüntülemek için -v parametresini ekledim):

$ curl -v localhost:2015
* Rebuilt URL to: localhost:2015/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 2015 (#0)
> GET / HTTP/1.1
> Host: localhost:2015
> User-Agent: curl/7.51.0
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< Content-Type: text/plain; charset=utf-8
< Server: Caddy
< X-Content-Type-Options: nosniff
< Date: Tue, 27 Jun 2017 23:07:25 GMT
< Content-Length: 14
< 
404 Not Found
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact

Bu süreci uygulamanın güzel yanı; klasik, alışıldık ve kolay uygulanır bir yapıda olması. Altyapıya Ubuntu’nun son sürümünü ekleyerek işin içinden kolayca kurtulmamızı da sayabiliriz. Kötü yanlarına bakacak olursak; Caddy’i elle indirdik, imajı her oluşturduğumuzda aynı Caddy versiyonu kalıyor ve imaj boyutu büyük. Şöyle bir kıyaslama yapacak olursak, Caddy’nin statik linklenmiş halinin boyutu 15MB civarında:

$ ls -lh caddy 
-rwxr-xr-x@ 1 guray  staff    15M 18 Haz 04:57 caddy

Ama biz statik linkli bir uygulama çalıştırmamıza rağmen imaj boyutu 135MB:

$ docker image ls | grep caddyv1
caddyv1             0.2                 a2bb5be8c970        6 minutes ago       135MB

Haliyle durduk yere büyüyen imaj boyutlarıyla karşılaşıyoruz. Bu durum yerine göre çok sıkıntı olmayabiliyor ancak ufak uygulamaların büyük imaj boyutlarına sahip olması yine de birçok sebepten tercih edilecek bir yöntem değil. Bunu nasıl düzelteceğimize ilerki aşamalarda bakacağız.

5. Sürecin otomasyonu – 2. Deneme

Caddy için yaptığımız Docker imaj denemelerinin ilkinde, imaj boyutunun büyüklüğünden ve işlemin otomatik hale getirilmemesinden şikayet ettik. Şimdi sürecin otomasyonu için yeni bir Dockerfile hazırlayalım:

FROM ubuntu:latest

WORKDIR /
RUN apt update && apt install wget -y && wget https://caddyserver.com/download/linux/amd64 && tar xzf amd64

ENTRYPOINT ["/caddy"]

İmajı oluşturalım:

docker build -t caddyv2:0.1 .

Şimdi bu imajın boyutuna bakalım:

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
caddyv2               0.1              d3222151ee9c        4 minutes ago       186MB

En başta karşımızda olan sorun büyüdü ve 186 MB oldu. Caddy’nin güncel sürümünü edinebilmek için sürekli tekrardan indiriyoruz ama boyut konusunda bize zararı oluyor.

6. İmaj boyutunun düşürülmesi

İmaj boyutunu düşürebilmek için, Dockerfile oluştururken genellikle yapılan işlemlerden biri olan, apt’ın güncelleme sırasında indirdiği repo verilerini silmek olsa da, bizim senaryomuzda işi olabildiğince küçültmek var. Bunu yapmak içinse, hazır elimizde statik linklenmiş bir program varken, kullandığımız base imajını değiştirerek daha ufak bir tane seçebiliriz. Ufak denildiğinde akla çoğumuzda ilk Alpine Linux geliyor ancak aslında daha da küçük olan scratch imajını baz alacağız. Yalnız bu baz imajının içerisinde bir paket yöneticisi, paket güncelleyici veya paket yönetici yardımcı araçları bulunmayacağı için, Caddy’yi indirme işlemini direkt olarak bu imajı baz alarak yapamıyoruz.

Bir önceki aşamada gördüğümüz, güncel paketin indirilmesini içeren aşamayı şimdilik bir kenara bırakıp boyut düşürmeyle ilgilenelim. En başta indirdiğimiz caddy ikili dosyasını kopyalayarak çalıştıracak bir Dockerfile oluşturalım:

FROM scratch
COPY ./caddy /caddy
CMD ["/caddy", "-quic"]

Şimdi bu dosyadan imajı oluşturursak:

docker build -t caddyv3:0.1 .

Yeni imajın boyutuna bakacak olursak:

$ docker image ls | grep caddyv3
caddyv3                0.1                 9a75eac60099        8 days ago          16.3MB

İmajın boyu 16.3 MB’ye indi. 16MB’lik Caddy ile birlikte düşünürsek oldukça düşük boyutlarda imajı oluşturmuş olduk.

7. Gözden Kaçanlar

Bu kısma kadar 2 farklı senaryonun birinde imajın oluşturulması sürecine uygulamanın güncel halinin alınmasını dahil ettik, ancak bu senaryoda boyut büyük oldu. Diğer senaryoda ise boyutu küçültürken uygulamanın son halini otomatik olarak alamaz durumda bir Dockerfile elde ettik.

Bu iki sebep zaten bildiklerimizdi. Ancak esas sorun, çalışacak programın başka programlara/dosyalara ihtiyacı olduğunda ortaya çıkıyor. Bunun en basit örneği ise, Caddy web sunucusunu vekil sunucu olarak kullanırken ortaya çıkıyor. Eğer arka tarafta yer alan uygulama sunucularına erişim TLS üzerinden yapılıyorsa, Caddy’nin de kök sertifikalara erişebilmesi gerekiyor, aksi takdirde bağlantıyı doğrulayamadığı için düzgün çalışamıyor.

Eğer kendi imzaladığınız(self-signed) sertifikalarınız yoksa, dağıtımlarla birlikte gelen kök sertifika paketindeki dosyalara ihtiyacınız olacak. Aslında bu da, yukarıda tanımladığımız ancak henüz birleştirme işlemini yapmadığımız iki aşamaya yeni bir tane daha sorun/çözüm ikilisi ekliyor.

8. Eski yöntemle çözüm

Sorunların sayısını 3 tane ana durumda toparladık. Şimdi bunları eski yöntemle tek seferde nasıl çözebileceğimize bakalım:

  • Bir Dockerfile üzerinde Caddy web sunucusunun güncel sürümü indirilir ve çalıştığı makineye bir Volume üzerinden aktarılır. (terminalden de yapılabilir)
  • TLS sertifikaları örneğindeki gibi, gerekli diğer dosyalar bir container üzerinde üretilir/indirilir ve ana makineye yine bir Volume üzerinden(veya alternatif yöntemlerle) aktarılır.
  • İlk iki aşamada aktarılan dosyalar düşük boyutlu bir base imaja eklenerek çalışabilir bir imaj oluşturulur.

Bu 3 temel aşama elle uygularken bizi zorlamaya çok müsait duruyor. Bizler de tabii ki bunu tek bir çalıştırmada sıra sıra yapacak Bash scriptleri yazabileceğimiz için, aklımıza ilk çözüm olarak bu aşamaları bir script içerisinde toparlamak geliyor. Yalnız her proje için bu scriptlerde farklılıklar olabileceği gibi, Dockerfile’lar üzerinde olabilecek değişiklikler de yine scriptlere her an yansımaya aday durumdalar. Yani bu haliyle yapı biraz kırılgan ve zorlayıcı duruyor. İşte bu aşamada devreye esas kahramanımız olan multistage build özelliği geliyor.

9. Yeni yöntemle çözüm

Multistage build, bir imajda kullanılacak programlar/dosyalar, uzun derleme süreçleri ve büyük boyutlu araçlarla üretiliyorsa bu süreci uygulama konteynırı yerine, geçici bir konteynırda tutma mantığını uyguluyor. Aslında bu şekilde bakınca eski yöntemden bir farkı gözükmüyor. Yalnız bunları aşama aşama scriptler üzerinden yaparak hata ayıklamak yerine, tek bir Dockerfile üzerinde yapmaya imkan sağlıyor. Lafı uzatmadan Dockerfile örneğini gösterip kod üzerinden açıklamaya devam edelim:

FROM golang:1.8 as build
RUN go get -d github.com/mholt/caddy/caddy && cd /go/src/github.com/mholt/caddy/caddy && CGO_ENABLED=0 ./build.bash 

FROM ubuntu as cert
RUN apt-get update && apt-get install ca-certificates -y

FROM scratch
WORKDIR /
COPY --from=build /go/src/github.com/mholt/caddy/caddy/caddy /
COPY --from=cert /etc/ssl/certs /etc/ssl/certs

CMD ["/caddy", "-quic"]

Bu dosyayı FROM ile başlayan satırlarla gruplara ayırırsak, elimizde 3 farklı grup kalıyor. FROM ile başlayan satırların sonunda karşımıza çıkan yeni yazım ise as İSİM şeklinde. Mesela build isimli aşamadan elde edilen imajda üretilen bir dosyayı kullanmak istediğimizde direk kopyalayıp kullanabiliyoruz.

İkinci kısımda ise, serfitikaların indirilmesini kolaylaştırması için(aslında bu uygulamada, örnek olması için) boş bir ubuntu imajında sertifikalar yükleniyor/güncelleniyor. Bunu son kısımda sertifikaları kopyalarken alınacak yer olarak söyleyeceğiz.

Son olarak, ilk 2 kısımdaki as İSİM şeklinde yer alan imaj oluştururken elde edilen sonuçlardan istenen dosyalar COPY komutuna –from=İSİM şeklinde verilerek ilgili kısımdan istenen dosyaların alınması sağlanıyor. İlk COPY komutu, derlenen Caddy ikili dosyasını alırken, ikinci COPY komutu sertifika otoritelerinin sertifikalarını alarak ilerliyor. Böylece istediğimiz kısımlarda içerikleri oluşturup sadece gerektiği kadarını tek bir imajda toplayabiliyoruz.

Şimdi oluşturduğumuz yeni Dockerfile’ı imaj haline getirelim:

docker build  -t caddyv4:0.1 .

Boyutu da hala küçük kalmış:

$ docker image ls | grep v4
caddyv4                0.1                 b38e4949b3d5        About a minute ago   17.1MB

10. Sonuç

Bahsettiğimiz 3 aşamanın her birine stage deniyor. Normalde bir Dockerfile içerisinde bu şekilde tek aşama varken buna single-stage build derken, yeni haline de multi-stage build ismi veriliyor.

Not 1: Bu örnekte, uygulamanın derlenme aşamasını da Docker’a aktarmış olduk. Bu sayede güncel veya istenen etiketlere sahip Git branchları üzerinden test çalıştırarak başlatılan bir derleme süreci, paketlemenin sonunda oluşan imajda bu araçların hiçbiri olmadan temiz ve sade bir halde elde edilebilir.

Not 2: Bu yazıda, Go programlama dilinde yazılmış Caddy web sunucusunu özellikle seçtim, statik bağlanmış bir programın Docker tarafındaki uygulamasını görme ve kendi dokümantasyonunda yazılandan farklı mantıktaki bir uygulamayı bu kısma nasıl aktarabileceğimizi göstermeye çalıştım. Farklı bir örnek için kendi dokümantasyonuna da bakmanızı öneririm.

Sorularınızı yorum kısmından veya iletişim sayfasındaki e-posta adresi üzerinden iletebilirsin.

 
comments powered by Disqus