image
Это небольшая заметка на тему как запаковать vue.js приложение в Dockerfile и потом его запустить в контейнере в kubernetes'e.


Что делает


Я написал небольшую программку, которая генерирует номер свободного NodePort. Собственно ничего особо полезного она не делает, но можно не парится с поиском порта, ну и так для интереса посмотреть, как такое можно делать.


Начали


Весь проект состоит из друх частей — frontend и server. Фронтент спрашивает nodePort у сервера, а серверная часть через kubernetes api находит какой-нибудь свободный.
Собственно чтобы это всё работало в докере, надо вынести некоторые переменные из приложения, такие как адрес kubernetes api, порт, токен, итд.
Выглядит это так:


k8s-nodeport-gen/server/index.js:
var k8sInfo = {
  url: process.env.K8SURL,
  port: process.env.K8SPORT,
  timeout: process.env.K8STIMEOUT || '30',
  respath: process.env.RESSPATH || '/api/v1/services',
  token: process.env.K8STOKEN,
  nodePortStart: process.env.K8SPORTSTART || '30000',
  nodePortEnd: process.env.K8SPORTEND || '32000'
}
app.listen(process.env.PORT || 8081)

Cкажем, что всё протестировали и наше приложение работает.


Создаём докер образ


Те, кто работал с vue.js знает, что там куча всяких файлов, для чего они все нужны я не знаю, но видать нужны. Но благодаря тому, что есть такая вещь как vue-cli, всё можно довольно просто упаковать. Теперь всё пакуем:


npm run build

После этого у нас появится папка "dist" и файл "index.html" в "k8s-nodeport-gen/client". И для работы нам нужны только они. То есть по идее чтобы работал фронтенд надо какой-нибудь http сервер. Но в данном случае есть ещё и бэкенд, который тоже должен работать. По этому в моём случае как http сервер будет работать node.js express.
Файлы будут позде лежать в папке k8s-nodeport-gen/public. Для этого добавляем опцию в server/index.js


app.use(express.static(__dirname + '/public'))

Теперь когда разобрали с файлами можно создать Dockerfile. Нам из фронтенда надо только создать файлы для папки "dist". Для этого мы воспользуемся такой новомодной штукой, как multistage build.


FROM node:10-alpine AS base
COPY client /portgen/client
COPY server /portgen/server
WORKDIR /portgen
RUN cd client && npm i && npm run build

FROM node:10-alpine 
WORKDIR /portgen
COPY server/index.js /portgen/index.js
COPY server/package.json /portgen/package.json
COPY --from=base /portgen/client/dist ./public
RUN npm i

CMD ["/usr/local/bin/node", "/portgen/index.js"]

То есть в первом контейнере запускаем "npm run build", а во втором контейнере копируем файлы из "dist" в "public". В конце у нас получается образ в 95мб.
Теперь у нас есть docker образ, который я уже залил на hub.docker.com.


Запуск


Теперь мы хотим запустить этот образ в kubernetes'e, к тому же нам нужен токен, который может через kubernetes api видеть какие порты уже используются.


Для этого надо надо создать сервесный аккаунт, роль(можно использовать уже существующую) и rolebinding (не знаю как правильно перевести).
У меня в кластере уже есть кластерная роль "view"


ceku@ceku1> kubectl describe clusterrole view
Name:         view
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate=true
PolicyRule:
  Resources                                Non-Resource URLs  Resource Names  Verbs
  ---------                                -----------------  --------------  -----
  bindings                                 []                 []              [get list watch]
  configmaps                               []                 []              [get list watch]
  endpoints                                []                 []              [get list watch]
  events                                   []                 []              [get list watch]
  limitranges                              []                 []              [get list watch]
  namespaces                               []                 []              [get list watch]
  namespaces/status                        []                 []              [get list watch]
  persistentvolumeclaims                   []                 []              [get list watch]
  pods                                     []                 []              [get list watch]
  pods/log                                 []                 []              [get list watch]
  pods/status                              []                 []              [get list watch]
  replicationcontrollers                   []                 []              [get list watch]
  replicationcontrollers/scale             []                 []              [get list watch]
  replicationcontrollers/status            []                 []              [get list watch]
  resourcequotas                           []                 []              [get list watch]
  resourcequotas/status                    []                 []              [get list watch]
  serviceaccounts                          []                 []              [get list watch]
  services                                 []                 []              [get list watch]
  daemonsets.apps                          []                 []              [get list watch]
  deployments.apps                         []                 []              [get list watch]
  deployments.apps/scale                   []                 []              [get list watch]
  replicasets.apps                         []                 []              [get list watch]
  replicasets.apps/scale                   []                 []              [get list watch]
  statefulsets.apps                        []                 []              [get list watch]
  horizontalpodautoscalers.autoscaling     []                 []              [get list watch]
  cronjobs.batch                           []                 []              [get list watch]
  jobs.batch                               []                 []              [get list watch]
  daemonsets.extensions                    []                 []              [get list watch]
  deployments.extensions                   []                 []              [get list watch]
  deployments.extensions/scale             []                 []              [get list watch]
  ingresses.extensions                     []                 []              [get list watch]
  networkpolicies.extensions               []                 []              [get list watch]
  replicasets.extensions                   []                 []              [get list watch]
  replicasets.extensions/scale             []                 []              [get list watch]
  replicationcontrollers.extensions/scale  []                 []              [get list watch]
  networkpolicies.networking.k8s.io        []                 []              [get list watch]
  poddisruptionbudgets.policy              []                 []              [get list watch]

Теперь создадим аккаунт и rolebinding
account_portng.yml:


apiVersion: v1
kind: ServiceAccount
metadata:
  name: portng-service-get
  namespace: internal
  labels:
    k8s-app: portng-service-get
    kubernetes.io/cluster-service: "true"

rolebindng_portng.yml:


kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: internal
  name: view
  labels:
    k8s-app: portng-service-get
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
subjects:
- kind: ServiceAccount
  name: portng-service-get
  namespace: kube-system
  apiGroup: ""
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: ""

Теперь у нас есть аккаунт, а у него есть токен. Его название написано в аккаунте:


ceku@ceku1 /a/r/aditointernprod.aditosoftware.local> kubectl get serviceaccount portng-service-get -n internal -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2018-08-02T07:31:54Z
  labels:
    k8s-app: portng-service-get
    kubernetes.io/cluster-service: "true"
  name: portng-service-get
  namespace: internal
  resourceVersion: "7270593"
  selfLink: /api/v1/namespaces/internal/serviceaccounts/portng-service-get
  uid: 2153dfa0-9626-11e8-aaa3-ac1f6b664c1c
secrets:
- name: portng-service-get-token-vr5bj

Теперь надо только написать deploy, service, ingress для страницы. Начнём:
deploy_portng.yml


apiVersion: apps/v1beta1 # for versions before 1.6.0 use extensions/v1beta1
kind: Deployment
metadata:
  namespace: internal
  name: portng.server.local
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: portng.server.local
    spec:
      serviceAccountName: portng-service-get
      containers:
      - name: portgen
        image: de1m/k8s-nodeport-gen
        env:
        - name: K8SURL
          value: ceku.server.local
        - name: K8SPORT
          value: '6443'
        - name: K8STIMEOUT
          value: '30'
        - name: RESSPATH
          value: '/api/v1/services'
        - name: K8SPORTSTART
          value: '30000'
        - name: K8SPORTEND
          value: '32000'
        - name: PORT
          value: '8080'
        args:
        - /bin/sh
        - -c
        - export K8STOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) && node /portgen/index.js

Здесь надо обратить внимание на две вещи, это "serviceAccountName: portng-service-get" и токен для kubernetes'a, точнее говоря то, как я его добавил.


Теперь напишем сервис:
svc_portng.yml


apiVersion: v1
kind: Service
metadata:
  name: portng-server-local
  namespace: internal
spec:
  ports:
  - name: http
    port: 8080
    targetPort: 8080
  selector:
    app: portng.server.local

И ingress, для него у вас должен быть установлен ingress controller
ingress_portng.yaml:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: portng.aditosoftware.local
  namespace: internal
  annotations:
    kubernetes.io/ingress.class: "internal"
spec:
  rules:
  - host: portng.server.local
    http:
      paths:
      - path: /
        backend:
          serviceName: portng-server-local
          servicePort: 8080

Всё, осталось только закинуть файлы на сервер и запустить.
Всё это можно запустить и как докер контейнер и даже без него, но часть с аккаунтами в kubernetes'e всё равно придётся проходить.


Ресурсы:


Docker образ на hub.docker.com
Git репозиторий на github.com


Как вы видете, ничего особого в данной статье нету, но для некоторых я думаю будет интересно.

Комментарии (5)


  1. therhino
    02.08.2018 17:11

    Люди, вы еб***лись.


  1. Scott_Maison
    02.08.2018 18:27

    Хотелсоь бы узнать, зачем такие сложности с nodePort? Разве не достаточно использовать ingress-контроллер для связывания frontend — backend?


    1. de1m Автор
      02.08.2018 18:46

      Если вы про описанное тут приложение для nodePort, то как уже написано было выше, это просто чтобы попробывать.
      А если про nodePort в целом, то ingress controller используется всё таки больше для http/https соединений, через него можно конечно и tcp/udp пробрасывать, но это как-то по-моему не правильно, хотя я и делал такое, если к примеру был обязательно нужен ssh или ftp.


  1. GeneAYak
    03.08.2018 06:55

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


    1. de1m Автор
      03.08.2018 08:56

      Там не только статика, там ещё и бэкенд, который к kubernetes api обращается. Если бы была только статика, то тогда да, хватило бы и nginx или чего-то похожего.