Как стать автором
Обновить

Разворачиваем распределенное хранилище CEPH и подключаем его к Kubernetes

Время на прочтение13 мин
Количество просмотров39K


Часть 1 Разворачиваем среду для работы с микросервисами. Часть 1 установка Kubernetes HA на bare metal (Debian)


Здравствуйте, уважаемые читатели Хабра!


В прошлой публикации я рассказал, как развернуть отказоустойчивый кластер Kubernetes. Но дело в том, что в Kubernetes удобно деплоить stateless приложения, которым не требуется сохранять свое состояние или работать с данными. Но в большинстве случаев нам требуются сохранять данные и не терять их при рестартах подов.
Для этих целей в Kubernetes используются тома (volume). Когда мы работаем с облачными решениями Kubernetes, то проблем особо нет. Нам лишь нужно у Google, Amazon или иного облачного провайдера заказать требуемый объем и, руководствуясь документаций , подключить полученные тома к подам.
Когда же мы имеем дело с bare metal, тут дела обстоят немного сложнее. Сегодня я хочу рассказать об одном из решений основанном на использовании ceph.


В данной публикации я расскажу:


  • как развернуть распределенное хранилище Ceph
  • как использовать Ceph при работе с Kubernetes

Введение


Для начала я хотел бы объяснить для кого будет полезна эта статья. Во первых, для читателей, которые разворачивали кластер по моей первой публикации, чтобы продолжать выстраивать микросервисную архитектуру. Во вторых, для людей, которые хотят попробовать самостоятельно развернуть кластер ceph и оценить его работу.


В данной публикации я не буду затрагивать тему планирования кластера под какие либо нужды, я лишь расскажу общие принципы и понятия. Не буду углублятся в "тюнинг" и глубокую настройку, на эту тему есть много публикаций, в том числе и на хабре. Статья будет носить более ознакомительный характер, но при этом позволит получить рабочее решение, которое Вы в дальнейшем сможете адаптировать под Ваши нужды.


  1. Cписок хостов, ресурсы хостов, версии ОС и ПО
  2. Структура кластра ceph
  3. Настройка нод кластера перед установкой
  4. Установка ceph-deploy
  5. Создание кластера ceph
  6. Настройка сети
  7. Установка пакетов ceph
  8. Установка и инициализация мониторов
  9. Добавление OSD
  10. Подключение ceph к kubernetes
  11. Создание пула данных
  12. Создание client secret
  13. Деплой ceph rbd provisioner
  14. Создание storage class
  15. Тест работы связки kubernetes + ceph
  16. Список материалов используемых при подготовке статьи


Список хостов и системные требования


Name IP адрес Comment
ceph01-test 10.73.88.52 ceph-node01
ceph02-test 10.73.88.53 ceph-node02
ceph03-test 10.73.88.54 ceph-node03

При написании статьи я использую виртуальные машины вот такой конфигурации



На каждой установлена ОС Debian 9.5. Это тестовые машины, в каждой два диска, первый под ОС, второй для OSD цефа.


Разворачивать кластер я буду через утилиту ceph-deploy. Можно разворачивать кластер ceph и в ручном режиме, все шаги описаны в документации, но целью данной статьи является рассказать, как быстро можно развернуть ceph и начать использовать его в kubernetes.
Ceph довольно прожорлив к ресурсам особенно к оперативной памяти. Для хорошей скорости желательно использовать ssd диски.


Более подробно про требования можно почитать в официальной документации ceph



Структура кластра ceph


MON
Монитор — это демон, выполняющий роль координатора, с которого начинается кластер. Как только у нас появляется хотя бы один рабочий монитор, у нас появляется Ceph-кластер. Монитор хранит информацию о здоровье и состоянии кластера, обмениваясь различными картами с другими мониторами. Клиенты обращаются к мониторам, чтобы узнать, на какие OSD писать/читать данные. При разворачивании нового хранилища, первым делом создается монитор (или несколько). Кластер может прожить на одном мониторе, но рекомендуется делать 3 или 5 мониторов, во избежание падения всей системы по причине падения единственного монитора. Главное, чтобы количество оных было нечетным, дабы избежать ситуаций раздвоения сознания (split-brain). Мониторы работают в кворуме, поэтому если упадет больше половины мониторов, кластер заблокируется для предотвращения рассогласованности данных.
MGR
Менеджер Ceph демон работает вместе с демоном монитора, чтобы обеспечить дополнительный контроль.
С версии 12.x демон ceph-mgr стал необходим для нормальной работы.
Если демон mgr не запущен, вы увидите предупреждение об этом.

OSD (Object Storage Device)
OSD — это юнит хранилища, который хранит сами данные и обрабатывает запросы клиентов, обмениваясь данными с другими OSD. Обычно это диск. И обычно за каждый OSD отвечает отдельный OSD-демон, который может запускаться на любой машине, на которой установлен этот диск.


На каждой машине нашего кластера будут работать все три демона. Соответственно, демоны монитора и менеджера как служебные, и демон OSD для одного диска нашей виртуальной машины.



Настройка нод кластера перед установкой


В документации ceph указана следующая схема работы:



Я буду работать с первой ноды кластера ceph01-test она же и будет Admin Node, на ней же будут конфигурационные файлы для утилиты ceph-deploy. Для корректной работы утилиты ceph-deploy все узлы кластера должны быть доступны по ssh c Admin node. Я для удобства пропишу в hosts короткие имена для кластера


10.73.88.52     ceph01-test
10.73.88.53     ceph02-test
10.73.88.54     ceph03-tset

И скопирую ключи на остальные хосты. Все команды я буду выполнять из под root.


ssh-copy-id ceph02-test
ssh-copy-id ceph03-test

Документация по настройке


ceph-deploy

Установка ceph-deploy


Первым шагом установим ceph-deploy на машине ceph01-test


wget -q -O- 'https://download.ceph.com/keys/release.asc' | apt-key add -

Далее нужно выбрать релиз, который Вы хотите ставить. Но тут возникают сложности, в настоящее время ceph для ОС Debian поддерживают только пакеты luminous.
Если вы хотите поставить более свежий релиз то придется воспользоваться зеркалом, например
https://mirror.croit.io/debian-mimic/dists/


Добавляем репозиторий с mimic на всех трех нодах


apt install curl  apt-transport-https -y
curl https://mirror.croit.io/keys/release.gpg > /usr/share/keyrings/croit-signing-key.gpg
echo 'deb [signed-by=/usr/share/keyrings/croit-signing-key.gpg] https://mirror.croit.io/debian-mimic/ stretch main' > /etc/apt/sources.list.d/croit-ceph.list
apt update
apt install ceph-deploy

Если Вам достаточно luminous то можно воспользоваться официальными репозиториями


echo deb https://download.ceph.com/debian-luminous/ $(lsb_release -sc) main | tee /etc/apt/sources.list.d/ceph.list
apt-transport-https
apt update
apt install ceph-deploy

Так же установим NTP на всех трех нодах.


так как данная рекомендация есть в документации ceph

We recommend installing NTP on Ceph nodes (especially on Ceph Monitor nodes) to prevent issues arising from clock drift.


apt install ntp

Убедитесь, что вы включили службу NTP. Убедитесь, что каждый узел Ceph использует один и тот же сервер NTP. Более подробно посмотреть можно тут



Создание кластера ceph


Создадим каталог для конфиг файлов и файлов ceph-deploy


mkdir my-cluster
cd my-cluster

Создадим конфиг нового кластер, при создании укажем что в нашем кластере будет три монитора


ceph-deploy new ceph01-test ceph02-test ceph03-test


Настройка сети


Теперь важный момент, настало время поговорить о сети для ceph. Ceph для работы использует две сети public network и cluster network


Как видно из схемы public network это уровень пользователя и приложений, а cluster network это сеть по которой происходит репликация данных.
Очень желательно отделять эти две сети друг от друга. Также скорость сети cluster network желательна не меньше 10 Gb.
Конечно, можно все держать в одной сети. Но это черевато тем, что как только возрастет объем репликаций между OSD, например, при падении или добавлении новых OSD (дисков), то нагрузка на сеть ОЧЕНЬ сильно возрастет. Так что скорость и стабильность вашей инфраструктуры, будет сильно зависеть от сети используемой ceph.
На моем кластере виртуализации к сожалению нет отдельной сети, и я буду использовать общий сегмент сети.
Настройка сети для кластера производится через конфиг файл, который мы сгенерировали предыдущей командой.


/my-cluster# cat ceph.conf
[global]
fsid = 2e0d92b0-e803-475e-9060-0871b63b6e7f
mon_initial_members = ceph01-test, ceph02-test, ceph03-test
mon_host = 10.73.88.52,10.73.88.53,10.73.88.54
auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx

Как мы видим цеф деплой не создал нам настройки сети по умолчанию, по этому я добавлю в конфиг в раздел global параметр public network = {public-network/netmask}. Моя сеть 10.73.0.0/16 по этому после добавления мой конфиг будет выглядеть следующим образом


[global]
fsid = 2e0d92b0-e803-475e-9060-0871b63b6e7f
mon_initial_members = ceph01-test, ceph02-test, ceph03-test
mon_host = 10.73.88.52,10.73.88.53,10.73.88.54
public network = 10.73.0.0/16
auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx

Если Вы хотите отделить сеть cluster от public, то добавьте параметр cluster network = {cluster-network/netmask}
Более подробно про сети можно почитать в документации



Установка пакетов ceph


C помощью ceph-deploy установим все нужные нам пакеты ceph на наши три ноды.
Для этого на ceph01-test выполним
Если версия mimic то


ceph-deploy install --release mimic ceph01-test ceph02-test ceph03-test

Если версия luminous то


ceph-deploy install --release luminous ceph01-test ceph02-test ceph03-test

И подождем пока все установится.



Установка и инициализация мониторов


После того как все пакеты установлены, мы созданим и инициируем мониторы нашего кластера.
C ceph01-test выполняем следующее


ceph-deploy mon create-initial

В процессе будут созданы мониторы, запущены демоны, также ceph-deploy проверит quorum.
Теперь раскидаем конфиги по нодам кластера.


ceph-deploy admin ceph01-test ceph02-test ceph03-test

И проверим статус нашего кластера, если Вы все сделали правильно то статус должен быть
HEALTH_OK


~/my-cluster# ceph status
  cluster:
    id:     2e0d92b0-e803-475e-9060-0871b63b6e7f
    health: HEALTH_OK

  services:
    mon: 3 daemons, quorum ceph01-test,ceph02-test,ceph03-test
    mgr: no daemons active
    osd: 0 osds: 0 up, 0 in

  data:
    pools:   0 pools, 0 pgs
    objects: 0  objects, 0 B
    usage:   0 B used, 0 B / 0 B avail
    pgs:

Создадим mgr


ceph-deploy mgr create ceph01-test ceph02-test ceph03-test

И еще раз проверим статус


ceph -s 

Должна появится строчка


mgr: ceph01-test(active), standbys: ceph02-test, ceph03-test

Запишем конфиг на все хосты кластера


ceph-deploy admin ceph01-test ceph02-test ceph03-test


Добавление OSD


В данный момент мы имеем работающий кластер, но в нем еще нет дисков (osd в терминологии ceph) для хранения информации.


OSD можно добавить следующей командой (общий вид)


ceph-deploy osd create --data {device} {ceph-node}

В моем тестовом стенду под osd выделен disk /dev/sdb, по этому в моем случае команды будут следующие


ceph-deploy osd create --data /dev/sdb ceph01-test
ceph-deploy osd create --data /dev/sdb ceph02-test
ceph-deploy osd create --data /dev/sdb ceph03-test

Проверим что все OSD работают


ceph -s

Вывод


  cluster:
    id:     2e0d92b0-e803-475e-9060-0871b63b6e7f
    health: HEALTH_OK

  services:
    mon: 3 daemons, quorum ceph01-test,ceph02-test,ceph03-test
    mgr: ceph01-test(active)
    osd: 3 osds: 3 up, 3 in

Так же можете попробовать полезные команды для OSD


ceph osd df
ID CLASS WEIGHT  REWEIGHT SIZE    USE     AVAIL   %USE  VAR  PGS
 0   hdd 0.00490  1.00000 5.0 GiB 1.0 GiB 4.0 GiB 20.05 1.00   0
 1   hdd 0.00490  1.00000 5.0 GiB 1.0 GiB 4.0 GiB 20.05 1.00   0
 2   hdd 0.00490  1.00000 5.0 GiB 1.0 GiB 4.0 GiB 20.05 1.00   0
                    TOTAL  15 GiB 3.0 GiB  12 GiB 20.05

и


ceph osd tree
ID CLASS WEIGHT  TYPE NAME            STATUS REWEIGHT PRI-AFF
-1       0.01469 root default
-3       0.00490     host ceph01-test
 0   hdd 0.00490         osd.0            up  1.00000 1.00000
-5       0.00490     host ceph02-test
 1   hdd 0.00490         osd.1            up  1.00000 1.00000
-7       0.00490     host ceph03-test
 2   hdd 0.00490         osd.2            up  1.00000 1.00000

Если все ОК, то мы имеем работоспособный кластер ceph. В следующей части я расскажу как использовать ceph с kubernetes



Подключение ceph к kubernetes


К сожалению, я не смогу в рамках данной статьи подробно рассказать про работу томов Kubernetes, поэтому попробую уложиться в один абзац.
Для работы с томами данных данных Kubernetes использует storage classes, для каждого storage class есть свой provisioner, можно рассматривать его как некий "драйвер" для работы с различными томами хранения данных. Полный список который поддерживает kubernetes можно посмотреть в официальной документации.
В самом Kubernetes так же есть поддержка работы с rbd, но в официальном образе kube-controller-manager нет установленного клиента rbd, поэтому нужно использовать другой образ.
Также следует учесть, что тома (pvc) созданные как rbd могут быть только ReadWriteOnce (RWO) и, а это значит что созданный том Вы сможете примонтировать ТОЛЬКО к одному поду.


Для того что бы наш кластер смог работать с томами на ceph, нам нужно:
в кластере Сeph:


  • создать pool данных в кластере ceph
  • создать клиента и ключ доступа до пула данных
  • получить сeph admin secret

Для того что бы наш кластер смог работать с томами на ceph, нам нужно:
в кластере Сeph:


  • создать pool данных в кластере ceph
  • создать клиента и ключ доступа до пула данных
  • получить сeph admin secret

в кластере Kubernetes:


  • создать сeph admin secret и ключ клиента ceph
  • установить ceph rbd provisioner или изменить образ kube-controller-manager на образ который поддерживает rbd
  • создать secret c ключом клиента ceph
  • создать storage class
  • установить ceph-common на воркер нодах kubernetes


Создание пула данных


В кластере ceph создадим pool для томов kubernetes


ceph osd pool create kube 8 8

Тут я сделаю небольшое пояснение, цифры 8 8 в конце это количество pg и pgs. Эти значения зависят от размера Вашего кластера ceph. Есть специальные калькуляторы которые рассчитывают количество pg и pgs, например официальный от ceph
Для начала я рекомендую оставить по умолчанию, если что в будущем это количество можно увеличить (уменьшить можно только с версии Nautilus).



Создание клиента для пула данных


Создадим клиента для нового пула


ceph auth add client.kube mon 'allow r' osd 'allow rwx pool=kube'

Получим ключ для клиента, в дальнейшем он понадобится нам для создания secret kubernetes


ceph auth get-key client.kube
AQDd5aldka5KJRAAkpWTQYUMQi+5dfGDqSyxkg==

Получение ключа админа


И получим админский ключ


ceph auth get client.admin 2>&1 |grep "key = " |awk '{print  $3'}
AQAv+Itdx4DwKBAAKVhWRS3+eEPqV3Xrnlg9KA==

На кластере ceph все работы завершены и теперь нам нужно перейти на машину которая имеет доступ до кластера kubernetes
Я буду работать с master01-test (10.73.71.25) кластера развернутого мной в первой публикации.



Создание client secret


Создадим файл с клиентским токеном который мы получили (не забудьте заменить на свой токен)


echo AQDd5aldka5KJRAAkpWTQYUMQi+5dfGDqSyxkg== > /tmp/key.client

И создадим secret, который будем использовать в дальнейшем


kubectl create secret generic ceph-secret --from-file=/tmp/key.client --namespace=kube-system --type=kubernetes.io/rbd

Создание admin secret


Создадим файл с админским токеном (не забудьте заменить на свой токен)


echo AQAv+Itdx4DwKBAAKVhWRS3+eEPqV3Xrnlg9KA== > /tmp/key.admin

После этого создадим admin secret


kubectl create secret generic ceph-admin-secret --from-file=/tmp/key.admin --namespace=kube-system --type=kubernetes.io/rbd

Проверим что секреты создались


kubectl get secret -n kube-system | grep ceph
ceph-admin-secret                            kubernetes.io/rbd                     1      8m31s
ceph-secret                                      kubernetes.io/rbd                     1      7m32s


Способ первый деплой ceph rbd provisioner


Клонируем с github репозиторий kubernetes-incubator/external-storage, в нем есть все необходимое для того чтобы 'подружить' кластер kubernetes c хранилищем ceph.


git clone https://github.com/kubernetes-incubator/external-storage.git
cd external-storage/ceph/rbd/deploy/
NAMESPACE=kube-system
sed -r -i "s/namespace: [^ ]+/namespace: $NAMESPACE/g" ./rbac/clusterrolebinding.yaml ./rbac/rolebinding.yaml

kubectl -n $NAMESPACE apply -f ./rbac

Вывод


clusterrole.rbac.authorization.k8s.io/rbd-provisioner created
clusterrolebinding.rbac.authorization.k8s.io/rbd-provisioner created
deployment.extensions/rbd-provisioner created
role.rbac.authorization.k8s.io/rbd-provisioner created
rolebinding.rbac.authorization.k8s.io/rbd-provisioner created
serviceaccount/rbd-provisioner created

Способ второй: Замена образа kube-controller-manager


В официальном образе kube-controller-manager нет поддержки rbd, поэтому нам нужно будет изменить образ controller-manager.
Для этого на каждом из мастеров Kubernetes нужно отредактировать файл kube-controller-manager.yaml и заменить image на gcr.io/google_containers/hyperkube:v1.15.2. Обратите внимание на версию образа она должна соответствовать Вашей версии кластера Kubernetes.


vim /etc/kubernetes/manifests/kube-controller-manager.yaml

После этого нужно будет рестартовать kube-controller-manager


ubectl get pods -A  | grep manager
kube-system     kube-controller-manager-master01-test       1/1     Running   0          5m54s
kube-system     kube-controller-manager-master02-test       1/1     Running   0          5m54s
kube-system     kube-controller-manager-master03-test       1/1     Running   9111       103d

Поды должны обновиться автоматически, но если этого по какой либо причине не произошло то можно пересоздать их в ручную, через удаление.


kubectl delete pod -n kube-system kube-controller-manager-master01-test
kubectl delete pod -n kube-system kube-controller-manager-master02-test
kubectl delete pod -n kube-system kube-controller-manager-master03-test

Проверим что все ОК


kubectl describe pod -n kube-system kube-controller-manager-master02-test | grep Image:
    Image:         gcr.io/google_containers/hyperkube:v1.15.2

-



Создание storage class


Способ первый — если вы использовали provisioner ceph.com/rbd
Создадим yaml файл с описанием нашего storage class. Также все файлы используемые ниже можно скачать в моем репозитории в каталоге ceph


cat <<EOF >./storage-class.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: ceph-rbd
provisioner: ceph.com/rbd
parameters:
  monitors: 10.73.88.52:6789, 10.73.88.53:6789, 10.73.88.54:6789
  pool: kube
  adminId: admin
  adminSecretNamespace: kube-system
  adminSecretName: ceph-admin-secret
  userId: kube
  userSecretNamespace: kube-system
  userSecretName: ceph-secret
  imageFormat: "2"
  imageFeatures: layering
EOF

И задеплоим его в наш кластер


kubectl apply -f storage-class.yaml

Проверим что все ОК


kubectl get sc
NAME       PROVISIONER    AGE
ceph-rbd   ceph.com/rbd   7s

Способ второй — если вы использовали provisioner kubernetes.io/rbd
Создаем storage-class


cat <<EOF >./storage-class-hyperkube.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: ceph-rbd
provisioner: kubernetes.io/rbd
allowVolumeExpansion: true
parameters:
  monitors: 10.73.88.52:6789, 10.73.88.53:6789, 10.73.88.54:6789
  pool: kube
  adminId: admin
  adminSecretNamespace: kube-system
  adminSecretName: ceph-admin-secret
  userId: kube
  userSecretNamespace: kube-system
  userSecretName: ceph-secret
  imageFormat: "2"
  imageFeatures: layering
EOF

И задеплоим его в наш кластер


kubectl apply -f storage-class-hyperkube.yaml
storageclass.storage.k8s.io/ceph-rbd created

Проверим что все ОК


kubectl get sc
NAME       PROVISIONER         AGE
ceph-rbd   kubernetes.io/rbd   107s


Тест работы связки kubernetes + ceph


Перед тем как тестировать работу ceph + kubernetes, на КАЖДОЙ воркноде кластера нужно установить пакет ceph-common.


apt install curl apt-transport-https -y
curl https://mirror.croit.io/keys/release.gpg > /usr/share/keyrings/croit-signing-key.gpg
echo 'deb [signed-by=/usr/share/keyrings/croit-signing-key.gpg] https://mirror.croit.io/debian-mimic/ stretch main' > /etc/apt/sources.list.d/croit-ceph.list
apt update
apt install ceph-common

Создадим yaml файл PersistentVolumeClaim


cat <<EOF >./claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: claim1
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ceph-rbd
  resources:
    requests:
      storage: 1Gi
EOF

Задеплоим его


kubectl apply -f claim.yaml

Проверим что PersistentVolumeClaim создался.


bectl get pvc
NAME     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
claim1   Bound    pvc-d1e47825-289c-4201-acb8-033e62a3fe81   1Gi        RWO            ceph-rbd       44m

А также автоматически создался PersistentVolume.


kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM            STORAGECLASS   REASON   AGE
pvc-d1e47825-289c-4201-acb8-033e62a3fe81   1Gi        RWO            Delete           Bound    default/claim1   ceph-rbd                37m

Создадим один тестовый pod, в котором мы подключим созданный pvc в каталог /mnt. Пускай данный файл /mnt/test.txt c текстом "Hello World!"


cat <<EOF >./create-file-pod.yaml
kind: Pod
apiVersion: v1
metadata:
  name: create-file-pod
spec:
  containers:
  - name: test-pod
    image: gcr.io/google_containers/busybox:1.24
    command:
    - "/bin/sh"
    args:
    - "-c"
    - "echo Hello world! > /mnt/test.txt && exit 0 || exit 1"
    volumeMounts:
    - name: pvc
      mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
  - name: pvc
    persistentVolumeClaim:
      claimName: claim1
EOF

Задеплоим его и проверим, что он выполнил свою задачу


kubectl apply -f create-file-pod.yaml
kubectl get pods -w

Дождемся статуса


create-file-pod   0/1     Completed   0          16s

Создадим другой под, подключим к нему наш том но уже в /mnt/test, а после этого убедимся, что созданный первым томом файл на месте


cat <<EOF >./test-pod.yaml
kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: gcr.io/google_containers/busybox:1.24
    command:
    - "/bin/sh"
    args:
    - "-c"
    - "sleep 600"
    volumeMounts:
    - name: pvc
      mountPath: "/mnt/test"
  restartPolicy: "Never"
  volumes:
  - name: pvc
    persistentVolumeClaim:
      claimName: claim1
EOF

Выполним kubectl get po -w и дождемся пока pod будет в статусе running
После этого зайдем в него и проверим что том подключился и наш файл в каталоге /mnt/test


kubectl exec test-pod -ti sh
cat /mnt/test/test.txt
Helo world!

Спасибо за то что прочитали до конца. Извиняюсь за задержку публикации.
На все вопросы я готов ответить в личных сообщениях или в соцсетях, которые указаны у меня в профиле.
В следующей небольшой публикации я расскажу, как на базе созданного ceph кластера развернуть хранилище S3, и далее по плану из первой публикации.



Материалы используемые для публикации


Теги:
Хабы:
Всего голосов 9: ↑9 и ↓0+9
Комментарии38

Публикации