Ta treść została automatycznie przetłumaczona z ukraińskiego.
Prędzej czy później prawie każdy projekt, który przechowuje pliki użytkowników - awatary i inne multimedia - staje przed tym samym pytaniem: czy naprawdę trzeba nadal płacić dostawcy chmury za każdy bajt ruchu i każde zapytanie HTTP?
Jest to szczególnie aktualne dla małych serwisów. Aplikacja już działa na własnym VPS, który ma znaczną ilość przestrzeni dyskowej, czasu procesora i przepustowości. W tym czasie osobny serwis do plików nadal nalicza opłaty za każde zapytanie GET, każdy gigabajt ruchu wychodzącego i dodatkowo - CDN przed nim.
W tym artykule opowiem na poziomie koncepcji, jak przenieść przechowalnię plików z Amazon S3 na własny serwer, nie przepisywując aplikacji i nie zmieniając logiki biznesowej. Przyjrzymy się architekturze, modelowi bezpieczeństwa, rzeczywistym problemom podczas migracji oraz liście kontrolnej, którą warto przejść przed ostatecznym wyłączeniem starego koszyka.
Dlaczego na poziomie koncepcji? Narzędzi jest wiele, a podejścia uniwersalne. Niedawno migrowałem pliki z Amazon S3 na SeaweedFS (kontener na tym samym serwerze, co aplikacja). Więc póki doświadczenie jest świeże - piszę.
Dlaczego w ogóle warto przeprowadzić migrację z S3
Amazon S3 to doskonała usługa. Jego główne zalety są dobrze znane: wysoka dostępność, praktycznie nieograniczone skalowanie i minimalna administracja.
Jednak za te zalety trzeba płacić, i to nie tylko za objętość przechowywanych plików.
Model płatności oparty jest na korzystaniu z usługi:
- każde zapytanie GET i PUT;
- każdy gigabajt ruchu wychodzącego;
- dodatkowe usługi, takie jak CDN.
Dla większości małych witryn jest to niezauważalne. Problem pojawia się, gdy obciążenie przestaje być kontrolowane.
Na przykład:
- skrypty lub hotlinki mogą w ciągu kilku godzin wygenerować setki tysięcy zapytań;
- CDN tylko zmniejsza obciążenie S3, ale sam również jest naliczany za zapytania i ruch;
- budżetowe alerty AWS informują o przekroczeniu wydatków, ale nie mogą ich automatycznie zatrzymać.
- Błąd w kodzie. Na przykład retry-storm w tle przetwarzania obrazów może prowadzić do ciągłych zapytań do S3. I to z rzeczywistego doświadczenia.
W rezultacie nawet mały incydent może przerodzić się w nieprzyjemny rachunek na koniec miesiąca.
Jeśli jednak Twoja aplikacja już działa na VPS z stałą opłatą, pojawia się logiczne pytanie: po co płacić osobno za przechowalnię plików, jeśli serwer ma wystarczająco dużo przestrzeni dyskowej i zasobów sieciowych?
Właśnie tutaj pojawia się pomysł własnej przechowalni zgodnej z S3.
Czym jest przechowalnia zgodna z S3
Sam fakt istnienia przechowalni zgodnych z S3 sprawia, że taka migracja jest stosunkowo prosta.
Dziś istnieje wiele otwartych realizacji przechowalni obiektów, które obsługują ten sam interfejs API, co Amazon S3:
- PUT Object
- GET Object
- ListObjects
- Wersja podpisu AWS 4
- standardowe SDK Amazon
Dla aplikacji oznacza to, że nadal działa przez ten sam klient (aws-sdk, fog-aws lub jakąkolwiek inną bibliotekę), a zmieniają się tylko:
- endpoint;
- klucz dostępu;
- sekretny klucz;
- parametr force_path_style (path_style).
Sam kod pracy z plikami pozostaje praktycznie niezmieniony.
Na rynku jest kilka jakościowych realizacji self-hosted. Różnią się one wydajnością, wymaganiami zasobów oraz dojrzałością realizacji API S3, ale zasada działania jest taka sama.
Dlatego migracja nie staje się przepisywaniem połowy aplikacji - wystarczy podmienić adres przechowalni.
Architektura: dlaczego zapis i odczyt powinny być różne
Najczęstszy błąd podczas przejścia - to po prostu otworzyć API S3 na zewnątrz i używać go jednocześnie do zapisu i odczytu.
Tak będzie działać.
Ale to zły pomysł.
Zapis plików i ich odczyt mają zupełnie różne wymagania dotyczące bezpieczeństwa.
Zapis
Pliki są przesyłane przez samą aplikację.
Działa ona wewnątrz prywatnej sieci, ma klucze dostępu i musi mieć pełny dostęp do API S3.
Żaden z tych portów nie powinien być dostępny z Internetu.
Odczyt
Użytkownicy muszą tylko pobierać już istniejące pliki.
W tym celu wystarczy zezwolić tylko na dwa metody HTTP:
- GET
- HEAD
Wszystko inne powinno być blokowane jeszcze przed dotarciem zapytania do przechowalni.
W rezultacie architektura wygląda mniej więcej tak:
┌────────────────────────────────┐
│ Twój serwer │
│ │
Aplikacja ──────▶│ Przechowalnia zgodna z S3 │
(wewnętrzna sieć) │ │
│ │ │
│ ▼ │
│ nginx (proxy tylko do odczytu) │
└─────────────┬──────────────────┘
│
assets.example.com
GET / HEAD
Takie podejście ma od razu kilka zalet:
- API zapisu jest całkowicie niedostępne z Internetu.
- Odwiedzający mogą tylko przeglądać pliki.
- W każdej chwili można dodać CDN bez zmiany aplikacji.
- Nginx może samodzielnie dodawać nagłówki cache'ujące i ograniczać niepożądane zapytania.
Innymi słowy, aplikacja i użytkownicy nie pracują już z tym samym interfejsem - każdy otrzymuje tylko te możliwości, które są mu naprawdę potrzebne.
Proxy tylko do odczytu: mały szczegół, który chroni całą przechowalnię
Zazwyczaj rolę takiego proxy pełni zwykły nginx. Nie przechowuje plików i nie wie nic o API S3 - jego jedynym zadaniem jest bezpieczne udostępnianie już istniejących obiektów.
Typowa konfiguracja wygląda mniej więcej tak:
server {
listen 80;
server_name _;
# Zabrania przeglądania listy plików.
location ~ /$ {
return 403;
}
location / {
limit_except GET HEAD {
deny all;
}
proxy_pass http://storage:8080/bucket-name/;
proxy_set_header Host $host;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
Konfiguracja jest niewielka, ale to w niej najczęściej kryją się błędy.
Błąd nr 1. Otwarty listing katalogów
Jeśli go pominiesz, zwykłe zapytanie na:
GET /
może zwrócić listę wszystkich obiektów w koszyku.
To nie oznacza, że haker będzie mógł coś usunąć lub przesłać. Ale uzyska:
- nazwy plików;
- rozmiary;
- daty utworzenia;
- strukturę katalogów.
W rzeczywistości sam umożliwisz pełne zindeksowanie swojej przechowalni plików.
Dlatego najprostsze rozwiązanie to całkowite zablokowanie zapytań do katalogów. Użytkownik musi znać pełną ścieżkę do konkretnego pliku. Jeśli ścieżki nie ma - niech otrzymuje 403 Forbidden.
Błąd nr 2. Cache-Control i parametr always
Jeszcze bardziej podstępnym problemem jest inny błąd.
Wiele przykładów konfiguracji nginx zaleca używanie:
add_header Cache-Control "public, max-age=31536000, immutable" always;
Na pierwszy rzut oka wydaje się to logiczne: niech wszystkie odpowiedzi będą buforowane w ten sam sposób.
Jednak jeśli przed serwerem działa CDN (na przykład Cloudflare), sytuacja może stać się bardzo nieprzyjemna.
Wyobraźmy sobie typowy scenariusz wdrożenia.
Nowy HTML został już opublikowany, ale plik JavaScript jeszcze nie zdążył trafić do nowej przechowalni.
Pierwszy użytkownik otwiera stronę.
Origin zwraca:
404 Not Found
Razem z tym nginx dodaje:
Cache-Control: public, max-age=31536000
CDN uczciwie wykonuje instrukcję i buforuje odpowiedź na rok.
Po kilku sekundach plik pojawia się w przechowalni.
Ale użytkownicy nadal otrzymują 404 z cache'u.
Z perspektywy aplikacji wszystko działa poprawnie.
Plik istnieje.
Origin go zwraca.
A CDN nawet nie sprawdza tego ponownie.
Dlatego Cache-Control warto dodawać tylko do udanych odpowiedzi, a nie do błędów.
Publiczna domena bez otwartych portów
Kolejna interesująca możliwość nowoczesnej infrastruktury - w ogóle nie otwierać serwera na zewnątrz.
Jeśli przed witryną działa CDN lub reverse proxy, prawie na pewno obsługuje on wyjściowe tunelowanie.
Zasada działania jest bardzo prosta.
Zamiast czekać na przychodzące połączenia TCP, serwer sam nawiązuje stałe wyjściowe połączenie z siecią dostawcy.
Dalszy ruch jest przesyłany przez to połączenie.
Z punktu widzenia Internetu wygląda to tak, jakby Twój serwer w ogóle nie istniał.
Skanowanie portów nic nie pokaże.
Żadnego otwartego HTTP.
Żadnego HTTPS.
Żadnego API S3.
Na zewnątrz widać tylko węzły brzegowe CDN.
Daje to od razu kilka zalet:
- Adres IP serwera origin jest ukryty;
- można całkowicie zamknąć zaporę dla przychodzących połączeń;
- znacznie trudniej przeprowadzać ataki DDoS bezpośrednio na serwer;
- przechowalnia jest niedostępna bezpośrednio nawet przy znajomości IP.
Dla domowego serwera lub małego VPS to chyba najprostszy sposób, aby uzyskać dodatkowy poziom ochrony praktycznie za darmo.
Przenoszenie danych: gdzie zaczynają się prawdziwe przygody
Na papierze wszystko wygląda bardzo prosto.
Bierzemy rclone.
Tworzymy dwa remote:
- Amazon S3;
- nową przechowalnię zgodną z S3.
Po tym uruchamiamy:
rclone sync s3:bucket storage:bucket
I czekamy.
W praktyce to właśnie tutaj zaczyna się większość niespodzianek.
Co więcej, prawie nigdy nie są one związane z samym rclone.
Zazwyczaj problemy pojawiają się z powodu drobnych różnic między realizacjami API S3.
Nowoczesne SDK czasami są „mądrzejsze”, niż to konieczne
W ostatnich latach biblioteki klienckie Amazon nauczyły się korzystać z nowych mechanizmów podpisywania zapytań.
Na przykład:
- streaming signatures;
- trailer checksums;
- dodatkowe kontrole integralności.
Amazon S3 doskonale to wszystko obsługuje.
Ale nie każda przechowalnia self-hosted.
W rezultacie można uzyskać bardzo dziwną sytuację.
Klucze dostępu są poprawne.
Czas jest zsynchronizowany.
Uprawnienia również są poprawne.
A serwer odpowiada:
SignatureDoesNotMatch
Dlatego łatwo zacząć szukać problemu w IAM, kluczach lub strefach czasowych, podczas gdy prawdziwa przyczyna jest zupełnie inna.
Jeśli napotkałeś podobne zachowanie, spróbuj wyłączyć nowe tryby podpisywania lub skorzystać z natywnego narzędzia importu samej przechowalni.
Czasami to oszczędza godziny poszukiwań nieistniejącego błędu.
Content-Type - drobnostka, która psuje obrazy
Kolejny niespodziewany problem - typy MIME.
Wiele narzędzi podczas kopiowania próbuje samodzielnie określić typ pliku.
Najczęściej analizują pierwsze bajty pliku.
Dla PNG lub JPEG działa to doskonale.
Ale nowoczesne formaty, takie jak AVIF lub HEIC, nie zawsze są poprawnie rozpoznawane.
W rezultacie plik trafia do nowej przechowalni z takim nagłówkiem:
Content-Type: application/octet-stream
Przeglądarka już nie wie, że to obraz.
W jednym przypadku zaproponuje pobranie pliku.
W innym - po prostu odmówi jego wyświetlenia.
Najlepszym rozwiązaniem jest w ogóle nie określać MIME-typu ponownie, a przenieść go razem z metadanymi oryginalnego obiektu.
To one są źródłem prawdy.
Wstępnie skompresowane pliki - osobna historia
Jeśli w przechowalni znajdują się pliki JavaScript, CSS lub inne pliki tekstowe, które podczas przesyłania były już skompresowane (gzip lub brotli), warto osobno sprawdzić, czy nowa przechowalnia poprawnie działa z odpowiednimi nagłówkami HTTP.
Przede wszystkim chodzi o:
- Content-Encoding
- Content-Type
Amazon S3 przechowuje te metadane bez niespodzianek. Ale niektóre realizacje self-hosted zachowują się mniej przewidywalnie - szczególnie jeśli pliki były przesyłane przez zewnętrzne narzędzia.
W rezultacie przeglądarka może otrzymać nie rozpakowany JavaScript, a dosłownie strumień skompresowanych bajtów.
Błąd wygląda dość dziwnie.
Zamiast oczekiwanego JavaScript konsola przeglądarki pokazuje coś w stylu:
Failed to load module script: Expected a JavaScript module but the server responded with application/octet-stream
lub
Unexpected token
Na pierwszy rzut oka wydaje się, że problem leży w budowie frontendu.
W rzeczywistości plik istnieje, ale serwer zwraca go z niepoprawnymi metadanymi.
Najprostsze rozwiązanie
Jeśli przed Twoim serwerem działa już CDN (Cloudflare lub podobny), wstępne kompresowanie plików często w ogóle nie jest potrzebne.
Nowoczesne CDN same kompresują treści tekstowe podczas wydawania:
- gzip;
- brotli;
- zstd (w zależności od klienta).
Dlatego rezygnacja z wstępnie skompresowanych plików często upraszcza infrastrukturę i eliminuje całą klasę potencjalnych problemów.
Nie wszystkie błędy są widoczne podczas kopiowania
Kiedy koszyk zawiera kilka tysięcy plików, migracja zazwyczaj przebiega bez przygód.
Ale jeśli jest ich dziesiątki lub setki tysięcy, pojawia się pokusa, aby maksymalnie zwiększyć równoległość.
To jest poprawne.
W przeciwnym razie kopiowanie może trwać wiele godzin.
Jednak istnieje inny problem.
Przy dużej liczbie równoczesnych zapytań część z nich niemal nieuchronnie zakończy się błędem.
Przyczyny mogą być różne:
- krótkotrwała awaria sieci;
- przeciążenie podsystemu dyskowego;
- tymczasowa niedostępność usługi;
- wewnętrzne timeouty.
W większości przypadków będą to dziesiąte lub nawet setne części procenta.
Brzmi to niegroźnie.
Ale jeśli przenosisz milion plików, nawet 0,05% to już setki brakujących obiektów.
Najbardziej niebezpieczne jest to, że niektóre narzędzia mogą nie traktować takich sytuacji jako krytycznych i kończyć pracę bez wyraźnego błędu.
Dlatego po zakończeniu migracji koniecznie trzeba sprawdzić wynik.
Nie wystarczy zaufać komunikatowi:
Transfer completed successfully
ale upewnić się, że wszystkie pliki rzeczywiście dotarły.
Sprawdzenie liczby plików - to nie formalność
Po zakończeniu kopiowania warto odpowiedzieć na bardzo proste pytanie:
Czy liczba obiektów w starym i nowym przechowalni jest taka sama?
Jeśli odpowiedź brzmi "tak" - świetnie.
Jeśli nie - nie spiesz się, aby uruchomić migrację ponownie.
Znacznie efektywniej jest znaleźć brakujące obiekty i wykonać jeszcze jedno przejście.
Ponowne kopiowanie całego koszyka dla kilku dziesiątek lub setek plików zazwyczaj tylko marnuje czas.
Dlatego końcowe "doładowanie" to całkowicie normalna praktyka nawet dla dużych firm.
Czasami kłamie nawet narzędzie do sprawdzania
Jest jeszcze jedna niespodzianka, o której łatwo zapomnieć.
Na bardzo dużych koszykach same narzędzia do liczenia mogą działać niepoprawnie.
Na przykład mogą:
- mieć wewnętrzny limit na liczbę zapisów;
- przycinać wynik po pewnym znaku;
- buforować listę obiektów.
W takiej sytuacji wydaje się, że po kolejnej synchronizacji liczba plików nie wzrosła.
Chociaż w rzeczywistości nowe obiekty są już od dawna obecne w przechowalni.
Jeśli widzisz dziwną statystykę, nie panikuj od razu.
Lepiej sprawdzić kilka konkretnych plików bezpośrednio.
Na przykład:
- czy URL się otwiera;
- czy zwraca poprawny Content-Type;
- czy suma kontrolna się zgadza.
Bardzo często okazuje się, że problem nie leży w migracji, ale w samym narzędziu, które pokazuje statystykę.
Najniebezpieczniejszy moment - przełączenie
Po udanym kopiowaniu wydaje się, że najtrudniejsza część jest już za nami.
W rzeczywistości nie.
Największe ryzyko pojawia się właśnie w momencie przełączania aplikacji na nową przechowalnię.
Bardzo często pojawia się pokusa, aby zrobić to stopniowo.
Na przykład:
- najpierw przełączyć odczyt;
- upewnić się, że wszystko działa;
- dopiero potem przełączyć zapis.
Brzmi logicznie.
Ale to jeden z najczęstszych błędów.
Dlaczego stopniowe przełączanie jest niebezpieczne
W większości aplikacji URL pliku jest generowany z dwóch części:
- klucza obiektu;
- podstawowej domeny przechowalni.
Jeśli odczyt i zapis zaczynają korzystać z różnych przechowalni, pojawia się bardzo nieprzyjemne okno niespójności.
Wyobraźmy sobie sytuację.
Użytkownik przesyła nowy awatar.
Aplikacja już zapisuje go w nowej przechowalni.
Ale strona profilu wciąż generuje URL starego S3.
Plik fizycznie istnieje.
Jednak użytkownik widzi tylko:
404 Not Found
Po kilku minutach inny użytkownik otwiera tę samą stronę.
U niego już wszystko działa.
W rezultacie problem wydaje się losowy, trudno go odtworzyć, a jeszcze trudniej wyjaśnić.
Poprawna kolejność przejścia
Znacznie bezpieczniej jest wykonać migrację w trzech etapach.
1. Całkowicie przygotować nową infrastrukturę
Rozwijamy:
- przechowalnię;
- nginx;
- CDN;
- publiczną domenę;
- monitoring.
Przy tym aplikacja nadal działa ze starym S3.
2. Przenieść wszystkie dane
Wykonujemy pełną synchronizację.
Bezpośrednio przed wdrożeniem uruchamiamy jeszcze jedną krótką synchronizację, aby przenieść pliki, które użytkownicy przesłali podczas pierwszego kopiowania.
To właśnie ta "delta" sprawia, że przełączenie jest praktycznie bezbolesne.
3. Jedno wdrożenie - jedna zmiana
Dopiero po tym aplikacja jednocześnie zmienia:
- endpoint do zapisu;
- endpoint do odczytu.
Nie dwie oddzielne zmiany.
Nie dwa różne wydania.
Jedno atomowe wdrożenie.
Dzięki temu praktycznie nie pozostaje momentu, w którym nowe pliki mogą być zapisywane w jednym miejscu, a odczytywane z innego.
Lista kontrolna przed wyłączeniem starego S3
Przed ostatecznym zrezygnowaniem z Amazon S3 warto przejść przez małą listę kontrolną. Większość punktów wydaje się oczywista, ale to właśnie one najczęściej stają się przyczyną problemów już po przełączeniu.
- Publiczna domena nie zwraca listy plików. Zapytanie do / lub do katalogu zwraca 403 Forbidden, a nie listę obiektów.
- Przez publiczną domenę nie można nic zapisać. Zapytania PUT, POST, DELETE i inne metody, poza GET i HEAD, są blokowane jeszcze na poziomie nginx.
- API S3 nie jest publiczne. Jeśli jest do niego dostęp, każde zapytanie musi wymagać poprawnej autoryzacji za pomocą AWS Signature V4.
- CDN rzeczywiście buforuje pliki. Sprawdź nagłówki odpowiedzi (CF-Cache-Status, Age lub podobne) i upewnij się, że ponowne zapytania nie docierają do origin.
- Content-Type wszystkich plików zgadza się z oryginałem. Szczególnie dotyczy to AVIF, WebP, SVG, czcionek i innych formatów, dla których niepoprawny MIME-typ może powodować problemy w przeglądarce.
- Nagłówki kompresji nie zniknęły. Jeśli używane są wstępnie skompresowane pliki, sprawdź Content-Encoding.
- Wszystkie obiekty zostały przeniesione. Liczba plików w starym i nowym przechowalni zgadza się, a losowa kontrola potwierdza, że pliki otwierają się i zwracają poprawne metadane.
- Wykonano finalną synchronizację. Przed samym wdrożeniem przeniesiono wszystkie pliki, które użytkownicy zdążyli przesłać podczas głównej migracji.
Jeśli wszystkie punkty są spełnione, to właśnie czas na przełączenie aplikacji.
Nie spiesz się z usuwaniem starego koszyka
Po udanym wdrożeniu pojawia się naturalna chęć natychmiastowego usunięcia starego koszyka i zaprzestania płacenia za niego.
Ja bym tego nie robił. Przynajmniej przez miesiąc lub dwa.
Amazon S3 to nie tylko miejsce, gdzie leżą pliki. To usługa o wysokiej odporności na awarie, replikacji i infrastrukturze, którą trudno odtworzyć na jednym VPS.
Natomiast własna przechowalnia, szczególnie jeśli działa na jednym serwerze, ma jedną punkt awarii.
Nawet jeśli regularnie tworzysz kopie zapasowe, pierwsze kilka tygodni po migracji warto zostawić stary koszyk jako dodatkowe ubezpieczenie.
Daje to od razu kilka zalet:
- można szybko znaleźć dowolny plik, jeśli pojawią się wątpliwości co do błędu podczas migracji;
- łatwo porównać metadane lub sumy kontrolne;
- w ostateczności zawsze można szybko wrócić do poprzedniej konfiguracji.
Po tym, jak nowe przechowalnia działa przez pewien czas bez problemów, a kopie zapasowe zostaną sprawdzone w praktyce, stary koszyk można spokojnie usunąć.
Czy w ogóle warto przeprowadzać migrację?
Jak w przypadku prawie każdego rozwiązania architektonicznego, to nie jest uniwersalna rekomendacja.
Jeśli Twój serwis obsługuje miliony użytkowników, korzysta z dziesiątek regionów AWS lub wymaga gwarantowanej dostępności, własna przechowalnia raczej nie będzie lepszą alternatywą.
Ale dla małych i średnich projektów sytuacja jest zupełnie inna.
Jeśli aplikacja już działa na własnym serwerze, a przechowalnia plików nie wymaga globalnej replikacji, przejście na rozwiązanie self-hosted może przynieść całkiem wymierne korzyści:
- stałe koszty zamiast naliczania za każde zapytanie;
- brak rachunków za ruch wychodzący;
- pełna kontrola nad infrastrukturą;
- niezależność od konkretnego dostawcy chmury.
Najprzyjemniejsze jest to, że dzięki zgodnemu z S3 API taka migracja nie wymaga przepisywania aplikacji. W większości przypadków wystarczy zmienić endpoint i dane logowania.
Na pierwszy rzut oka wydaje się, że przeniesienie przechowalni plików to po prostu kopiowanie danych z jednego koszyka do drugiego.
W rzeczywistości główną trudnością nie jest kopiowanie plików.
Najważniejsze jest prawidłowe zbudowanie architektury.
Oddzielenie zapisu od odczytu.
Nie otwieranie API S3 na zewnątrz.
Nie pozwalanie na listing katalogów.
Poprawne skonfigurowanie buforowania.
Sprawdzenie metadanych plików po migracji.
I dopiero po tym wykonanie jednego atomowego przełączenia aplikacji.
Jeśli wszystko zostanie zrobione w takiej kolejności, przejście zajmuje znacznie mniej czasu, niż wydaje się na początku. Zamiast tego otrzymujesz pełną kontrolę nad przechowalnią plików, przewidywalne koszty i infrastrukturę, którą możesz rozwijać niezależnie od stawek konkretnego dostawcy chmury.
Najważniejsze - nie traktować migracji jako prostego kopiowania plików. To przede wszystkim zmiana architektury. I to od tego, jak starannie zostanie przemyślana ta architektura, zależy, czy użytkownicy w ogóle zauważą migrację.
Podoba ci się?Zareaguj
🧵
Ten post nie ma jeszcze żadnych dodatków od autora.