Genel Yazılım ve Sistem Mühendisliği

Belirli Bir Url e Gelen İsteklerin Farklı Bir Sunucudaki Uygulama Tarafından Karşılanması

Bazı durumlarda, alan adınız altında servis ettiğiniz belirli bir yola gelen isteklerin, güvenlik veya çeşitli farklı nedenlerle, farklı lokasyon/sunucuda çalıştırılan bir uygulamaya yönlendirilmesini; ancak bu işlemin url değişmeksizin gerçekleşmesini isteyebilirsiniz.

Şayet Microsoft teknolojileri kullanan bir platform üstünde çalışıyor ve NetScaler gibi bir cihazınız yoksa aşağıdaki işlem basamaklarını gerçekleştirerek bu işlemi IIS üzerinde kolayca gerçekleştirebilirsiniz.

* Aşağıdaki sayfayı ziyaret ederek Application Request Routing eklentisini IIS makinenizin üzerine kurun.
Application Request Routing

* Komut İstemi (Command Prompt) uygulamasını açarak aşağıdaki komutu çalıştırın. Bu işlem, isteği karşılayan IIS makinesindeki headerların, isteğin yönlendirileceği sunucu sistemine transfer edilebilmesi için gereklidir.

%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost

Alan adımızın ibrahimgunduz.net olduğunu ve http://www.ibrahimgunduz.net/ornek url ine gelen istekleri mysampleapp-13241324-aws.amazon.com/ gibi bir url e yönlendireceğimizi varsayalım.

* IIS yönetim penceresinin sol tarafındaki ağaçtan ilgili site tanımlamasını seçin.
* Sağ taraftaki listeden URL ReWrite seçeneğine çift tıklayın.
* Yeni açılan ekranın en sağ tarafındaki Actions menüsünden Manage Server Varialbes bölümündeki View Server Variables… seçeneğine tıklayın.
* Yeni açılan penceredeki Actions bölümünden Add… seçeneğine tıklayın.
* Karşınıza gelen dialogdaki Server Variable Name isimli alana HTTP_HOST yazın ve OK butonuna tıklayın. Siz değişken ismini yazmaya çalıştığınız sırada, dialog sizin yazmaya çalıştığınız değişken ismini otomatik olarak tamamlamaya çalışacaktır.
* İşlemi tamamladığınızda yine Actions menüsünden Back to Rules seçeneğine tıklayın.
* Yeni url kuralını oluşturmak üzere Actions menüsünden Add Rule… seçeneğine tıklayın.
* Yeni açılan pencereden Inbound and Outbound Rules grubundan Reverse Proxy seçeneğini seçin ve OK butonuna tıklayın.
* Yeni açılan dialogdaki Enter the server name or IP address where HTTP requests will be forwarded alanına, ilgili url e istek gelmesi durumunda, sunucunun yönlendirileceği adresi girin.
* Eğer SSL yönetimi IIS sunucunuz tarafından gerçekleştirilmiyorsa veya SSL kullanmıyorsanız, Enable SSL offloading seçeneğinin işaretini kaldırın.
* OK butonuna basarak pencereyi kapatın.
* Bir sonraki pencerede inbound rules listesinden oluşturduğunuz kuralı (listenin en sonundaki kural) bularak çift tıklayın.
* Pattern bölümüne başına slash işareti gelmeyecek şekilde yönlendirilecek url ile ilgili regex deseninizi tanımlayın. Mevcut örnek için ^ornek/?(.*) diyebiliriz.
* Server Variable bölümündeki listenin yanında yeralan Add butonuna tıklayın ve yeni açılan penceredeki server variable name alanına HTTP_HOST, Value alanına {HTTP_HOST} değerini girin ve OK butonuna basın.
* Action grubundaki Action Type bölümünün Rewrite olarak seçili olduğundan emin olun.
* Yine aynı gruptaki Rewrite Url alanında mevcut örneğe göre {C:1}://mysampleapp-13241324-aws.amazon.com/{R:0}
* Pencerenin sağ tarafında yeralan Actions menüsünden Apply seçeneğine tıklayarak yönlendirme işlemini aktif hale getirin.

Tüm bu işlemleri gerçekleştirdiğinizde, isteği karşılayan url e gelen tüm istekler, belirttiğiniz sunucuya yönlendirilir ve HTTP_HOST gibi isteğin geldiği sunucuyu belirleyen değişkenler isteğiniz doğrultusunda tamamen karşı sunucuya aktarılır.

Genel Yazılım ve Sistem Mühendisliği

MySQL ile 5 Dakikada Replikasyon ve Docker Üzerinde Deneme Ortamının Oluşturulması

MySQL veritabanınızın kopyalarını farklı sunuculara dağıtarak okuma işlemlerinden doğan yükü farklı makinelere dağıtabilirsiniz. Replikasyon denilen bu işlemin gerçekleştirildiği yapılarda, okuma işlemi slave denilen çok sayıdaki sunucudan gerçekleştirilebilirken, yazma işlemi yalnızca master denilen ana mysql sunucusuna gerçekleştirilmektedir.MySQL üzerinde replikasyon işlemini gerçekleştirmek son derece basittir. Aşağıdaki işlem basamaklarını gerçekleştirerek siz de varolan veritabanınızın kopyalarını oluşturarak okuma işlemini farklı makinelere dağıtabilirsiniz.

Halihazırdaki MySQL Sunucusunun Master MySQL Sunucusu Olarak Konfigüre Edilmesi

Hali hazırda çalışan MySQL sunucunuzu Master MySQL Server olarak atamak için;

* favori text editörünüz ile /etc/mysql/conf.d/repl.cnf dosyasını olşuturun.

$ sudo vim /etc/mysql/conf.d/repl.cnf

* Aşağıdaki içeriği oluşturduğunuz /etc/mysql/conf.d/repl.cnf dosyasına kayıt edin.

[mysqld]
bind-address	= 0.0.0.0
server-id		= 1
log-bin			= /var/log/mysql/mysql-bin.log

Şayet replikasyon işlemini yalnızca spesifik bir veritabanı için gerçekleştirmek isterseniz aşağıdaki satırı konfigürasyon dosyanıza ekleyebilirsiniz.

	binlog-do-db	= veritabaninizin_adi

* MySQL sunucunuzu yeniden başlatın.

Yukarıdaki konfigürasyonda belrittiğiniz bind-addresstanımı, MySQL sunucusunun hangi IP adresi üzerinden servis portunu dinleyeceğini, server-id tanımı, sunucunun kimliğini ifade eden 2^32 max değerine sahip sayısal değer; log-bin tanımı, master mysql sunucusunun değişiklikleri tutacağı log dosyasını ifade eder.

* MySQL sunucunuza root kullanıcısı ile oturum açın.

	$ mysql --user=root --password=root_kullanicisinin_parolasi

* Replika sunucuların (slave mysql sunucuları) Master MySQL sunucunuza bağlanacağı yeni bir kullanıcı oluşturarak replikasyon yetkisi verin.

mysql> CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_kullanici_parolasi'; 
	   GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

Replikasyon parolasını belirlerken parola uzunluğu ile ilgili güvenlik ilkesini göz önünde bulundurunuz. Aksi halde slave sunucular, master mysql sunucusuna oturum açarken bağlantı hatası alacaklardır.

* Master mysql veritabanınızı kilitleyerek yedek alma işlemi için hazırlık yapalım.

mysql> FLUSH TABLES WITH READ LOCK;
SET GLOBAL read_only = ON;
EXIT

* Aşağıdaki komutu çalıştırarak master mysql sunucumuzdaki tüm veritabanlarının yedeğini alma işlemini başlatalım.

$ mysqldump --user=root --password=root_kullanicisinin_parolasi --lock-all-tables --all-databases > master_mysql.dump

* Oluşturduğumuz veritabanı yedeğini sıkıştırarak slave mysql sunucusu olarak belirlediğimiz sunucuya transfer edelim.

$ tar -czvf /tmp/master_mysql.dump.tar.gz master_mysql.dump
$ scp master_mysql.dump.tar.gz slave_server_ip:/tmp/.

* Kilidi kaldırmak için tekrar mysql konsoluna giriş yapın.

$ mysql --user=root --password=root_kullanicisinin_parolasi

* Aşağıdaki komutları çalıştırarak master mysql sunucusundaki kilidi kaldırın.

mysql> SET GLOBAL read_only = OFF;
UNLOCK TABLES;

* Artık MySQL sunucunumuz kullanıma hazır. Aşağıdaki komutu çalıştırarak master mysql sunucunuzun durumunu görüntüleyi.

mysql> SHOW MASTER STATUS;

Şayet herşey yolundaysa aşağıdaki benzeri bir komut çıktısı ile karşılaşmanız gerekir. Slave Mysql sunucularını kurarken buradaki log dosyasının adı ve pozisyonu gibi bilgilere ihtiyacınız olacaktır.

+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 |      107 |              |                  |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

Slave MySQL Sunucusu Kurulumu

Yeni bir slave MySQL Server kurmak için;

* Aşağıdaki komutu kullanarak MySQL server kurulumunu gerçekleştirin.

$ sudo apt-get update; \
sudo apt-get install -y mysql-server

Master mysql sunucusundan kopyaladığımız veritabanı yedeğini, slave mysql sunucusuna yükleyelim.

$ cd /tmp
$ tar -zxvf master_mysql.dump.tar.gz 
$ mysql --user=root --password=root_kullanicisinin_parolasi < master_mysql.dump

* Favori text editörünüzle /etc/mysql/conf.d/repl.cnf isimli yeni bir dosya oluşturun.

$ sudo vim /etc/mysql/conf.d/repl.cnf

* Aşağıdaki içeriği /etc/mysql/conf.d/repl.cnf dosyasına kayıt edin.

[mysqld]
bind-address	= 0.0.0.0
server-id		= 2

Yukarıdaki içerik, master mysql sunucunuzdaki ile aynı amaca hizmet eder. server-id parametresi sunucuya spesifik olarak atandığı için ilk slave sunucumuzun sunucu kimliğini 2 olarak atadık.

* Master mysql sunucusu ile ilgili tanımlamaları yapmak üzere mysql konsolunu root kullanıcısı ile çalıştıralım.

	$ mysql --user=root --password=root_kullanicisinin_parolasi

Aşaığdaki komutu çalıştırarak slave sunucumuza, master mysql sunucusunun kim olduğunu gösterelim :)
Buradaki MASTER_LOG_FILE ve MASTER_LOG_POST parametrelerine atanacak değerler, master mysql sunucusunun kurulumu sırasında çalıştırdığınız SHOW MASTER STATUS; komutunun çıktısında yeralan değerler ile aynı olmalıdır.

mysql> CHANGE MASTER TO
MASTER_HOST='master_mysql_sunucusunun_ip_adresi',
MASTER_USER='repl',
MASTER_PASSWORD='repl_kullanicisinin_parolasi',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=107;

Artık hazır olduğumuza göre slave mysql sunucusunun master mysql sunucusuna bağlanmasını sağlayalım.

mysql> START SLAVE;

Aşağıdaki komutu çalıştırarak slave mysql sunucunuzun durumunu kontrol edebilirsiniz.

mysql> SHOW SLAVE STATUS;

Şimdi gelin dilerseniz Docker Compose kullanarak Master/Slave veritabanı replikasyonunu kendi geliştirme ortamımızda deneyelim.

MySQL Replikasyonu İçin Docker Üzerinde Deneme Ortamının Oluşturulması

Öncelikle text editörünüzle docker-compose.yml isimli yeni bir dosya oluşturun.

$ vim docker-compose.yml

Şayet siz de benim gibi vim sever biriyseniz docker-compose un yaml parser ı kızmasın diye tab karakterlerini boşluğa dönüştürmeyi unutmayın.

:set expandtab

Aşağıdaki içeriği yaml dosyasına kayıt edin.

version: "2.0"
services:
    master_mysql:
        container_name: master_mysql
        image: mysql:5.5
        environment:
            - "MYSQL_ROOT_PASSWORD=benim_gizli_root_parlam"
        networks:
            my_network:
                aliases:
                    - "masterdb.mynetwork.net"
                ipv4_address: 10.0.0.2
    slave_mysql1:
        container_name: slave_mysql_1
        image: mysql:5.5
        environment:
            - "MYSQL_ROOT_PASSWORD=benim_gizli_root_parlam"
        networks:
            my_network:
                aliases:
                    - "slavedb1.mynetwork.net"
                ipv4_address: 10.0.0.3
networks:
    my_network:
        driver: bridge
        ipam:
            driver: default
            config:
                - subnet: 10.0.0.0/24
                  gateway: 10.0.0.1

Aşağıdaki komutu çalıştırarak ortamimizi ayaklandıralım. Komutu, oluşturduğunuz yaml dosyası ile aynı dizinde çalıştırdığınızdan emin olun.

$ docker-compose up -d

Buradaki -d parametresi, docker-compose un containerları arka planda çalıştırmasını sağlayacaktır.

Master mysql sunucusunu konfigüre etmeye başlamadan önce, kullandığımız docker imajı normal şartlarda sunucunuza kuracağınız bir mysql sunucusuna göre biraz daha minimal bir konfigürasyona sahip olduğu için mysql loglarının baırndırılacağı klöasörün yaratılması ve yetkielndirilmesi gibi bir iki ufak işlemi başlangıçta manual olarak gerçekleştireceğiz. Normal bir kurulumda bu tarz bir işlem yapmanıza pek gerek olmayacaktır.

$ docker exec master_mysql /bin/bash -c 'mkdir /var/log/mysql; chown mysql:mysql /var/log/mysql'

Hemen master mysql sunucusunun konfigüre edilmesi ile ilgili bölümde belirttiğimiz /etc/mysql/conf.d/repl.cnf dosyasını oluşturarak master_mysql konteynerinin içine kopyalayalım.

$ echo '[mysqld]
bind-address	= 0.0.0.0
server-id		= 1
log-bin			= /var/log/mysql/mysql-bin.log' > repl_master.cnf

Oluşturduğumuz dosyayı master_mysql konteynerine kopyalayalım.

$ docker cp repl_master.cnf master_mysql:/etc/mysql/conf.d/repl.cnf

Aynı şekilde slave mysql sunucumuz için de slave sunucusunun konfigüre edilmesi ile ilgili bölümde anlattığımız biçimde yeni bir konfigürasyon dosyası oluşturarak slave_mysql_1 konteynerinin içine kopyalayalım.

$ echo '[mysqld]
bind-address	= 0.0.0.0
server-id		= 2' > repl_slave.cnf

Oluşturduğumuz dosyayı slave_mysql_1 konteynerine kopyalayalım.

$ docker cp repl_slave.cnf slave_mysql_1:/etc/mysql/conf.d/repl.cnf

Artık konteynerlerimiz replikasyon işlemine hazır olduğuna göre her iki konteyneri de docker-compose marifetiyle yeniden başlatalım.

$ docker-compose restart

Aşağıdaki komutu çalıştırarak mysql konteynerlerinin ayakta olup olmadığını kontrol edin.

$ docker-compose ps

Şayet herşey yolunda ise aşağıdaki gibi bir çıktı ile karşılaşmanız gerekir.

    Name                  Command             State    Ports
--------------------------------------------------------------
master_mysql    docker-entrypoint.sh mysqld   Up      3306/tcp
slave_mysql_1   docker-entrypoint.sh mysqld   Up      3306/tcp

Aşağıdaki komutu çalıştırarak slave sunucumuzun, master mysql sunucusuna bağlanacağı replikasyon kullanıcısını oluşturalım.

	
$ docker exec -i master_mysql mysql --user=root --password=benim_gizli_root_parlam <<< "CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';"

Slave mysql sunucumuza master mysql sunucusu ile ilgili tanımlamaları gerçekleştirebilmek için master mysql sunucumuzun durum bilgisini görüntüleyelim.

$ docker exec master_mysql /bin/bash -c '/usr/local/mysql/bin/mysql --user=root --password=$MYSQL_ROOT_PASSWORD -e "show master status;"'

Çalıştırdığınız komut, aşağıdakine benzer bir çıktı üretmelidir.

File	Position	Binlog_Do_DB	Binlog_Ignore_DB
mysql-bin.000001	107

Yukarıda, ortam kurulumu sırasında oluşturduğumuz yml dosysından hatırlayacağınız üzere, master mysql sunucunumuzu 10.0.0.2, slave mysql sunucumuz 10.0.0.3 ip adresleriyle adreslemiştik. Bu doğrultuda aşağıdaki komutu çalıştırarak slave mysql sunucumuza, master mysql sunucusunun kim oldğu ile ilgili tanımlamaları gerçekleştirelim.

MYSQL_QUERY="CHANGE MASTER TO "\
"MASTER_HOST='10.0.0.2', "\
"MASTER_USER='repl', "\
"MASTER_PASSWORD='repl_password',"\
"MASTER_LOG_FILE='mysql-bin.000001',"\
"MASTER_LOG_POS=107; "; \
docker exec -i slave_mysql_1 mysql --user=root --password=benim_gizli_root_parlam <<< $MYSQL_QUERY

Artık herşey hazır olduğuna göre slave sunucumuzun master mysql server ile iletişimini başlatalım.

$ docker exec slave_mysql_1 /bin/bash -c '/usr/local/mysql/bin/mysql --user=root --password=$MYSQL_ROOT_PASSWORD -e "START SLAVE;"'

Son olarak bir durum kontrolü yaparak slave sunucumuzun master mysql server ile iletişimi ile ilgili bir sorun olup olmadığını kontrol edelim.

$ docker exec slave_mysql_1 /bin/bash -c '/usr/local/mysql/bin/mysql --user=root --password=$MYSQL_ROOT_PASSWORD -e "SHOW SLAVE STATUS;"'

Şayet herşey yolundaysa aşağıdaki çıktı ile karşılaşıyor olmalısınız.

Slave_IO_State	Master_Host	Master_User	Master_Port	Connect_Retry	Master_Log_File	Read_Master_Log_Pos	Relay_Log_File	Relay_Log_Pos	Relay_Master_Log_File	Slave_IO_Running	Slave_SQL_Running	Replicate_Do_DB	Replicate_Ignore_DB	Replicate_Do_Table	Replicate_Ignore_Table	Replicate_Wild_Do_Table	Replicate_Wild_Ignore_Table	Last_Errno	Last_Error	Skip_Counter	Exec_Master_Log_Pos	Relay_Log_Space	Until_Condition	Until_Log_File	Until_Log_Pos	Master_SSL_Allowed	Master_SSL_CA_File	Master_SSL_CA_Path	Master_SSL_Cert	Master_SSL_Cipher	Master_SSL_Key	Seconds_Behind_Master	Master_SSL_Verify_Server_Cert	Last_IO_Errno	Last_IO_Error	Last_SQL_Errno	Last_SQL_Error	Replicate_Ignore_Server_Ids	Master_Server_Id
Waiting for master to send event	10.0.0.2	repl	3306	60	mysql-bin.000001	329	405c4af91c66-relay-bin.000002	475	mysql-bin.000001	Yes	Yes							0		0	329	638	None		0	No						0	No	00			1

Hemen ufk bir deneme yaparak master mysql sunucusu üzerinde bir veritabanı yaratalım ve yarattığımız bu veritabanını slave mysql server üzerinde oluşup oluşmadığını görelim.

Aşağıdaki komutu çalıştrarak master mysql sunucusu üzerinde test_db isimli yeni bir veritabanı oluşturalım.

$ docker exec -i master_mysql mysql --user=root --password=benim_gizli_root_parlam <<< "CREATE DATABASE test_db;"

Oluşturduğumuz veritabanının slave sunucusunda da oluştuğunu görelim.

$ docker exec slave_mysql_1 /bin/bash -c '/usr/local/mysql/bin/mysql --user=root --password=$MYSQL_ROOT_PASSWORD -e "SHOW DATABASES;"'

Ta-daaaa….

Database
information_schema
mysql
performance_schema
test_db
Genel

Sublime Text ve OSX İşletim Sisteminde Klavye İle Çoklu Satır Seçme Sorunu

Sorunun çözümü için aşağıdaki işlem basamaklarını uygulayın.

  • Bilgisayarınızın ekranının sol üst köşesindeki elma simgesini tıklayarak System Preferences (Sistem Tercihleri) bölümünü açın.
  • Keyboard (Klavye) simgesine tıklayın ve Shortcuts sekmesine tıklayın.
  • Sol taraftaki listeden Mission Control öğesini seçin. Sağ taraftaki listeden Mission Control ve Application Windows öğelerinin işaretini kaldırın veya kısayolunu değiştirin.
Genel Yazılım ve Sistem Mühendisliği

Docker İle Geliştirme Ortamınızı Taşınabilir Hale Getirin #1

Yazılım geliştirme ekiplerinin büyük problemlerinden biri de taşınabilirliktir. Eğer yazdığınız yazılımın yeterli/güncel bir kurulum dökümanı yoksa, yazılımı geliştirildiği ortamdan farklı bir ortama taşımak büyük problemdir. Taşınma, geliştirme ortamı ile test/production ortamları arasından olmasının yanında, projenin ekibe yeni dahil olan ekip arkadaşınızın bilgisayarın geliştirme ortamının kurulması da olabilir.

Docker, yazılımınızın tüm sistem paketleriyle birlikte çalışabieceği izole ve taşınabilir ortamlar sağlar. Geliştirme ortamınızı bir docker imajı haline getirerek gerek test/production ortamlarına, gerekse yeni bir geliştiricinin makinesine kolaylıkla taşıyabilirsiniz.

Başlamadan önce bazı temel kavramları netleştirelim:

Temel Docker Kavramları

Imaj (image): Bir yazılımın çalışabilmesi için gerekli sistem bağımlılıklarını ve hatta yazılımın kendisini içeren izole alanın taşınabilir görüntüsüdür. Docker imajları Docker tarafından sağlanan dockerhub.com üzeirnden saklanır. Docker dilediğinizde çalıştığınız ortamdaki imajları tar arşivi olarak yedeklemenize olanak sağlar. Docker imajları katmanlı ve salt okunur bir yapıya sahiptir. Imajların katmanlı yapısı, yeniden kullanılabilirliği sağlamaktadır.

Konteyner (Container): Imajların çalışır durumdaki örnekleridir. Bir imajdan birden fazla konteyner oluşturulabilir. Konteynerler, kendisini oluşturan katmanlı imajın üstüne yazılabilir yeni bir katman oluşturur. Konteyner içinde çalşıan yazılımın gerçekleştirdiği tüm değişiklikler bu katmanda meydana gelir. Konteynerde gerçekleşen değişiklikler imajları değiştirmez.

Konuyla ilgili daha fazla ayrıntı için şu dökümanı inceleyebilirsiniz.

Yeni Bir Docker Konteyneri Oluşturmak

Docker run komutu ile varolan veya docker registry (dockerhub.com) da yeralan docker imajlarından yeni bir container oluşturur. Imaj, şayet docker registry den temin edilmişse container ın çalıştırıldığı dosya sisteminde saklanır ve bir sonraki container oluşturma işleminde dosya sisteminde saklanan imajdan yararlanılır.

$ docker run --name cache_server -d -p 6379:6379 redis:latest 

Unable to find image 'redis:latest' locally


latest: Pulling from library/redis

43c265008fae: Downloading [============>                                      ] 13.09 MB/51.35 MB
2738f760012a: Download complete
a3b2771d56b8: Downloading [=======================================>           ] 12.98 MB/16.61 MB
5d98f21a4432: Download complete
79007f20bee8: Download complete
438fc7d50051: Waiting
5e8f776b71d7: Waiting

Yukarıdaki komutu çalıştırdığınızda docker, redis imajının latest etiketli sürümü için ilk defa container oluşturuyorsanız öncelikle dockerhub.com’da bu imajı arayarak bilgisayarınıza indirir. Indirme işlemi sonuçlandığında ise bu imajdan yeni bir örnek oluşturur.
Komut çıktısından da görebileceğiniz gibi imajı oluşturan tüm katmanlar ayrı ayrı indirilmektedir. Yine yukarıdaki komut çıktısında yeralan “Unable to find image ‘redis:latest’ locally” ifadesi, imajın yerel ortamda bulunamadığını ifade eder. Docker bu nedenle imajı dockerhub.com dan indirmektedir.

Örnekteki parametrelerden biraz bahsedelim.

–name Bu parametre, oluşturulan konteyneri isimlendirmek için kullanılır. Bu parametreyi belirtmediğimiz takdirde docker, oluşturulan konteynere otomatik olarak isim verir.

-d: Bu parametre Daemonize anlamına gelir. Çalıştırılmak istenen konteyneri arka planda çalıştırır.

-p 6379:6379 Çalıştırılan portun 6379 nolu portunu, docker servisinin çalıştığı bilgisayarın 6379 nolu TCP portuna map eder. Parametre değerinde iki nokta işareti(:) ile ayrılmış olan sayısal ifadelerden ilki, host (docker servisinin çalıştığı bilgisayar), ikinci sayısal ifade konteynerin portunu ifade eder. Docker, sanal makinelerden aşina olduğunuz farklı ağ modellerine destek vermektedir. Ancak bu bir giriş yazısı olduğu için burada bu konulara hiç girmeyeceğim.
Aynı host üzerinde barındırılan docker konteynerleri, ekstra bir konfigürasyon yapmadan aralarında sağlıklı iletişim sağlayabilirler. Ancak konteynerlerin farklı sunucularda barındırılması veya docker servisinin linux dışındaki bir işletim sisteminde çalışıyor olması çeşitli routing sorunlarına olabileceğinden, bu gibi durumlarda konteyner portları, ev sahibi bilgisayarın portlarına map edilmelidir.

redis:latest: “Redis” repositorysindeki “latest” etiketi ile etiketlenmiş docker imajını ifade eder.

Dilerseniz gelin biraz önce oluşturduğumuz konteynerin durumuna göz atalım.

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
a30b208e1da2        redis:latest        "docker-entrypoint.sh"   51 seconds ago      Up 51 seconds       0.0.0.0:6379->6379/tcp   cache_server

Docker imajları, varsayılan bir giriş noktası (entrypoint) e sahiptir. Imaj isminden sonra belirtilecek parametre, varsayılan giriş noktasını ezmek için kullanılır.

$ docker run --name cache_server -d -p 6379:6379 redis:latest /bin/bash

Bilgisayarınızda sakladığınız imajların listesini şu şekilde görüntüleyebilirsiniz.

$ docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
redis               latest              74b99a81add5        41 hours ago        182.9 MB
ubuntu              14.04               1e0c3dd64ccd        2 weeks ago         187.9 MB

İki Konteyner Arasında İletişim Kurmak

Docker konteynerleri, siz aksini söylemediğiniz sürece docker tarafından sağlanan dahili bir dhcp sunucusu üerinden IP adresi alırlar. IP adresini bildiğiniz* durumda konteynerlar arasında ileitşim sağlayabilirsiniz. Bunun dışında pratikte containerlar ile ilgili IP bilgilerini konteynerler arasında paylaşmak için –link parametresi kullanılır.

Yeni bir linux container oluşturarak bir önceki örnekte oluşturduğumuz konteyner e bağlayalım. Yapacağımız bu işlem, yeni oluşturduğmuz konteynerde açtığımız bash oturumunda, diğer konteynerdeki redis sunucusuna bağlanmamızı sağlayacak.

$ docker run --name linux_machine1 --link=cache_server:cache -i -t ubuntu:14.04 /bin/bash

Yukarıdaki komutu çalıştırdığınızda docker, ubuntu repositorysindeki 14.04 etiketiyle etiketlenmiş docker imajından linux_machine1 isimli yeni bir konteyner oluşturur. Konteyner açıldığında bash oturumu başlatır. Buradaki -t parametresi pseudo (sözde) terminal ayırmasını ve -i klavyeden yapılan girişlerin konteynere iletilmesini sağlar. Redis sunucusu ile ilgili bağlantı bilgileri, –link parametresi ile cache takma adıyla ortam değişkeni olarak konteynere iletilir.

Gelin, redis konteyneri ile ilgili aktarılan iletişim bilgilerini, yeni açılan konteynerden görüntüleyelim.

root@1e3ffaec27b0:/# env|grep -i cache

CACHE_PORT_6379_TCP_PORT=6379
CACHE_PORT_6379_TCP_PROTO=tcp
CACHE_PORT=tcp://172.17.0.2:6379
CACHE_ENV_REDIS_DOWNLOAD_SHA1=6f6333db6111badaa74519d743589ac4635eba7a
CACHE_ENV_REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-3.2.5.tar.gz
CACHE_ENV_REDIS_VERSION=3.2.5
CACHE_NAME=/linux_machine1/cache
CACHE_PORT_6379_TCP_ADDR=172.17.0.2
CACHE_PORT_6379_TCP=tcp://172.17.0.2:6379
CACHE_ENV_GOSU_VERSION=1.7

Redis sunucusuna erişerek bir iki deneme yapmak için, yeni açtıığımız konteynere redis istemcisi kuralım.

root@1e3ffaec27b0:/# apt-get update; apt-get install -y redis-tools

Haydi redis container e bağlanalım.

root@1e3ffaec27b0:/# redis-cli -h ${CACHE_PORT_6379_TCP_ADDR}

172.17.0.2:6379> SET test-key 1
OK
172.17.0.2:6379> GET test-key
"1"

Konu biraz büyük olduğu için bu yazıyı burada sonlandırıyorum. Bir sonraki devam yazısında kendi docker imajınızı nasıl üretebileceğinizden söz edeceğim.

Genel Yazılım ve Sistem Mühendisliği

Docker’la Daha Taşınabilir Ortamlara Merhaba

Şu sıralar hem benden hem çevrenizden yoğun olarak bir docker muhabbeti duyuyorsunuzdur. Bugün bu blog yazısında sizlere Docker ve docker araçları ile ilgili fikir sağlayacak bazı küçük bilgiler vermeyi amaçlıyorum.

Linux işletim sisteminin çekirdeği, proseslerin gruplanarak yönetilebilmesine olanak sağlar. Böylelikle linux üzerinde koşturan yazılımlar, yalnızca kendisine ait sistem paketlerinin yeraldığı izole alanlarda mutlu mesut çalışabilmektedirler. 2008 yılında hayatımıza giren LXC, Linux’un bu güzelliğini insani seviyeye indirgeyerek sağladığı image ve kaynak yönetimi konseptiyle tek host üzerinde çok sayıda izole alan yaratılmasını kolaylaştırmıştır.

Linux ve LXC nin sağladığ çekirdek sanallaştırma konsepti, bilinen işletim sistemi sanallaştırma konspetinden tamamen farklıdır. İşletim sistemi sanallaştırma konseptinde bilgisayarın üzerinde çalışan ev sahibi işletim sistemi, sanallaştırılmış işletim sistemlerini barındırır. Bu nedenle böyle yapılarda donanom, hem ev sahibi işletim sisteminin hem de diğer sannalştırılmış işletim sistemlerinin rahatça çalışabileceği seviyede yüksek olmalıdır. Çekirdek sanallaştırma konseptinde ise sanallaştırma, kabaca çalışan proseslerin belirli alanlarda gruplanması şeklinde gerçekleştirildiğinden daha az kaynak gereksinimi vardır.

LXC, gelecekte benzer konseptleri farklı bakış açılarıyla sağlayacak olan LXD ve Docker’ın ortaya çıkmasına olanak sağlamıştır.
(Bkz. LXC Wiki Article )

Docker Nedir ?

Docker, linux’un kernel sanallaştırma kütüphaneleri ve lxc kullanarak, işletim sistemi üzerinde, belirli bir yazılımın, gereksinim duyduğu tüm sistem bağımlılıklarıyla birlikte çalışabildiği, kendi ağ kaynaklarına sahip izole ortamlar oluşturulmasına olanak sağlayan bir araçtır. Docker’ı diğer sanallaştırma araçlarından ayıran en can alıcı özelliği dağıtık çalışabilme yeteneğidir. Docker herhangibir ortamda ürettiğiniz konteyneri imajlar vasıtasıyla başka ortamlara taşıyabilmenize olanak sağlar.

Neden Docker ?

Docker, bir yazılımı ihtiyaç duyduğu tüm sistem paketi bağımlılıklarıyla birlikte bir konteyner içine yerleştirebilmenize ve dilediğiniz herhangibir yere taşıyabilmenize olanak sağlar. Yazılım, konteyner halinde deploy edileceği için ortam farklılıkları ile ilgil sorunlar ortadan kalkar.

Konteyner imajları sayesinde ölçeklenmiş ortamlarda sistem paketlerinin kurulması, konfigürasyon, yazılım kurulumu gibi tekrarlayan işlerin bir kerede daha soyut biçimde gerçekleştirilebilmesini sağlar.

Docker, özellikle çok sayıda servis ve mikro servisten oluşan yazılımların taşınabilirliğinde büyük kolaylık sağlar. Docker, Compose ve Swarm gibi araçları sayesinde bir web uygulaması, mysql server, redis/memcache ve çok sayıda mikro servislerden oluşan bir ortamı ister bir geliştiricinin makinesine isterseniz çok sayıda clusterdan oluşan bir ortama kolaylıkla taşımanıza olanak sağlar. Ölüm ve aşk acısı dışında her türlü derde devadır Docker :)

Bir sonraki yazıda kullanım pratikleri konusunda daha detayları bilgileri sizlerle paylaşıyor olacağım.

Genel

PHPKonf Geliyor…!

PHPKonfİstanbul PHP User Group yine sabırsızlıkla beklediğim muhteşem bir konferansa ev sahipliği yapıyor. 21-22 Mayıs 2016 tarihinde Bahçeşehir Üniversitesi, Beşiktaş yerleşkesinde gerçekleştirilecek olan etkinlikte paralel oturumlarla, yazılım dünyasından pek çok yerli ve yabancı konuşmacı, dinleyicileriyle buluşacak.

Eğer PHP geliştiricisiyseniz yerinizde olsam bu etkinliği kesinlikle kaçırmazdım. Zira SensioLabs kurucusu ve Symfony framework’ün babası Fabien Potencier, Doctrine core geliştiricilerinden Marco Pivetta ve Gianluca Arbezzano, SitePoint’in PHP editörü Bruno Skvorc, Yii framework’ün core geliştiricisi Alexander Makarov, Magento Evangelist Ben Marks ve Microsoft’tan PHP’in ve eklentilerinin neredeyse bütün desteğini sağlayan Pierre Joye, Ahmet Alp Balkan gibi(ve çok daha fazlası!) çok değerli isimleri bir arada görebilme şansına sahip olacağız.

Etkinlikle ilgili gelişmeleri şuradan takip edebilir; etkinlikle ilgili detayları buradan öğrenebilirsiniz.

Genel Yazılım ve Sistem Mühendisliği

Logstash ile Veri Analizi

Logstash farklı kanallardan veri toplayıp, konfigürasyon seviyesinde filtrelerle belirli kuralalra göre parçalamanızı sağlayan ve farklı tipterde kanallara dağıtabilen gerçek zamanlı ve açık kaynaklı bir veri toplama moturudur.

Logstash, ayrıştırma işlemini kendi komut seti içerisinde klasik regex eşleştirmeleri ile yapabildiği gibi LOGLEVEL, TIMESTAMP_ISO8601, DATETIME.. gibi ön tanımlı çok sayıda veri tipleriyle de kolayca yapabilmenizi sağlar.

Logstash, TCP/UDP soketleri ve log dosyaları gibi basit kaynaklar dışında çeşitli streaming protokollerindan tutun, kafka, log4j, redis araçlardan github, heroku, twitter ve irc gibi servislere kadar pek çok kaynaktan beslenebilme yeteneğine sahiptir. Ayrıca tüm bu kaynaklardan aldığınız verileri belirlediğiniz kurallar çerçevesinde işledikten sonra dilerseniz email olarak gönderebilir, ya da elastic search üzerine yazarak Kibana gibi veri görselleştirme araçlarıyla analiz edebilrsiniz.

Logstash aynı anda birden fazla kaynaktan beslenip yine birden fazla kaynağa veri dağıtabilir. Desteklenen veri kaynakları ile ilgili olarak şuraya, aktarım kaynakları ile ilgili olarak buraya bakabilirsiniz.

Logstash, tüm sistem/servis loglarınızı toplamaktan, özel olarak yazacağınız filtrelerle ödeme sisteminizin loglarını işleyerek istatistiksel veriler çıkarmanıza olanak sağlayacak çok geniş bir kullanım alanına sahiptir.

NGINX Access Logları Logstash ile Nasıl İşlenir ?

Aşağıdaki adresten logstash uygulamasının güncel sürümünü indirelim.
https://www.elastic.co/downloads/logstash

Ben şu anki güncel sürüm olan 2.2.2 sürümünü indiriyorum.
https://download.elastic.co/logstash/logstash/logstash-2.2.2.tar.gz

$ wget https://download.elastic.co/logstash/logstash/logstash-2.2.2.tar.gz

Sıkıştırılmış logstash paketini açalım.

$ tar -zxvf logstash-2.2.2.tar.gz

Konfigürasyon dosya(ları)mızı barındıracağımız conf.d dizinini oluşturalım.

$ cd logstash-2.2.2
$ mkdir conf.d

Logstash konfigürasyon dosyası, json benzeri bir yapıya sahiptir. Konfigürasyon dosyası, veri kaynaklarının tanımlandığı input{}, işleme tanımlamalarının yapıldığı filter{} ve işlenen verinin aktarılacağı kaynaklarla ilgili tanımlamaların yapıldığı output{} olmak üzere üç bölümden oluşur.

input {
	# Bu bölümde logstash i besleyecek veri kaynakları 
	# ve veri kaynaklarının opsiyonları ile ilgili 
	# ayarlar yeralır.
}

filter {
	# Bu bölümde gelen verinin nasıl parse edileceği,
	# verinin hangi bölümünün hangi alanlarda tutulacağı
	# gibi tanımlamalar yeralır.
}

output {
	# Burada işlenen verinin hangi kaynaklara aktarılacağı 
	# ve kaynakların ayarları ile ilgili tanımlamalar yeralır.
}

Okuyacağımız veri, nginx access loglarında yeraldığına göre file eklentisini kullanarak dosyayı okumak için gerekli tanımlamaları gerçekleştirelim.

input {
	file {
		path => "/var/log/nginx/*-access*"
		exclude => "*.gz"
		start_position => "beginning"
	}
}

Yukarıdaki konfigürasyonda;

path => “/var/log/nginx/*” ifadesi, /var/log/nginx dizinindeki tüm dosyaların okunması gerektiğini logstash e söyler. *-access* kullanılmasının nedeni, logstash in güncel ve rotate edilmiş tüm access loglarını değerlendirmesini sağlamaktır. Ancak bu dizinde arşivlenen log dosyaları da barındırıldığından exclude komutunu kullanarak *.gz uzantılı arşiv dosyalarını hariç tutulmasını sağlıyoruz.

Logstash varsayılan olarak path bölümünde verilen dosya yada dosyaların sonuna eklenen satırları değerlendirmektedir. Ancak log stash in rate edilmiş logları da değerlendirebilmesi için start_position parametresine beginning değerini vererek, dosyaları başından itibaren değerlendirmesi gerektiğini logstash e söylüyoruz.

Artık tüm nginx access loglarını okuyabilir durumda olduğumuza göre, aşağıdaki örnek log çıktısının logstash tarafından incelenmesini sağlamaya hazırız.

157.55.39.132 - - [20/Mar/2016:06:49:05 -0400] "GET /tag/software-engineering/page/2/ HTTP/1.1" 200 9329 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:06 -0400] "GET /tag/yii-framework/ HTTP/1.1" 200 14823 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:07 -0400] "GET /goygoy-yonelimli-programlama-ggop/ HTTP/1.1" 200 14980 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:07 -0400] "GET /django-uygulamalarinin-apache-uzerinde-sanal-ortam-virtualenv-ile-birlikte-calistirilmasi/ HTTP/1.1" 200 10964 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:08 -0400] "GET /dekorator-tasarim-deseni/ HTTP/1.1" 200 13180 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:08 -0400] "GET /cv/ HTTP/1.1" 200 9723 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:09 -0400] "GET /tasarim-candir/ HTTP/1.1" 200 12253 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:10 -0400] "GET /cem-karaca-5-nisan-1945-8-subat-2004/ HTTP/1.1" 200 9825 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
157.55.39.132 - - [20/Mar/2016:06:49:10 -0400] "GET /page/2/?s=git+di HTTP/1.1" 200 33628 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"

Logdaki veriyi parse edecek filtre konfigürasyonunu oluşturalım.

filter {
	grok {
		match => {"message" => "%{COMBINEDAPACHELOG}+%{GREEDYDATA:extra_fields}"}
	}

	mutate {
		convert => ["response", "integer"]
		convert => ["bytes", "integer"]
		convert => ["responsetime", "float"]
	}

	geoip {
		source => "clientip"
		target => "geoip"
	}

	date {
		match => ["timestamp", "dd/MMM/YYYY:HH:mm:ss Z"]
	}
}

Yukarıda gördüğünüz filtre konfigürasyonunda grok, mutate, geoip ve date eklentilerini kullandık. Şimdi dilerseniz sırayla bu eklentilerin ne işe yaradıklarını ve konfigürasyondaki işlevlerini görelim.

grok eklentisi, belirli bir yapıya sahip olmayan girdiyi belirtilen patterne uygun olarak parse etmeye yarar. Parse edilen veriyi COMBINEDAPACHELOG gibi ön tanımlı değişken gruplarıyla parse edebileceğiniz gibi GREEDYDATA tipindeki belirli bir veriyi kendi istediğiniz bir değişkene atayabilirsiniz.
Yukarıdaki örnekde grok eklentisi, match parametresinden gönderilen paterne uygun verileri parse ederek ön tanımlı ve/veya paternde tanımlanan extra_fields gibi değişkenlere atamaktadır. COMBINEDAPACHELOG veri tipi, log verisinde yeralan istemci ip adresi, istek yapılan zaman, istek yapılan uç nokta, transfer edilen veri miktarı vb. verileri clientip, timestamp, request, bytes gibi değişkenlere aktarılmasını sağlar. GREEDYDATA tipi ise genel amaçlı bir veri tipidir. Yapısal olmayan ve COMBINEDAPACHELOG tarafından parse edilmeyen diğer veriler GREEDYDATA tipindeki extra_fields değişkenine aktarılır.

Desteklenen diğer grok paternleri için şu github reposunu inceleyebilirsiniz.

mutate eklentisi, işlenen veri sonucunda oluşan alanlarda değişiklik yapmaya yarayan filtre eklentisidir. Yukarıdaki örnekte mutate eklentisi, convert komutuyla COMBINEDAPACHELOG tarafından parse edilip response, bytes ve responsetime gibi alanlara yazılan sayısal verilerin, tam sayı tipine çevirerek (Logstash, yapısal olmayan log girdisindeki veriyi varsayılan olarak string tipinde parse eder.) gerektiğinde sayısal operasyonlara tabi tutulabilir durumda olmalarını sağlar.

geoip eklentisinin bu örnekteki en eğlenceli eklenti olduğunu söyleyebilirim. Kendisi, access logda parse edilen istemci IP adresinin, coğrafi konum bilgisini elde eder.

date eklentisi ise belirli bir alandaki zaman bilgisini parse ederek belirli bir alana yazmak veya logstash in dahili zaman damgası olarak kullanabilmek için kullanılır. Burada, logdan parse edilen tarih/saat bilgisi, kendisine uygun zaman formatı ile eşleştirilerek logstash UTC formatındaki dahili zaman damgası alanının üzerine yazılır. Date filtresi, target komutu ile özellikle farklı bir hedef alan belirtilmediği sürece eşleşen zaman bilgisini varsayılan olarak logstash in dahili zaman damgası alanı olan @timestamp alanına yazar.

Artık logumuzu parse edebilir durumda olduğumuza göre sıra geldi parse edilen veriyi aktaracağımız kaynağı tanımlamaya. Örnek çıktısını konsolda görebilmek adına stdout eklentisini kullanacağız. Ancak başta bahsettiğim gibi şuradan kullanabileceğiniz tüm eklentilerle ilgili detaylı bilgi alabilirsiniz.

İşlenen veri çıktısını konsola basmak için aşağıdaki konfigürasyonu oluşturuyoruz.

output {
	stdout {
		codec => "rubydebug"
	}
}

Burada stdout eklentisi, işlenen verinin konsola basılmasını sağlar. codec olarak belirtilen rubydebug ise awsome_print isimli ruby kütüphanesini kullanarak çıktının okunabilirliğini arttırır. Diğer codec seçenekleri için şu sayfayı inceleyebilirsiniz.

Oluşturduğumuz konfigürasyonu conf.d dizinine kaydettikten sonra artık uygulamayı çalıştırmaya hazırırz. Örnek konfigürasyonumuzun ismi

nginx_access_log.conf

olsun.

$ ./bin/logstash -f ./conf.d/nginx_access_log.conf

Uygulamayı çalıştırdığınızda, logstash size şöyle bir çıktı üretecektir.

...

{
        "message" => "157.55.39.132 - - [20/Mar/2016:06:49:10 -0400] \"GET /cem-karaca-5-nisan-1945-8-subat-2004/ HTTP/1.1\" 200 9825 \"-\" \"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)\"",
       "@version" => "1",
     "@timestamp" => "2016-03-20T10:49:10.000Z",
           "host" => "king",
       "clientip" => "157.55.39.132",
          "ident" => "-",
           "auth" => "-",
      "timestamp" => "20/Mar/2016:06:49:10 -0400",
           "verb" => "GET",
        "request" => "/cem-karaca-5-nisan-1945-8-subat-2004/",
    "httpversion" => "1.1",
       "response" => 200,
          "bytes" => 9825,
       "referrer" => "\"-\"",
          "agent" => "\"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)\"",
          "geoip" => {
                    "ip" => "157.55.39.132",
         "country_code2" => "US",
         "country_code3" => "USA",
          "country_name" => "United States",
        "continent_code" => "NA",
              "latitude" => 38.0,
             "longitude" => -97.0,
              "dma_code" => 0,
             "area_code" => 0,
              "location" => [
            [0] -97.0,
            [1] 38.0
        ]
    }
}


...
Genel Yazılım ve Sistem Mühendisliği

Bileşik (Composite) Tasarım Kalıbı

Composite design pattern, belirli bir nesne grubuna, tek bir nesneyi kullanıyormuşcasına muamele etmek için kullanılan yapısal bir tasarım desenidir.

Gerçek Hayat Örneği:

Problem:
2300 TL değerindeki Nikon D90 fotograf makinesi, 150 TL değerindeki sparkfun tripod ve 110 TL değerindeki şarjlı pil+şarj aleti seti bundle ürün halinde satışa sunulmak isteniyor.

Uygulama:
Elektronik ticaret sitelerinde ürünler, teker teker satılabildiği gibi yukarıdaki problemde belirtildiği gibi zaman zaman kit şeklinde de satılabilirler. Ancak sistemin her durumda tek ürüne de çoklu ürüne de benzer muamele uygulayarak indirim, kupon kodu kullanımı gibi diğer mevcut işlevlerini yerine getirebilmeye devam etmesi gerekmektedir. Composite tasarım deseni bu gibi durumlar için elverişli bir tasarım kalıbıdır.

Uygulamaya ilk olarak ürünümüzün özelliklerini tanımlayacak arayüz sınıfını oluşturarak başlayalım.
ProductInterface.php:

<?php
interface ProductInterface
{
    /**
     * @return string
     */
    public function getName();

    /**
     * @return string
     */
    public function getPrice();
}

Basit ürün ve bundle ürün tarafından kullanılacak ortak metodları içeren soyut ürün sınıfını oluşturalım.

AbstractProduct.php:

<?php
abstract class AbstractProduct implements ProductInterface
{
    /**
     * @var string
     */
    protected $name;
    
    /**
     * @var double
     */
    protected $price;

    /**
     * constructor
     * @param string $name
     * @param double $price
     */
    public function __construct($name, $price=null)
    {
        $this->name = $name;
        $this->price = $price;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return double
     */
    public function getPrice()
    {
        return $this->price;
    }
}

Fotograf makinesi ve tripod gibi tekil ürünleri temsil edecek basit ürün sınıfını tanımlayalım.

SimpleProduct.php:

<?php
class SimpleProduct extends AbstractProduct
{
    
}

Şarj aleti+pil seti ve diğer basit ürünleri kit haline getirmemizi sağlayacak bundle ürün sınıfını tanımlayalım.
BundleProduct.php:

<?php
class BundleProduct extends AbstractProduct
{
    /**
     * @var array
     */
    private $products;

    /**
     * constructor
     * @param string $name
     * @param array $products
     * @param double $price
     */
    public function __construct($name, array $products, $price=null)
    {
        $this->setProducts($products);
        parent::__construct($name, $price ? $price : $this->getTotalPrice());
    }

    /**
     * @param ProductInterface $product
     */
    private function addProduct(ProductInterface $product)
    {
        $this->products[] = $product;
    }   

    /**
     * @param array $products
     */
    private function setProducts(array $products)
    {
        if(count($products) <= 1) {
            throws new ValueErrorException('Bundle products must have more than one product!');
        }
        for($products as $product) {
            $this->addProduct($product);
        }
    }

    /**
     * @return duobule
     */
    public function getTotalPrice()
    {
        return array_sum(array_map(function($product) {
            return $product->getPrice();
        }, $this->products));
    }
}

Gerekli tüm sınıf tanımlamalarını tamamladığımıza göre artık örnek problemde tanımlanan senaryoyu oluşturabiliriz.

<?php
$photoMachine = new SimpleProduct('Nikon D90', 2300);
$tripod = new SimpleProduct('SparkFun Tripod', 150);

$chargerKit = new BundleProduct('Şarjlı Pil + Şarj aleti seti', array(
    new SimpleProduct('Şarj aleti', 80),
    new SimpleProduct('Pil', 30)
));

$bundleProduct = new BundleProduct('Amatör Fotoğraf Kiti', array(
    array($photoMachine, $tripod, $chargerKit)
));

echo $bundleProduct->getPrice(); // 2560

Genel

Cem Karaca – 5 Nisan 1945 – 8 Şubat 2004

Cem Karaca
Cem Karaca

Seni çok özledik…
Cem Karaca
(5 Nisan 1945 – 8 Şubat 2004)

Genel Yazılım ve Sistem Mühendisliği

Süzgeç Tasarım Deseni

Belirli bir nesne grubunun bir veya daha fazla kritere göre filtrelenebilmesini sağlayan yapısal tasarım desenidir. Tanımdan da anlaşılacağı üzere filtreleme işlemi tekil veya zincir halinde gerçekleştirilebilir.

Gerçek Hayat Örneği:

Dilerseniz bu örneği Dekoratör Tasarım Deseni yazısında söz ettiğimiz e-ticaret uygulaması üzerinden devam ettirelim.

Kısaca hatırlatmak gerekirse sitemizden aynı kategoriden 3 ürün alan müşteriye 2 fiyatı ödetecek ve 10 TL indirim vereecktik. Bu işlemi yaparken dekoratör ve strateji tasarım desenlerinden yararlanmıştık. Şimdi bu örneği biraz daha özelleştirelim.

Kullanıcımızın, yeni yıl kategorisinden satın alacağı 3 adet 100 TL üzeri ürün için 2 fiyatı ödetelim ve 10 TL indirim uygulayalım.

Uygulama:

Örnekde belirttiğimiz şartlara uygun olan sepet ürünlerini filtrelemek için oluşturacağımız filtrelerin implement edeceği arayüz sınıfını oluşturalım.

<?php
namespace Promotion;

interface FilterInterface
{
    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array());
}

Kullanıcıya sağlayacağımız fırsatın ilk şartı kategori ekseninde olduğuna göre sepet öğelerini kategori bazlı filtreleyecek filtre sınıfını kodlayalım.

<?php

namespace Promotion\Filter;

use Promotion\FilterInterface;

class CategoryFilter implements FilterInterface
{
    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array())
    {
        foreach($items as $item) {
            if( $item->getCategory()->getId() == $args['category_id'] ) {
                yield $item;
            }
        }
    }
}

Bir sonraki ölçütümüz olan tutar bilgisi için yeni bir filtre sınıfı oluşturalım.

<?php

namespace Promotion\Filter;

use Promotion\FilterInterface;

class PriceFilter implements FilterInterface
{
    const CRITERIA_EQUAL = 'eq';
    const CRITERIA_LESS_THAN = 'lt';
    const CRITERIA_GREATER_THAN = 'gt';
    const CRITERIA_LESS_THAN_OR_EQUAL = 'lte';
    const CRITERIA_GREATER_THAN_OR_EQUAL = 'gte';

    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array())
    {
        foreach($items as $item) {
            switch($args['condition']) {
                case self::CRITERIA_EQUAL:
                    if($item->getPrice() == $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_LESS_THAN:
                    if($item->getPrice() < $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_GREATER_THAN:
                    if($item->getPrice() > $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_LESS_THAN_OR_EQUAL:
                    if($item->getPrice() <= $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_GREATER_THAN_OR_EQUAL:
                    if($item->getPrice() >= $args['price']) {
                        yield $item;
                    }
                    break;
            }
        }   
    }
}

Son olarak her iki şartı VE lojiğine tabi tutacak filtre sınıfını oluşturalım.

<?php

namespace Promotion\Filter;

use Promotion\FilterInterface;

class AndFilter implements FilterInterface
{
    /**
     * @var FilterInterface $filter1
     */
    private $filter1;

    /**
     * @var FilterInterface $filter2
     */
    private $filter2;

    /**
     * @param FilterInterface $filter1
     * @param FilterInterface $filter2
     */
    public function AndFilter(FilterInterface $filter1, FilterInterface $filter2)
    {
        $this->filter1 = $filter1;
        $this->filter2 = $filter2;
    }

    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array())
    {
        $filtered = $this->filter1->apply($items, $args);
        return $this->filter2->apply($filtered, $args);
    }
}

Filtrelerimiz hazır olduğuna göre bir önceki örnekde oluşturduğumuz SpecialOfferCalculator sınıfını yeni şartlara göre düzenleyelim.

<?php
namespace Calculator\Calculator;

use Calculator\Calculator\CalculatorAbstract;
use Promotion\Filter\CategoryFilter;
use Promotion\Filter\PriceFilter;
use Promotion\Filter\AndFilter;

class SpecialOfferCalculator extends CalculatorAbstract
{
    const OFFER_LIMIT = 3;
    const OFFER_CATEGORY_ID = 5; // CATEGORY_ID:5 = Yeni Yıl
    const OFFER_PRICE_LIMIT = 100;
    const OFFER_PRICE_COND = PriceFilter::CRITERIA_GREATER_THAN_OR_EQUAL;

    private prepareItems()
    {
        $filter = new AndFilter(new CategoryFilter(), new PriceFilter());
        $filteredItems = $filter->apply(
            $this->decoratedObject->getItems(), 
            array(
                'category_id' => self::OFFER_CATEGORY_ID,
                'price'       => self::OFFER_PRICE_LIMIT,
                'condition'   => self::OFFER_PRICE_COND
            )
        );

        // Generator tip, iterate edilmedigi surece sonuc donmedigi icin,
        // bir sonraki metodda yapilacak belirli kategorideki urun sayisi
        // kontrolu nedeniyle generator tipini diziye donusturuyoruz.
        $items = array();
        foreach($filteredItems as $item) {
            $items[] = $item;
        }
        return $items;
    }

    public function getTotalPrice()
    {
        $totalPrice = $this->decoratedObject->getTotalPrice();
        $items = $this->prepareItems();

        if(count($items) > self::OFFER_LIMIT) {
            // Urunleri pahalidan ucuza siralayarak 100 TL ve uzeri en ucuz
            // urunun tutarini indirim olarak uyguluyoruz.

            usort($items, function($itemA, $itemB) {
                return $itemA->getPrice() > $itemB->getPrice();
            });

            $totalPrice -= end($items)->getPrice();
        }
        return $totalPrice;
    }
}

Artık sepet tutarımızı hesaplamaya hazırız.

<?php
namespace MyApp;

use Basket\Basket;
use Basket\BasketItem;
use Promotion\Discount;
use Calculator\Calculator\TotalPriceCalculator;
use Calculator\Calculator\SpecialOfferCalculator;
use Calculator\Calculator\DiscountCalculator;

class Main
{
    public function main()
    {
        $basket = new Basket();
        $basket->add(new BasketItem("001", new Category(5, 'Yeni Yıl'), 'Gömlek', 150))
            ->add(new BasketItem("001", new Category(5, 'Yeni Yıl'), 'Gömlek', 180))
            ->add(new BasketItem("001", new Category(5, 'Yeni Yıl'), 'Gömlek', 105))
            ->add(new BasketItem("002", new Category(5, 'Yeni Yıl'), 'Pantolon', 80);
        $basket->setDiscount(new Discount(Discount::DISCOUNT_TYPE_FIXED, 10));
        $basket = new DiscountCalculator(
            new SpecialOfferCalculator(
                new TotalPriceCalculator($basket)
            )
        );
        print $basket->getTotalPrice(); 
        // sub total = 150 + 180 + 105 + 80 = 515
        // ofered total = sub total - 105 = 410
        // Payment total = offered total - discount amount (10) = 405 TL
    }
}