SQL Server i docker-compose lokalnie

Mateusz Gajda11/23/2018 - 5 min read

Photo by: Alesia Kazantceva

Cześć, w ostatnim czasie na potrzeby swojej aplikacji potrzebowałem podpiąć kontener dockerowy z SQL Serverem. Mimo że teoretycznie wydaje się to proste, spotkałem parę uprzykrzających życie problemów. Dlatego też chciałbym przedstawić wam swoje rozwiązanie. Stworzyłem je na potrzeby lokalnego developmentu i póki co jeszcze nie testowałem tego w chmurze, ale na pewno do tego dojdzie. Wtedy będzie to doskonała okazja żeby ewentualnie zaktualizować ten wpis :)

Background

W swojej pobocznej aplikacji wykorzystuję dockera aby spiąć ją z Mongo. Początkowo na kontenerze miał stać MSSQL, ale problemy z nim związane sprawiły, że odciągnąłem to w czasie. W końcu nadszedł ten czas :) Moim głównym zamiarem, było za pomocą docker-compose spiąć całą infrastrukturę. Najbardziej zależało mi, by wraz z jego odpaleniem powstawała czysta, gotowa do boju baza danych. Dodatkowo była to też dobra okazja aby poszerzyć swoją wiedzę w zakresie kontenerów. # Docker

Pomijając oczywisty fakt konieczności posiadania zainstalowanego u siebie dockera (którego jeżeli ktoś nie ma, można go ściągnąć tutaj), będziemy również potrzebować obrazu SQL Servera. Możemy ściągnąć go z poziomu PowerShella za pomocą polecenia:

docker pull microsoft/mssql-server-linux:2017-latest

Obraz waży ok 1.32GB. Aby się upewnić że został ściągnięty wystarczy skorzystać z polecenia:

docker images

W konsoli, na liście powinny nam się pojawić wszystkie pobrane obrazy, a wraz z nim microsoft.com/mssql/server. Warto wspomnieć że ten punkt nie jest obligatoryjny. Możemy też odłożyć to na później, ponieważ docker przy uruchamianiu aplikacji, dociągnie sam wszystkie potrzebne obrazy. Mimo wszystko zawsze warto wiedzieć, jak zrobić to samemu :) Już w tym momencie jesteśmy w stanie z poziomu konsoli postawić nasz obraz i cieszyć się działającym SQL Serverem. Jednakże jako że programista to leniwa osoba i nikomu nie chciałoby się wpisywać co chwila tego samego polecenia, musimy ugryźć to inaczej. Dlatego też kolejnym, ważnym krokiem będzie stworzenie Dockerfile w głównym katalogu naszej aplikacji. Najprostszym sposobem na jego wygenerowanie będzie kliknięcie PPM na projekcie, oraz wybranie Add -> Docker Support. Dockerfile definiuje w jaki sposób ma być zbudowany obraz naszej aplikacji. Nie chcę się skupiać na wyjaśnianiu, do czego służy każde z poleceń, dlatego dociekliwych odsyłam do google.

FROM microsoft/aspnetcore-build:lts
COPY . /app
WORKDIR /app
RUN [dotnet, restore]
RUN [dotnet, build]
EXPOSE 80/tcp
RUN chmod +x ./startingpoint.sh
CMD /bin/bash ./startingpoint.sh

Docker-compose

Przejdźmy więc do kolejnej potrzebnej nam rzeczy, którą musimy utworzyć czyli docker-compose. Pozwala on nam na uruchomienie wielu kontenerów na raz, za pomocą notacji YAML która opisuje naszą infrastrukturę.

version: '3.4'
services:
myapp.api:
image: ${DOCKER_REGISTRY}myapp
network_mode: bridge
build:
context: .
dockerfile: MyApp.Api/Dockerfile
links:
- mongoDb
- sqlServer
sqlServer:
image: microsoft/mssql-server-linux:latest
network_mode: bridge
container_name: sqlServer
environment:
SA_PASSWORD: Your_password123
ACCEPT_EULA: Y
ports:
- '20:1433'
working_dir: /usr/src/app
volumes:
- /var/opt/mssql
- ./scripts/SqlServer:/usr/src/app
command: bash -c sh entrypoint.sh & /opt/mssql/bin/sqlservr

W taki sposób prezentuje się mój plik. Znajdują się w niej dwie usługi. Pierwszą jest aplikacja ASP.NET Core, druga to nasz Sql Server. Wyjaśniając: - image - wskazuje nam obraz, z jakiego chcemy zbudować kontener

  • container_name - to jak nietrudno się domyślić, nazwa naszego kontenera. Jest to o tyle przydatne że z poziomu konsoli, na przykład za pomocą polecenia docker inspect możemy odwołać się do danego kontenera poprzez tą nazwę,
  • environment - zmienne środowiskowe, które definiujemy dla danego kontenera,
  • ports - definicja portów które mapujemy z kontenera na porty hosta docker (po lewej definiujemy port hosta, po prawej kontenera),
  • working_dir - katalog roboczy w kontenerze,
  • volumes - krótko rzecz ujmując, jest to miejsce w którym składujemy dane
  • command - komenda jaka ma zostać uruchomiona po starcie kontenera
  • links - tworzy dodatkowe aliasy pod którymi dostępne są nasze usługi. Nie sa one jednak wymagane do komunikacji między usługami, gdyż każda z nich może znaleźć inną po jej nazwie

W zmiennych środowiskowych definiujemy hasło dla użytkownika sa. Na potrzeby developmentu, u mnie jest to wartość Your_Password123. Dodatkowo musimy ustawić zmienną ACCEPT_EULA na dowolną wartość w celu zaakceptowania End-User Licensing Agreement. Następnie możemy przejśc do mapowania portów. Jako że mam u siebie zaalokowany port 1433, musiałem więc zmapować port z kontenera, na port 20. Jeżeli chodzi o volumeny, to w tym przypadku ustawiam jeden z nich na folder /var/opr/mssql. Nie chcę by restart kontenera spowodował utratę logów, czy też samych baz danych, z tego więc powodu, muszę ten folder umieścić w volumenie. Drugim volumenem jest mój lokalny folder ./scripts, tam znajdują się skrypty bashowe i pliki .sql które będę chciał odpalić przy starcie kontenera. Dzięki temu, odpalając docker-compose, stworzy mi się świeża baza danych, gotowa do pracy z aplikacją. Ostatnim parametrem jest command. To tutaj z poziomu folderu /usr/src/app odpalam skrypt entrypoint.sh który ma za zadanie stworzyć schemat bazy. Jednocześnie, w tym samym czasie włączam sam MSSQL dzięki polecenu /opt/mssql/bin/sqlservr. Dodatkowo możemy zauważyć że dla każdej usługi zdefiniowany jest network_mode: bridge. Parametr ten definiuje nam typ sieci w jakiej znajdą się kontenery. Z racji tego że chcemy by miały swobodny dostęp do siebie nawzajem, musimy sprawić aby wszystkie znalazły się w jednej. Da nam to możliwość łączenia się z każdym z kontenerów poprzez jego nazwę.

Entrypoint.sh

#!/bin/sh
#start SQL Server, start the script to create the DB and initial data
echo 'starting database setup'
wait_time=15
password=Your_password123
echo 'Please wait while SQL Server 2017 warms up'
sleep $wait_time
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $password -i ./InitDb.sql
echo 'InitDb done'
echo 'Finished initializing the database'

Tak prezentuje się mój plik entrypoint.sh. Bardzo ważną rzeczą jest odpalenie go dopiero jak wstanie SQL Server, z tego powodu wywołuję komendę sleep ustawioną na 15 sekund. Następnie odpalamy nasz skrypt SQL, który ma za zadanie postawienie schematu bazy danych. W tym celu uruchamiamy sqlcmd i przekazujemy do niego adres serwera, z którym chcemy się połączyć. Jako że skrypt odpala się już w kontenerze, będzie to localhost. Nie definiujemy bazy danych, więc domyślnie skrypt wykona się na bazie master. Następnie musimy przekazać credentiale, w celu zautentykowania się na serwerze. Ostatnim parametrem tej komendy jest -i ./InitDb.sql, to on wskazuje nam plik który chcemy wykonać. Warto pamiętać że wcześniej ustawiliśmy folder roboczy, na ten w którym znajdują się nasze skrypty z tego powodu odwołujemy się do obecnego folderu. Tak przygotowany skrypt pozwoli nam na automatyczne postawienie gotowej bazy danych.

Entity Framework

Aby EF mógł połączyć się z naszą bazą danych musimy go odpowiednio skonfigurować. Nie jest to żadne rocket science. Wystarczy w pliku Startup.cs w metodzie ConfigureServices dodać następujący wpis:

services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString(SqlServer)));

W tym miejscu ustawiamy connection string, za pomocą metody Configuration.GetConnectionString(). Pobiera ona z pliku appsettings.json connection string o nazwie jaki do niej przekazaliśmy. Pobrany w ten sposób, zostaje ustawiony na przekazanym do metody AddDbContext typie naszego DbContextu. Dzięki temu, aplikacja wie dokąd uderzać z jego poziomu. Wpis w appsettings jak i sam ConnectionString, prezentują się następująco:

ConnectionStrings: {
SqlServer: Server=sqlServer;Integrated Security=False;Database=STOCQRES;User=sa;Password=Your_password123;
}

W tym miejscu miałem problem połączyć się z kontenerem, ponieważ sieć którą automatycznie tworzy docker-compose nie pozwalała mi znaleźć kontenera po jego nazwie. Żeby się z nim połączyć musiałem łączyć się z adresem lokalnym mojego komputera 192.168.1.1. Dopiero ustawienie sieci jako bridge spowodowało, że bez problemu zamiast adresu lokalnego, mogłem podać nazwę kontenera, # SSMS

Jeżeli chcemy połączyć się z naszą bazą danych za pomocą Microsoft SQL Server Management Studio musimy podać następujące dane: Jako server name, tym razem podajemy localhost a po przecinku port z którym chcemy się połączyć. W moim wypadku, ze względu na to że wcześniej ustawiłem port 20, będzie to on. Dalej przekazujemy credentiale, te same co podaliśmy w definicji docker-compose. Może się okazać że przy próbie połączenia odrzuci nas z błędem mówiącym o braku możliwości zautoryzowania certyfikatu, aby tego uniknąć wystarczy rozwinąć Options. Tam w Connection Properties będzie chceckbox Trust server certificate. Tym sposobem powinniśmy dostać się do naszego serwera.

Podsumowanie

Spoglądając na to z boku, nie widać by miałyby zajść jakiekolwiek problemy. Niestety, różne rzeczy lubią płatać figle. U mnie największym problemem było podłączenie się jakkolwiek z bazą danych. Dopiero bardziej zagłębiając się w temat doszedłem do tego, dlaczego w tym miejscu powinniśmy się łączyć w ten sposób, a nie inaczej. Ale generalnie taka jest esencja naszej pracy, ciągła walka z nieoczekiwanymi problemami :) Mam nadzieję że ten mały referat pomoże komuś już bez problemów postawić ten kontener. Sam mam zamiar sprawdzić jak zachowa się to już w Azure i wtedy albo uzupełnię ten wpis, albo stworzę kolejny. Zobaczymy :) Jeżeli wystąpiłyby jakieś nieścisłości w tym co piszę, lub mielibyście jakiekolwiek problemy, śmiało zapraszam do komentarzy. Na pewno odpiszę :)