Работа с файлами

Прямо сейчас мы пишем новые главы самоучителя!
Эта страница находится в разработке.

Показать, что уже готово

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

Подготовка окружения

Подготовьте рабочее окружение согласно инструкциями главы “Подготовка окружения”, если это ещё не сделано.

Рабочее окружение работало, но перестало? Инструкции из этой главы не работают? Может помочь:

Работает ли Docker?

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим Docker:

sudo systemctl restart docker

Убедимся, что Docker запустился:

sudo systemctl status docker

Результат выполнения команды, если Docker успешно запущен:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2013888 (dockerd)
      Tasks: 36
     Memory: 100.3M
     CGroup: /system.slice/docker.service
             └─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"

Теперь проверим, что Docker доступен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Перезагружали компьютер после подготовки окружения?

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:

minikube tunnel --cleanup=true

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start --namespace werf-guide-app

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Случайно удаляли Namespace приложения?

Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:

kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<имя пользователя Docker Hub>' \
  --docker-password='<пароль пользователя Docker Hub>'

Результат успешного выполнения команды:

namespace/werf-guide-app created
secret/registrysecret created
Ничего не помогло, окружение или инструкции по-прежнему не работают?

Если ничего не помогло, то пройдите главу “Подготовка окружения” с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.

Подготовка репозитория

Обновим существующий репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rfT ~/werf-guide/guides/ .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Не работает? Попробуйте инструкции на вкладке “Начинаю проходить руководство с этой главы” выше.

Подготовим новый репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
if (-not (Test-Path ~/werf-guide/guides)) {
  git clone https://github.com/werf/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/ ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
test -e ~/werf-guide/guides || git clone https://github.com/werf/werf-guides ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/ ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rfT ~/werf-guide/guides/ .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Файлы, упомянутые в главе

  • .helm/templates/deployment.yaml
  • .helm/secret-values.yaml
  • .helm/values.yaml
  • package.json
  • app.js

В этой главе мы настроим в нашем базовом приложении работу с пользовательскими файлами. Для этого потребуется персистентное (постоянное) хранилище.

В идеале — нужно добиться, чтобы приложение было stateless, а данные хранились в S3-совместимом хранилище — например, AWS S3, Selectel S3 или MinIO. Это обеспечивает простое масштабирование, работу в HA-режиме и высокую доступность.

А есть какие-то способы кроме S3?

Первый и более общий способ — это использовать как volume хранилище NFS, CephFS или hostPath.

Мы не рекомендуем этот способ, потому что при возникновении неполадок с такими типами volume’ов они влияют на работоспособность контейнера и всего демона Docker в целом. Тогда могут пострадать приложения, не имеющие никакого отношения к вашему.

Более надёжный путь — пользоваться S3. Так мы используем отдельный сервис, который имеет возможность масштабироваться, работать в HA-режиме и иметь высокую доступность. Можно воспользоваться облачным решением вроде AWS S3, Google Cloud Storage, Microsoft Blobs Storage и т.д.

Природа Kubernetes (и оркестровки контейнеров в целом) такова, что если мы будем сохранять файлы в какой-либо директории у приложения (запущенного в Kubernetes), то после перезапуска контейнера все изменения пропадут.

Подключение S3

Данная настройка производится полностью в рамках приложения. Рассмотрим подключение к S3 на примере пакета aws-sdk.

$ npm install aws-sdk --save

… и настроим работу с S3 в приложении. Подключение:

// Connection to S3
var S3 = require('aws-sdk/clients/s3');

const S3_ENDPOINT = process.env.S3_ENDPOINT || "127.0.0.1";
const S3_PORT = Number(process.env.S3_PORT) || 9000;
const TMP_S3_SSL = process.env.S3_SSL || "false";
const S3_SSL = TMP_S3_SSL.toLowerCase() == "true";
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "SECRET123";
const S3_SECRET_KEY = process.env.S3_SECRET_KEY || "SECRET123";
const S3_BUCKET = process.env.S3_BUCKET || "avatars";
const S3_ZONE = process.env.S3_ZONE || "ru-1a";

let s3;
try {
  s3 = new S3({
    accessKeyId: S3_ACCESS_KEY,
    secretAccessKey: S3_SECRET_KEY,
    endpoint: S3_ENDPOINT,
    s3ForcePathStyle: true,
    region: S3_ZONE,
    apiVersion: 'latest'
  });
}catch(e){
  console.error(e.message);
}
// Connection to S3 var S3 = require('aws-sdk/clients/s3'); const S3_ENDPOINT = process.env.S3_ENDPOINT || "127.0.0.1"; const S3_PORT = Number(process.env.S3_PORT) || 9000; const TMP_S3_SSL = process.env.S3_SSL || "false"; const S3_SSL = TMP_S3_SSL.toLowerCase() == "true"; const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "SECRET123"; const S3_SECRET_KEY = process.env.S3_SECRET_KEY || "SECRET123"; const S3_BUCKET = process.env.S3_BUCKET || "avatars"; const S3_ZONE = process.env.S3_ZONE || "ru-1a"; let s3; try { s3 = new S3({ accessKeyId: S3_ACCESS_KEY, secretAccessKey: S3_SECRET_KEY, endpoint: S3_ENDPOINT, s3ForcePathStyle: true, region: S3_ZONE, apiVersion: 'latest' }); }catch(e){ console.error(e.message); }

И реализуем API-метод:

//// Generate file on S3 ////
app.get('/api/generate_report', function (req, res) {
  halt_if_errors(global_errors, res);

  var params = {
    Bucket: S3_BUCKET,
    Key: "report"+new Date().toISOString()+".txt",
    Body: new Date().toISOString()
  };

  s3.upload(params, (err, data) => {
    if (err) {
      console.log(err, err.stack);
      res.send(JSON.stringify({
        "result": "error",
        "comment": "File upload error: " + err.message
      }));
    } else {
      res.send(JSON.stringify({"result": true}));
    }
  });
});
//// Generate file on S3 //// app.get('/api/generate_report', function (req, res) { halt_if_errors(global_errors, res); var params = { Bucket: S3_BUCKET, Key: "report"+new Date().toISOString()+".txt", Body: new Date().toISOString() }; s3.upload(params, (err, data) => { if (err) { console.log(err, err.stack); res.send(JSON.stringify({ "result": "error", "comment": "File upload error: " + err.message })); } else { res.send(JSON.stringify({"result": true})); } }); });

Переменные окружения нужно будет прописать в объекте Deployment. В итоге файл deployment.yaml должен будет принять примерно следующий вид:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: basicapp
spec:
  selector:
    matchLabels:
      app: basicapp
  revisionHistoryLimit: 3
  strategy:
    type: RollingUpdate
  replicas: 1
  template:
    metadata:
      labels:
        app: basicapp
    spec:
      imagePullSecrets:
      - name: "registrysecret"
      containers:
      - name: basicapp
        command: ["node","/app/app.js"]
        image: {{ .Values.werf.image.basicapp }}
        workingDir: /app
        ports:
        - containerPort: 3000
          protocol: TCP
        env:
        - name: "SQLITE_FILE"
          value: "app.db"
        - name: "S3_ENDPOINT"
          value: "s3.selcdn.ru"
        - name: "S3_PORT"
          value: "9000"
        - name: "S3_SSL"
          value: "true"
        - name: "S3_ZONE"
          value: "ru-1a"
        - name: "S3_BUCKET"
          value: "test-container"
        - name: "S3_ACCESS_KEY"
          value: "47951_Clarisse"
        - name: "S3_SECRET_KEY"
          value: "i{z^e9WX"
apiVersion: apps/v1 kind: Deployment metadata: name: basicapp spec: selector: matchLabels: app: basicapp revisionHistoryLimit: 3 strategy: type: RollingUpdate replicas: 1 template: metadata: labels: app: basicapp spec: imagePullSecrets: - name: "registrysecret" containers: - name: basicapp command: ["node","/app/app.js"] image: {{ .Values.werf.image.basicapp }} workingDir: /app ports: - containerPort: 3000 protocol: TCP env: - name: "SQLITE_FILE" value: "app.db" - name: "S3_ENDPOINT" value: "s3.selcdn.ru" - name: "S3_PORT" value: "9000" - name: "S3_SSL" value: "true" - name: "S3_ZONE" value: "ru-1a" - name: "S3_BUCKET" value: "test-container" - name: "S3_ACCESS_KEY" value: "47951_Clarisse" - name: "S3_SECRET_KEY" value: "i{z^e9WX"

Конфигурирование приложения

Оставлять переменные окружения и настройки S3 прямо в helm-чарте — плохая практика. Чтобы иметь возможность конфигурировать разные значения для разных стендов (тестового, продакшн и т.п.) — воспользуйтесь подходами, описанными в главе “Конфигурирование инфраструктуры в виде кода”: ключи от S3 нужно унести в секретные значения (secret-values.yaml), а остальное — в настройки для чарта (values.yaml).

А можно подробнее?

Можно. У вас может получиться что-то вроде:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: basicapp
spec:
  selector:
    matchLabels:
      app: basicapp
  revisionHistoryLimit: 3
  strategy:
    type: RollingUpdate
  replicas: 1
  template:
    metadata:
      labels:
        app: basicapp
    spec:
      imagePullSecrets:
      - name: "registrysecret"
      containers:
      - name: basicapp
        command: ["node","/app/app.js"]
        image: {{ .Values.werf.image.basicapp }}
        workingDir: /app
        ports:
        - containerPort: 3000
          protocol: TCP
        env:
        - name: "SQLITE_FILE"
          value: "app.db"
        - name: "S3_ENDPOINT"
          value: {{ pluck .Values.global.env .Values.app.s3.endpoint | first | default .Values.app.s3.endpoint._default | quote }}
        - name: "S3_PORT"
          value: {{ pluck .Values.global.env .Values.app.s3.port | first | default .Values.app.s3.port._default | quote }}
        - name: "S3_SSL"
          value: {{ pluck .Values.global.env .Values.app.s3.ssl | first | default .Values.app.s3.ssl._default | quote }}
        - name: "S3_ZONE"
          value: {{ pluck .Values.global.env .Values.app.s3.zone | first | default .Values.app.s3.zone._default | quote }}
        - name: "S3_BUCKET"
          value: {{ pluck .Values.global.env .Values.app.s3.bucket | first | default .Values.app.s3.bucket._default | quote }}
        - name: "S3_ACCESS_KEY"
          value: {{ pluck .Values.global.env .Values.app.s3.login | first | default .Values.app.s3.login._default | quote }}
        - name: "S3_SECRET_KEY"
          value: {{ pluck .Values.global.env .Values.app.s3.password | first | default .Values.app.s3.password._default | quote }}
apiVersion: apps/v1 kind: Deployment metadata: name: basicapp spec: selector: matchLabels: app: basicapp revisionHistoryLimit: 3 strategy: type: RollingUpdate replicas: 1 template: metadata: labels: app: basicapp spec: imagePullSecrets: - name: "registrysecret" containers: - name: basicapp command: ["node","/app/app.js"] image: {{ .Values.werf.image.basicapp }} workingDir: /app ports: - containerPort: 3000 protocol: TCP env: - name: "SQLITE_FILE" value: "app.db" - name: "S3_ENDPOINT" value: {{ pluck .Values.global.env .Values.app.s3.endpoint | first | default .Values.app.s3.endpoint._default | quote }} - name: "S3_PORT" value: {{ pluck .Values.global.env .Values.app.s3.port | first | default .Values.app.s3.port._default | quote }} - name: "S3_SSL" value: {{ pluck .Values.global.env .Values.app.s3.ssl | first | default .Values.app.s3.ssl._default | quote }} - name: "S3_ZONE" value: {{ pluck .Values.global.env .Values.app.s3.zone | first | default .Values.app.s3.zone._default | quote }} - name: "S3_BUCKET" value: {{ pluck .Values.global.env .Values.app.s3.bucket | first | default .Values.app.s3.bucket._default | quote }} - name: "S3_ACCESS_KEY" value: {{ pluck .Values.global.env .Values.app.s3.login | first | default .Values.app.s3.login._default | quote }} - name: "S3_SECRET_KEY" value: {{ pluck .Values.global.env .Values.app.s3.password | first | default .Values.app.s3.password._default | quote }}

Несекретные значения — храним в values.yaml:

app:
  s3:
    endpoint:
      _default: "s3.selcdn.ru"
    port:
      _default: "9000"
    ssl:
      _default: "true"
    zone:
      _default: "ru-1a"
    bucket:
      _default: "test-container"
      production: "production-myapp-reports"
      staging: "staging-myapp-reports"
    login:
      _default: "47951_Clarisse"
      production: "61235_Malcolm"
      staging: "54563_Kaylee"
app: s3: endpoint: _default: "s3.selcdn.ru" port: _default: "9000" ssl: _default: "true" zone: _default: "ru-1a" bucket: _default: "test-container" production: "production-myapp-reports" staging: "staging-myapp-reports" login: _default: "47951_Clarisse" production: "61235_Malcolm" staging: "54563_Kaylee"

А секретные значения могут иметь, например, такой вид:

.helm/secret-values.yaml (расшифрованный) копировать имя копировать текст
app:
  s3:
    password:
      _default: "i{z^e9WX"
      production: "&Brc_Rzn4/7f2E]u"
      staging: ".^At8wE,k<kMk+x""
app: s3: password: _default: "i{z^e9WX" production: "&Brc_Rzn4/7f2E]u" staging: ".^At8wE,k<kMk+x""