werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

27 maja w sali głównej konferencji DevOpsConf 2019 odbywającej się w ramach festiwalu RIT++ 2019w ramach sekcji „Continous Delivery” został przekazany raport „werf – nasze narzędzie do CI/CD w Kubernetesie”. Mówi się o nich problemy i wyzwania, przed którymi staje każdy wdrażając rozwiązania na Kubernetesie, a także o niuansach, które mogą nie być od razu zauważalne. Analizując możliwe rozwiązania pokazujemy jak jest to realizowane w narzędziu Open Source werf.

Od czasu prezentacji nasze narzędzie (wcześniej znane jako dapp) osiągnęło historyczny kamień milowy 1000 gwiazdek na GitHubie — mamy nadzieję, że rosnąca społeczność użytkowników ułatwi życie wielu inżynierom DevOps.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Przedstawmy się więc wideo z raportu (~47 minut, znacznie więcej informacji niż artykuł) i główny wyciąg z niego w formie tekstowej. Iść!

Dostarczanie kodu do Kubernetes

Rozmowa nie będzie już dotyczyć werf, ale CI/CD w Kubernetesie, co sugeruje, że nasze oprogramowanie jest spakowane w kontenerach Docker (mówiłem o tym w Raport z 2016 roku), a K8 będą używane do uruchomienia go w środowisku produkcyjnym (więcej na ten temat w 2017 roku).

Jak wygląda dostawa w Kubernetesie?

  • Istnieje repozytorium Git z kodem i instrukcjami jego budowania. Aplikacja jest wbudowana w obraz Dockera i publikowana w Rejestrze Dockera.
  • W tym samym repozytorium znajdują się także instrukcje dotyczące wdrażania i uruchamiania aplikacji. Na etapie wdrożenia instrukcje te przesyłane są do Kubernetesa, który odbiera z rejestru żądany obraz i uruchamia go.
  • Poza tym zazwyczaj są testy. Niektóre z nich można wykonać podczas publikowania obrazu. Możesz także (postępując według tych samych instrukcji) wdrożyć kopię aplikacji (w osobnej przestrzeni nazw K8s lub w oddzielnym klastrze) i tam uruchomić testy.
  • Wreszcie potrzebujesz systemu CI, który odbiera zdarzenia z Git (lub kliknięcia przycisków) i wywołuje wszystkie wyznaczone etapy: budowanie, publikowanie, wdrażanie, testowanie.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Jest tu kilka ważnych uwag:

  1. Ponieważ mamy niezmienną infrastrukturę (infrastruktura niezmienna), obraz aplikacji, który jest używany na wszystkich etapach (staging, produkcja itp.), musi być jeden. Mówiłem o tym bardziej szczegółowo i na przykładach. tutaj.
  2. Ponieważ podążamy za infrastrukturą jako podejściem do kodu (IAC), powinien być kod aplikacji, instrukcja montażu i uruchomienia dokładnie w jednym repozytorium. Więcej informacji na ten temat zob ten sam raport.
  3. Łańcuch dostaw (dostawa) zwykle widzimy to tak: aplikacja została zmontowana, przetestowana, wydana (etap wydania) i tyle – dostawa nastąpiła. Ale w rzeczywistości użytkownik otrzymuje to, co wdrożyłeś, nie potem, kiedy dostarczyłeś to na produkcję i kiedy mógł tam pojechać i ta produkcja zadziałała. Dlatego uważam, że łańcuch dostaw się kończy dopiero na etapie operacyjnym (biegać)a dokładniej nawet w momencie wycofania kodu z produkcji (zastąpienia go nowym).

Wróćmy do powyższego schematu dostarczania w Kubernetesie: został wymyślony nie tylko przez nas, ale dosłownie przez wszystkich, którzy mieli do czynienia z tym problemem. W rzeczywistości ten wzorzec nazywa się teraz GitOps (możesz przeczytać więcej na temat tego terminu i idei, które się za nim kryją tutaj). Spójrzmy na etapy schematu.

Zbuduj scenę

Wydawałoby się, że o budowaniu obrazów Dockera można mówić w roku 2019, kiedy wszyscy wiedzą, jak pisać i uruchamiać pliki Dockerfile docker build?.. Oto niuanse, na które chciałbym zwrócić uwagę:

  1. Waga obrazu ma znaczenie, więc korzystaj wielostopniowapozostawić na obrazie tylko tę aplikację, która jest naprawdę niezbędna do operacji.
  2. Liczba warstw należy zminimalizować poprzez połączenie łańcuchów RUN-polecenia zgodnie ze znaczeniem.
  3. Jednak to dodaje problemów debugowanie, ponieważ w przypadku awarii zestawu należy znaleźć odpowiednie polecenie z łańcucha, który spowodował problem.
  4. Szybkość montażu ważne, ponieważ chcemy szybko wdrożyć zmiany i zobaczyć rezultaty. Na przykład nie chcesz odbudowywać zależności w bibliotekach językowych za każdym razem, gdy budujesz aplikację.
  5. Często z jednego repozytorium Git, którego potrzebujesz wiele obrazów, które można rozwiązać za pomocą zestawu plików Dockerfile (lub nazwanych etapów w jednym pliku) i skryptu Bash z ich sekwencyjnym montażem.

To był dopiero wierzchołek góry lodowej, przed którą wszyscy stoją. Ale są też inne problemy, w szczególności:

  1. Często na etapie montażu czegoś potrzebujemy uchwyt (na przykład buforuj wynik polecenia takiego jak apt w katalogu innej firmy).
  2. Chcemy Wiarygodne zamiast pisać w powłoce.
  3. Chcemy kompilacja bez Dockera (po co nam dodatkowa maszyna wirtualna, na której musimy do tego wszystko skonfigurować, skoro mamy już klaster Kubernetes, w którym możemy uruchamiać kontenery?).
  4. Montaż równoległy, co można rozumieć na różne sposoby: różne polecenia z pliku Dockerfile (w przypadku użycia wieloetapowego), kilka zatwierdzeń tego samego repozytorium, kilka plików Dockerfile.
  5. Montaż rozproszony: Chcemy gromadzić rzeczy w kapsułach, które są „efemeryczne”, ponieważ ich pamięć podręczna znika, co oznacza, że ​​należy ją przechowywać gdzieś osobno.
  6. Na koniec wymieniłem szczyt pragnień automagia: Idealnie byłoby udać się do repozytorium, wpisać jakieś polecenie i otrzymać gotowy obraz, złożony ze zrozumieniem, jak i co robić poprawnie. Jednak osobiście nie jestem pewien, czy wszystkie niuanse można w ten sposób przewidzieć.

A oto projekty:

  • moby/buildkit — konstruktor firmy Docker Inc (już zintegrowany z obecnymi wersjami Dockera), który próbuje rozwiązać wszystkie te problemy;
  • kaniko — kreator od Google, który pozwala budować bez Dockera;
  • Buildpacks.io — próba CNCF stworzenia automatycznej magii, a w szczególności ciekawe rozwiązanie z rebase dla warstw;
  • i mnóstwo innych narzędzi, np buduj, oryginalne narzędzia/img...

...i spójrz, ile mają gwiazdek na GitHubie. Czyli z jednej strony docker build istnieje i może coś zrobić, ale w rzeczywistości problem nie został całkowicie rozwiązany - dowodem na to jest równoległy rozwój alternatywnych kolektorów, z których każdy rozwiązuje jakąś część problemów.

Zgromadzenie w Werfie

Więc musimy werf (wcześniej sławny jak dapp) — Narzędzie open source firmy Flant, które tworzymy od wielu lat. Wszystko zaczęło się 5 lat temu od skryptów Bash optymalizujących montaż plików Dockerfiles, a przez ostatnie 3 lata pełnoprawny rozwój prowadzony był w ramach jednego projektu z własnym repozytorium Git (najpierw w Ruby, a potem przepisany do Go i jednocześnie zmieniono nazwę). Jakie problemy z montażem rozwiązuje się w werf?

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Problemy zaznaczone na niebiesko zostały już zaimplementowane, kompilacja równoległa została wykonana w ramach tego samego hosta, a problemy zaznaczone na żółto mają zostać ukończone do końca lata.

Etap publikacji w rejestrze (opublikować)

Wybraliśmy numer docker push... - co może być trudnego w przesłaniu obrazu do rejestru? I wtedy pojawia się pytanie: „Jaki tag umieścić na obrazie?” Powstaje z tego powodu, że mamy Gitflow (lub inna strategia Git) i Kubernetes, a branża stara się zapewnić, że to, co dzieje się w Kubernetesie, następuje po tym, co dzieje się w Git. W końcu Git jest naszym jedynym źródłem prawdy.

Co jest w tym tak trudnego? Zapewnij powtarzalność: z zatwierdzenia w Git, którego natura jest niezmienna (niezmienny), do obrazu platformy Docker, który powinien pozostać taki sam.

Dla nas jest to również ważne określić pochodzenie, ponieważ chcemy zrozumieć, z jakiego zatwierdzenia została zbudowana aplikacja działająca w Kubernetesie (wtedy możemy robić różnice i podobne rzeczy).

Strategie tagowania

Pierwsza jest prosta znacznik git. Mamy rejestr z obrazem oznaczonym jako 1.0. Kubernetes ma scenę i produkcję, do której przesyłany jest ten obraz. W Git dokonujemy zatwierdzeń i w pewnym momencie tagujemy 2.0. Zbieramy go zgodnie z instrukcją z repozytorium i wraz z tagiem umieszczamy w rejestrze 2.0. Wprowadzamy go na scenę i, jeśli wszystko jest w porządku, do produkcji.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Problem z tym podejściem polega na tym, że najpierw ustawiliśmy tag, a dopiero potem go przetestowaliśmy i wdrożyliśmy. Dlaczego? Po pierwsze, jest to po prostu nielogiczne: wystawiamy wersję oprogramowania, której jeszcze nawet nie testowaliśmy (inaczej nie możemy, bo żeby to sprawdzić, trzeba wstawić tag). Po drugie, ta ścieżka nie jest kompatybilna z Gitflow.

Druga opcja - git zatwierdzenie + tag. Gałąź master ma znacznik 1.0; dla niego w rejestrze - obraz wdrożony w środowisku produkcyjnym. Ponadto klaster Kubernetes ma kontury podglądu i przemieszczania. Następnie podążamy za Gitflow: w głównej gałęzi rozwoju (develop) wprowadzamy nowe funkcje, co skutkuje zatwierdzeniem z identyfikatorem #c1. Gromadzimy je i publikujemy w rejestrze przy użyciu tego identyfikatora (#c1). Z tym samym identyfikatorem wdrażamy do podglądu. To samo robimy z zatwierdzeniami #c2 и #c3.

Kiedy zdaliśmy sobie sprawę, że jest wystarczająco dużo funkcji, zaczynamy wszystko stabilizować. Utwórz oddział w Git release_1.1 (na bazie #c3 z develop). Nie ma potrzeby zbierania tego wydania, bo... zrobiono to w poprzednim kroku. Dlatego możemy po prostu wdrożyć go w fazie testowej. Naprawiamy błędy w #c4 i podobnie rozpocznij inscenizację. Jednocześnie trwa rozwój w develop, skąd okresowo pobierane są zmiany release_1.1. W pewnym momencie otrzymujemy skompilowane zatwierdzenie i przesłane do stagingu, z czego jesteśmy zadowoleni (#c25).

Następnie łączymy (z szybkim przewijaniem) gałąź wydania (release_1.1) w mistrzu. Umieściliśmy tag z nową wersją w tym zatwierdzeniu (1.1). Ale ten obraz jest już zgromadzony w rejestrze, więc aby go nie zbierać ponownie, po prostu dodajemy drugi tag do istniejącego obrazu (teraz ma on tagi w rejestrze) #c25 и 1.1). Następnie wdrażamy go na produkcję.

Wadą jest to, że do testowania przesyłany jest tylko jeden obraz (#c25), a w produkcji jest trochę inaczej (1.1), ale wiemy, że „fizycznie” są to te same obrazy z rejestru.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Prawdziwą wadą jest to, że nie ma obsługi zatwierdzeń scalających, trzeba przewijać do przodu.

Możemy pójść dalej i zrobić pewien trik... Spójrzmy na przykład prostego pliku Dockerfile:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Zbudujmy z niego plik według następującej zasady:

  • SHA256 z identyfikatorów użytych obrazów (ruby:2.3 и nginx:alpine), które są sumami kontrolnymi ich zawartości;
  • wszystkie zespoły (RUN, CMD i tak dalej.);
  • SHA256 z dodanych plików.

... i pobierz sumę kontrolną (ponownie SHA256) z takiego pliku. Ten podpis wszystko, co definiuje zawartość obrazu Dockera.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Wróćmy do diagramu i zamiast zatwierdzeń będziemy używać takich podpisów, tj. oznaczaj obrazy podpisami.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Teraz, gdy zachodzi potrzeba np. scalania zmian z wydania do mastera, możemy wykonać prawdziwe zatwierdzenie scalania: będzie ono miało inny identyfikator, ale ten sam podpis. Z tym samym identyfikatorem wdrożymy obraz do produkcji.

Wadą jest to, że teraz nie będzie można określić, jaki rodzaj zatwierdzenia został wypchnięty do produkcji - sumy kontrolne działają tylko w jednym kierunku. Problem ten rozwiązuje dodatkowa warstwa z metadanymi – więcej opowiem później.

Tagowanie w werf

W werf poszliśmy jeszcze dalej i przygotowujemy się do kompilacji rozproszonej z pamięcią podręczną, która nie jest przechowywana na jednej maszynie... Budujemy więc dwa typy obrazów Dockera, nazywamy je etap и obraz.

Repozytorium werf Git przechowuje instrukcje specyficzne dla kompilacji, które opisują różne etapy kompilacji (przed instalacją, zainstalować, przed konfiguracją, ustawienie). Zbieramy obraz pierwszego etapu z podpisem zdefiniowanym jako suma kontrolna pierwszych kroków. Następnie dodajemy kod źródłowy, dla nowego obrazu scenicznego obliczamy jego sumę kontrolną... Operacje te powtarzamy dla wszystkich etapów, w wyniku czego otrzymujemy zestaw obrazów scenicznych. Następnie wykonujemy ostateczny obraz, który zawiera także metadane dotyczące jego pochodzenia. I tagujemy ten obraz na różne sposoby (szczegóły później).

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Załóżmy, że po tym pojawi się nowe zatwierdzenie, w którym zmieniony został tylko kod aplikacji. Co się stanie? W przypadku zmian w kodzie zostanie utworzona łatka i przygotowany nowy obraz sceny. Jego podpis zostanie określony jako suma kontrolna starego obrazu scenicznego i nowego patcha. Z tego obrazu zostanie utworzony nowy ostateczny obraz. Podobne zachowanie będzie miało miejsce w przypadku zmian na innych etapach.

Zatem obrazy stołu montażowego stanowią pamięć podręczną, którą można przechowywać rozproszonie, a obrazy już z niej utworzone są przesyłane do rejestru platformy Docker.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Czyszczenie rejestru

Nie mówimy tu o usuwaniu warstw, które pozostały zawieszone po usuniętych tagach – jest to standardowa funkcja samego Rejestru Dockera. Mówimy o sytuacji, gdy kumuluje się dużo tagów Dockera i rozumiemy, że część z nich jest nam już niepotrzebna, ale zajmują miejsce (i/lub za to płacimy).

Jakie są strategie czyszczenia?

  1. Możesz po prostu nic nie robić nie sprzątaj. Czasami naprawdę łatwiej jest dopłacić trochę za dodatkową przestrzeń, niż rozwikłać ogromną plątaninę tagów. Ale to działa tylko do pewnego momentu.
  2. Pełny reset. Jeśli usuniesz wszystkie obrazy i odtworzysz tylko te bieżące w systemie CI, może pojawić się problem. Jeśli kontener zostanie ponownie uruchomiony w produkcji, zostanie dla niego załadowany nowy obraz - taki, który nie był jeszcze przez nikogo testowany. To zabija ideę niezmiennej infrastruktury.
  3. Niebieski zielony. Jeden rejestr zaczął się przepełniać - przesyłamy zdjęcia do drugiego. Ten sam problem co w poprzedniej metodzie: w którym momencie można wyczyścić rejestr, który zaczął się przepełniać?
  4. Według czasu. Usunąć wszystkie obrazy starsze niż 1 miesiąc? Ale na pewno znajdzie się usługa, która od miesiąca nie jest aktualizowana...
  5. ręcznie określić, co można już usunąć.

Istnieją dwie naprawdę realne opcje: nie czyścić lub kombinacja niebiesko-zielona + ręcznie. W tym drugim przypadku mówimy o następującej sytuacji: kiedy zrozumiesz, że czas wyczyścić rejestr, tworzysz nowy i dodajesz do niego wszystkie nowe obrazy w ciągu na przykład miesiąca. A po miesiącu sprawdź, które pody w Kubernetesie nadal korzystają ze starego rejestru i przenieś je również do nowego rejestru.

Do czego doszliśmy werf? Zbieramy:

  1. Git head: wszystkie tagi, wszystkie gałęzie - zakładając, że potrzebujemy wszystkiego, co jest otagowane w Git na obrazach (a jeśli nie, to musimy to usunąć w samym Gicie);
  2. wszystkie kapsuły, które są obecnie pompowane do Kubernetes;
  3. stare zestawy replik (które zostały niedawno wydane), a także planujemy przeskanować wydania Helma i wybrać tam najnowsze obrazy.

... i zrób z tego zestawu białą listę - listę obrazów, których nie usuniemy. Usuwamy wszystko inne, po czym znajdujemy osierocone obrazy sceniczne i również je usuwamy.

Etap wdrożenia

Rzetelna deklaratywność

Pierwszym punktem, na który chciałbym zwrócić uwagę podczas wdrożenia, jest wdrożenie zaktualizowanej konfiguracji zasobów, zadeklarowanej deklaratywnie. Oryginalny dokument YAML opisujący zasoby Kubernetesa zawsze bardzo różni się od wyniku faktycznie działającego w klastrze. Ponieważ Kubernetes dodaje do konfiguracji:

  1. identyfikatory;
  2. informacje serwisowe;
  3. wiele wartości domyślnych;
  4. sekcja z aktualnym statusem;
  5. zmiany wprowadzone w ramach webhooka wstępu;
  6. wynik pracy różnych kontrolerów (i planisty).

Dlatego gdy pojawi się nowa konfiguracja zasobów (nowa), nie możemy po prostu pobrać i zastąpić nim aktualnej, „żywej” konfiguracji (relacja na żywo). Aby to zrobić, będziemy musieli porównać nowa z ostatnio zastosowaną konfiguracją (ostatnio zastosowany) i zwiń relacja na żywo otrzymał poprawkę.

To podejście nazywa się Połączenie trójstronne. Jest używany na przykład w Helmie.

Istnieje również Połączenie trójstronne, który różni się tym, że:

  • porównanie ostatnio zastosowany и nowa, sprawdzamy, co zostało usunięte;
  • porównanie nowa и relacja na żywo, sprawdzamy, co zostało dodane lub zmienione;
  • do którego stosuje się zsumowaną poprawkę relacja na żywo.

Wdrażamy ponad 1000 aplikacji za pomocą Helma, więc faktycznie żyjemy w trybie łączenia dwukierunkowego. Ma jednak wiele problemów, które rozwiązaliśmy za pomocą naszych poprawek, które pomagają Helmowi normalnie działać.

Rzeczywisty stan wdrożenia

Gdy nasz system CI wygeneruje nową konfigurację dla Kubernetes na podstawie kolejnego zdarzenia, przesyła ją do użycia (stosować) do klastra — używając Helma lub kubectl apply. Następnie następuje opisane już scalanie N-way, na które Kubernetes API odpowiada z aprobatą systemowi CI, a następnie swojemu użytkownikowi.

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Jest jednak ogromny problem: w końcu pomyślna aplikacja nie oznacza pomyślnego wdrożenia. Jeśli Kubernetes zrozumie, jakie zmiany należy zastosować i zastosuje, nadal nie wiemy, jaki będzie wynik. Przykładowo aktualizacja i ponowne uruchomienie podów w frontendzie może się udać, ale nie w backendzie i otrzymamy różne wersje działających obrazów aplikacji.

Aby wszystko zrobić poprawnie, schemat ten wymaga dodatkowego łącza - specjalnego trackera, który będzie odbierał informacje o statusie z Kubernetes API i przesyłał je do dalszej analizy rzeczywistego stanu rzeczy. Stworzyliśmy bibliotekę Open Source w Go - kostkadog (zobacz jego ogłoszenie tutaj), który rozwiązuje ten problem i jest wbudowany w werf.

Zachowanie tego modułu śledzącego na poziomie werf konfiguruje się za pomocą adnotacji umieszczanych w Deployments lub StatefulSets. Główna adnotacja - fail-mode - rozumie następujące znaczenia:

  • IgnoreAndContinueDeployProcess — ignorujemy problemy związane z wdrożeniem tego komponentu i kontynuujemy wdrażanie;
  • FailWholeDeployProcessImmediately — błąd w tym komponencie powoduje zatrzymanie procesu wdrażania;
  • HopeUntilEndOfDeployProcess — mamy nadzieję, że ten komponent będzie działał do końca wdrożenia.

Na przykład ta kombinacja zasobów i wartości adnotacji fail-mode:

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Kiedy wdrażamy po raz pierwszy, baza danych (MongoDB) może nie być jeszcze gotowa - wdrożenie zakończy się niepowodzeniem. Możesz jednak poczekać na moment, aż się rozpocznie, a wdrożenie nadal będzie miało miejsce.

Istnieją jeszcze dwie adnotacje dla kubedoga w werf:

  • failures-allowed-per-replica — liczba dozwolonych upadków dla każdej repliki;
  • show-logs-until — reguluje moment do którego werf pokazuje (na stdout) logi ze wszystkich wyciągniętych podów. Wartość domyślna to PodIsReady (aby zignorować wiadomości, których prawdopodobnie nie chcemy, gdy ruch zacznie napływać do podu), ale wartości są również prawidłowe: ControllerIsReady и EndOfDeploy.

Czego jeszcze oczekujemy od wdrożenia?

Oprócz dwóch już opisanych punktów chcielibyśmy:

  • zobaczyć dzienniki - i tylko te niezbędne, a nie wszystko z rzędu;
  • ścieżka postęp, ponieważ jeśli zadanie zawiesza się „cicho” przez kilka minut, ważne jest, aby zrozumieć, co się tam dzieje;
  • mieć automatyczne cofanie na wypadek, gdyby coś poszło nie tak (dlatego znajomość prawdziwego statusu wdrożenia jest niezwykle istotna). Rollout musi być atomowy: albo dojdzie do końca, albo wszystko wróci do poprzedniego stanu.

Wyniki

Dla nas jako firmy, aby wdrożyć wszystkie opisane niuanse na różnych etapach dostawy (kompilacja, publikacja, wdrożenie), wystarczą system CI i narzędzie werf.

Zamiast konkluzji:

werf - nasze narzędzie do CI/CD w Kubernetes (przegląd i relacja wideo)

Z pomocą werf poczyniliśmy duże postępy w rozwiązywaniu dużej liczby problemów inżynierów DevOps i bylibyśmy zadowoleni, gdyby szersza społeczność przynajmniej wypróbowała to narzędzie w działaniu. Razem łatwiej będzie osiągnąć dobry wynik.

Filmy i slajdy

Film z występu (~47 minut):

Prezentacja raportu:

PS

Inne relacje o Kubernetesie na naszym blogu:

Źródło: www.habr.com

Dodaj komentarz