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

Мощный Managed Kubernetes бесплатно и надолго (для экспериментов и не только)

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

Вступление

Многие знают про аттракцион необычайной щедрости от Oracle. В своем облаке они дают Always Free не только пару небольших машинок на AMD, но и мощный сервер на ARM. 4 vCPU и целых 24GB RAM!

Поскольку с ARM я раньше дела практически не имел (только Raspberry, но это другое), мне было интересно погонять на нем Kubernetes, посмотреть отличия, сильно ли сложнее искать образы для ARM и т.п.

Так что в этой статье расскажу основные моменты, с которыми столкнулся, где ошибался. И в качестве примера свяжу его с домом через Wireguard, настрою Nginx ingress controller + basic auth + LetsEncrypt, а также мониторинг на Grafana + VictoriaMetrics.

Установка кластера

Общее понимание, что можно получить бесплатно: Always Free Resources.

Создание аккаунта

Часто бывают жалобы типа «не проходит карточка». Где-то видел подсказку, что адрес нужно вводить в американском формате: НомерДома, Улица, apt. НомерКв.

Не знаю, насколько правда, но у меня действительно принялась виртуальная кредитка Tinkoff (привязанная к основной, но с принудительно очень ограниченным лимитом), которая раньше не проходила. Полгода назад я несколько карт перепробовал прежде, чем нашел подходящую. Теперь же зарегистрировалось с первого раза. Может быть их антифрод менее подозрительно относится к правильно выглядящим адресам…

И да, вы не ослышались. У меня уже полгода работает такой бесплатный сервер просто с Docker, а сейчас вот завел второй аккаунт (другой номер телефона, карты и ФИО, но тот же самый домашний IP) для экспериментов с Kubernetes.

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

Не люблю предустановленные настройки, адреса по умолчанию… Первым делом удалил VCN и создал новый. А также две сети: Public (можно добавлять публичные IP) и Private (публичным такой ресурс уже не сделать, только через балансировщик и т.п.).

Для Public нужно создать Internet Gateway и настроить маршрут на него.

Для Private – создать NAT Gateway и для этой приватной сети указать маршрут в Интернет через него. В противном случае «внутри» все работать будет, виртуалка создастся, но даже apt update не пройдет. Даже нода хотя и создастся, на нее можно буджет зайти по ssh, но Ready она не станет.

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

Containers & Artifacts | Create cluster. Опять же, Quick Create мне не нравится, я шел через Custom. Хотя вполне можно и Quick. Система создаст VCN, сети, IGW, но названия ресурсов будут некрасивые.

Здесь важно определиться, какой  доступ к кластеру вы хотите. Проще всего, конечно, и Kubernetes API endpoint, и будущие ноды размещать в Public subnet. Я же выбрал делать все приватным и только Load Balancer в Public.

Когда дело дошло до Node pool, выбрал VM.Standard.A1.Flex (тот самый ARM). Указал 4 CPU и 24GB RAM и что нужна только 1 нода. Можно и 2 по 2CPU/12GB, но для меня это менее эффективно и нецелесообразно. Выбор образов небогатый, взял Oracle Linux 8.

Здесь я сначала задал Availability Domain = EU-FRANKFURT-1-AD-2, но система очень долго висела в состоянии «кластер создан, node pool тоже, но ни одной ноды нет». Тупо не было требуемых ARM ресурсов. Через кнопку Scale поменял availability domain на AD-1, и все очень быстро создалось.

Размер Boot Volume сначала указал 100GB (вместо штатных 46GB), но в OS виделось все равно как 46GB. Расширил потом до 100G, но почему-то у ноды появились taint node.kubernetes.io/unreachable:NoSchedule. Удаление не помогало, перезагрузка тоже. Может временный сбой какой-то был (Scale до 2 создавал дополнительную ноду, но она оставлась NotReady), а может действительно нельзя так вмешиваться в работу managed кластера. Не стал разбираться, пересоздал со стандартным Boot volume.

Как расширял диск

fdisk /dev/sda # удалил раздел 3, создал заново с того же места до максимума, Remove LVM2 signature – Нет, сохранил, вышел, перезгарузился на всякий случай.

pvresize /dev/sda3

lvextend -l +100%FREE /dev/ocivolume/root
Size of logical volume ocivolume/root changed from 35.47 GiB (9081 extents) to <88.90 GiB (22758 extents)

xfs_growfs /dev/ocivolume/root

Ах да, в Compute | Instances эта виртуалка будет видна без значка Always Free. Не пугайтесь, это нормально, потому что ограничивается общее количество CPU/RAM в месяц. При круглосуточной работе это те самые 4CPU/24GB. Мможно и несколькими маленькими набрать. Или наоборот, взять в 4 раза мощнее, но гонять только 1 неделю.

И на всякий случай про диск – в первом аккаунте был инцидент, что несмотря на общий объем менее 200GB, за один диск копеечку начислили (из бюджета 250 которые даются на первые 30 дней). Оказалось, что я его создал, когда лимит был исчерпан, поэтому в Always Free он не попал. И не вернулся в него, когда потом я лишние удалил. Надо учитывать такую особенность.

Доступ

Поскольку извне никак в приватные сети не попасть, в Public subnet создал  Compute | Instances. Ну а что, дают целых 2 штуки VM.Standard.E2.1.Micro (1 OCPU, 1GB RAM, 0.48Gbps). Тут я уже Ubuntu 20.04 заказал.

В Security group добавил полный доступ с публичных адресов домашней подсети, чтобы потом не заморачиваться отдельно с SSH, Wireguard и т.п.

Сеть

Конечно, было бы красиво установить IPSEC прямо в облако. Но у меня дома не то что публичного IP нет, я еще и за парой Hide NAT. Поэтому для связи с внешними ресурсами (облаками, VPS) я использую Wireguard еще со времен, когда он появился на Mikrotik в виде беты.

Для Ubuntu

sudo -i
apt install wireguard
cd /etc/wireguard
umask 077

wg genkey | tee privatekey | wg pubkey > publickey

cat <<EOF > wg0.conf
[Interface]
Address = 192.168.16.4/24
SaveConfig = true
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE;
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE
ListenPort = 51820
PrivateKey = XXX=

[Peer]
PublicKey = YYY=
AllowedIPs = 192.168.16.1/32, 192.168.4.0/22
EOF

Разрешить форвардинг пакетов между интерфейсами

echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p

Важно: нужно поправить настройку VNIC у этой виртуальной машины.

И там Skip Source/Destination Check

Раньше нужно было перезагрузиться после установки. Сейчас вроде не обязательно, но не повредит.

wg-quick up wg0
dmesg -wT | grep wireguard
[Sun Feb  6 07:13:57 2022] wireguard: WireGuard 1.0.0 loaded. See www.wireguard.com for information.
wg-quick down wg0

Все работает, пусть будет автостарт

systemctl enable wg-quick@wg0.service
systemctl daemon-reload
systemctl start wg-quick@wg0

И нужно входящие UDP пакеты разрешить. И сохранить  правила iptables.

sudo iptables -I INPUT 1 -i ens3 -p udp --dport 51820 -j ACCEPT
sudo /sbin/iptables-save |  sudo tee /etc/iptables/rules.v4

После этого туннель поднимется (если «дома» все уже настроено), но только для этой VM. Чтобы стали доступны Kubernetes Endpoint и сама нода в этой приватной сети, нужно поправить таблицу маршрутизации. К «все в Интернет через NAT gateway» добавить «192.168 (которые я использую в примерах) на приватный адрес этой Ubuntu». Если забыли поправить Skip src check, на этом шаге вам напомнят.

Ну и нужно разрешить форвардинг между ens3 и wg0. Для разнообразия поправим непосредственно /etc/iptables/rules.v4, добавив пару правил

:InstanceServices - [0:0]
-A INPUT -i ens3 -p udp -m udp --dport 51820 -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p udp -m udp --sport 123 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited

# Add this
-A FORWARD -i ens3 -o wg0 -j ACCEPT
-A FORWARD -i wg0 -o ens3 -j ACCEPT

-A FORWARD -j REJECT --reject-with icmp-host-prohibited

Теперь из дома можно подключаться к ARM ноде по ее приватному адресу. Равно как и к Kubernetes API endpoint.

Управление Kubernetes

Ниже я покажу упрощенный вариант, но на этой виртуальной Ubuntu я делал все стандартно

Установка oci cli

sudo apt install python3-pip
sudo pip install oci-cli

Поскольку на Windows машине у меня уже был .oci/config с ключами, я его просто перенес на Ubuntu.

Поправил key_file=C:\Users\user\.oci\sessions\DEFAULT\oci_api_key.pem на key_file=/home/ubuntu/.oci/sessions/DEFAULT/oci_api_key.pem и пофиксил разрешения oci setup repair-file-permissions --file /home/ubuntu/.oci/config

Дальше стандартно

ClusterID=ocid1.cluster.oc1.eu-frankfurt-1.aaa…….
oci ce cluster create-kubeconfig --cluster-id $ClusterID --file $HOME/.kube/config --region eu-frankfurt-1 --token-version 2.0.0  --kube-endpoint PRIVATE_ENDPOINT

kubectl

kubectl можно стандартно через apt поставить. Я просто скачал для унификации с установкой на Oracle Linux ARM.

RELEASE="v1.21.5"
ARCH="amd64" # "arm64" – для запуска на ноде
sudo curl -L --remote-name-all https://storage.googleapis.com/kubernetes-release/release/${RELEASE}/bin/linux/${ARCH}/kubectl

sudo chmod +x kubectl
sudo install -o root -g root -m 0755 kubectl /usr/bin/kubectl
source <(kubectl completion bash)
echo "source <(kubectl completion bash)" >> $HOME/.bashrc

Еще люблю смотреть поды с IP адресами и нодой, но не люблю лишние колонки READINESS GATES и т.п., поэтому в .bashrc обычно добавляю функцию (а не alias, который не принимает аргументы типа namespace)

kgpod() { kubectl get po -o wide -o=custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount,IP:.status.podIP,NODE:.status.hostIP $*; }

Итак, kubectl работает. На облачной Ubuntu вполне прилично, а вот с домашней винды довольно долго отвечает. И это понятно – по умолчанию аутентификация выполняется через плагин. Т.е. запускается нечто, меня аутентифицирует в облаке по ранее сохраненнм ключам… Прямо на глаз задержки видны. Да и на другие линуксовые машинки (а у меня в планах связать кластеры) не хочется ставить oci cli.

Поэтому я переключился на «по старинке» через токен. При том, что кластер доступен только изнутри через Wireguard, не считаю это существенным снижением безопасности.

ServiceAccount

Как из bash сделать, и так понятно. Покажу, как это делал с Windows машины из powershell (в частности, для декодирования токена вместо base64  используется штатный certutil).

kubectl -n kube-system create serviceaccount sa-user

kubectl create clusterrolebinding sa-user-bind --clusterrole=cluster-admin --serviceaccount=kube-system:sa-user

$TOKENNAME=kubectl -n kube-system get serviceaccount/sa-user -o jsonpath='{.secrets[0].name}'

kubectl -n kube-system get secret $TOKENNAME -o jsonpath='{.data.token}' > token.tmp

certutil -decode token.tmp token

$TOKEN=cat token

kubectl config set-credentials sa-user --token=$TOKEN

kubectl config set-context --current --user=sa-user

Теперь и откликаться система стала намного быстрее, и на другие машины достаточно перенести .kube/config

Если файл с другим именем, просто указываем, какой именно нужен.

export KUBECONFIG=~/.kube/ociK8s

Monitoring

Подготовку закончили, пора уже что-то полезное развернуть.

Ingress Controller

Я, конечно, начал с Nginx Ingress Controller. При этом автоматически создался Network Load Balancer. Кстати, при большом желании на нем можно прокинуть TCP/6443 на kube-api и TCP/22 на ноду. Но это не спортивно.

Если захочется поработать непосредственно с ноды, можно helm и для ARM поставить

https://github.com/helm/helm/releases
wget https://get.helm.sh/helm-v3.8.0-linux-arm64.tar.gz
sudo install -o root -g root -m 0755 kubectl /usr/bin/kubectl
sudo install -o root -g root -m 0755 linux-arm64/helm /usr/bin/helm
rm -rf linux-arm64

Но я уже все дальше с домашней VM делал

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

helm repo update

helm search repo ingress
NAME                            CHART VERSION   APP VERSION     DESCRIPTION
ingress-nginx/ingress-nginx     4.0.16          1.1.1           Ingress controller for Kubernetes using NGINX a...

Для системных подов и мониторинга создам namespace, в него же установлю nginx

kubectl create ns sysmon # to install system and monitoring tools

helm install -n sysmon ingress-nginx ingress-nginx/ingress-nginx

Кстати, в результатах работы helm приводится не совсем корректный пример ingress. В нем отсутствует pathType, без него будут ругательства.

Metrics-server

Без него даже kubectl top pod -A не покажет

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.0/components.yaml

И нужно отредактировать конфигурацию (добавить выделенную строчку, у меня же все на самоподписанных сертифкатах)

kubectl -n kube-system edit deployment metrics-server
    spec:
      containers:
      - args:
        - --cert-dir=/tmp
        - --secure-port=4443
        - --kubelet-insecure-tls # ADD
        - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname

Victoria Metrics

Устанавливаем

Значительно быстрее Prometheus, поддерживает кучу всего другого. Например, принимает данные также и от telegraf / InfluxDB. В общем, очень нравится.

Тут уже нужно думать, где хранить данные. Я рассчитывал на встроенные провизионеры oracle.com/oci, blockvolume.csi.oraclecloud.com. Но при попытке запросить PVC 1Gi автоматически был создан том 50GB. Типа это минимум. Спасибо, не надо.

Можно поднять NFS на виртуалке вместе с Wireguard. Наверняка так и сделаю для других задач. Но если телеметрия пропадет, не страшно. свободные 30 гигов на ноде жальче :) Решил это хранить локально.

sudo mkdir -p /mnt/LoSt/vicmet # Local Storage :)

cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: server-volume-vicmet-victoria-metrics-single-server-0
  labels:
    type: local
spec:
  storageClassName: "local"
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/LoSt/vicmet"
EOF

Для PV указал длинное имя в точности, как будет назван PVC при развертывании helm chart. При совпадении имен они автоматически свяжутся.

helm repo add vm https://victoriametrics.github.io/helm-charts/
helm repo update
helm search repo vm/victoria-metrics-single -l | head -5

helm show values vm/victoria-metrics-single > vm_values_single.yaml
Это посмотреть параметры и указать ниже:

cat <<EOF | helm install vicmet vm/victoria-metrics-single -n sysmon -f -
server:
  persistentVolume:
    # -- Persistent volume annotations
    annotations:
      volume.beta.kubernetes.io/storage-class: "local"
    # -- Storage class name. Will be empty if not setted
    storageClassName: local
    size: 5Gi
  resources:
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 500m
      memory: 512Mi
  scrape:
    enabled: true
    configMap: ""
  extraLabels:
    appname: victoriametrics
EOF

Настройки скрапинга хранятся в configmap. Он создается автоматически, дополнить своими можно:

kubectl edit configmaps -n sysmon vicmet-victoria-metrics-single-server-scrapeconfig
    scrape_configs:
    - job_name: victoriametrics
      static_configs:
      - targets:
        - localhost:8428

# Свой фрагмент
    - job_name: node-XXX
      static_configs:
      - targets: ['XXX.spec.antn.in:19100']

   - job_name: kubernetes-apiservers

Подключаемся

Временно опубликую через ingress. Только не простой, а с basic authentication.

htpasswd -c auth vicmet

и дважды указываю пароль

kubectl create secret generic basic-auth-vicmet --from-file=auth -n sysmon 

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vicmet
  namespace: sysmon
  annotations:
    # type of authentication
    nginx.ingress.kubernetes.io/auth-type: basic
    # name of the secret that contains the user/password definitions
    nginx.ingress.kubernetes.io/auth-secret: basic-auth-vicmet
    # message to display with an appropriate context why the authentication is required
    nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required - Victoria Metrics'
spec:
  ingressClassName: nginx
  rules:
  - host: vicmet.antn.in
    http:
      paths:
      - backend:
          service:
            name: vicmet-victoria-metrics-single-server
            port:
              number: 8428
        path: /
        pathType: ImplementationSpecific
EOF

После обновления DNS (CNAME на LoadBalancer IP, кучи IP не надо, бесплатно только 6 дают) http://vicmet.antn.in спрашивает пароль и потом позволяет посмотреть таргеты, метрики…

Grafana

Еще немного осталось :)

Grafana я установлю не через helm, а просто подготовленным yaml.

Длинный, поэтому спойлер

sudo mkdir -p /mnt/LoSt/grafana
sudo chown 472 /mnt/LoSt/grafana

Без этого grafana не сможет писать в директорию и не запустится.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
  name: grafana-pv
  labels:
    type: local
spec:
  storageClassName: "local"
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/LoSt/grafana"
---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: grafana-pv
  namespace: sysmon
  labels:
    app: grafana
spec:
  storageClassName: "local"
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: grafana
  name: grafana
  namespace: sysmon
spec:
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      securityContext:
        fsGroup: 472
        supplementalGroups:
          - 0
      containers:
        - name: grafana
          image: grafana/grafana:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000
              name: http-grafana
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /robots.txt 
             port: 3000
              scheme: HTTP
            initialDelaySeconds: 10
            periodSeconds: 30
            successThreshold: 1
            timeoutSeconds: 2
          livenessProbe:
            failureThreshold: 3
            initialDelaySeconds: 30
            periodSeconds: 10
            successThreshold: 1
            tcpSocket:
              port: 3000
            timeoutSeconds: 1
          resources:
            requests:
              cpu: 250m
              memory: 750Mi
            limits:
              cpu: 500m
              memory: 1000Mi
          volumeMounts:
            - mountPath: /var/lib/grafana
              name: grafana-pv
      volumes:
        - name: grafana-pv
          persistentVolumeClaim:
            claimName: grafana-pv
---

apiVersion: v1
kind: Service
metadata:
  name: grafana
  namespace: sysmon
spec:
  ports:
    - port: 3000
      protocol: TCP
      targetPort: http-grafana
  selector:
    app: grafana
  sessionAffinity: None
  type: ClusterIP
---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
  namespace: sysmon
spec:
  ingressClassName: nginx
  rules:
  - host: grafana.antn.in
    http:
      paths:
      - backend:
          service:
            name: grafana
            port:
              number: 3000
        path: /
        pathType: ImplementationSpecific
EOF

LetsEncrypt

И финальный аккорд – настроим https с валидными сертификатами.

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.7.1/cert-manager.yaml

cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging # <--
spec:
  acme:
    # You must replace this email address with your own.
    # Let's Encrypt will use this to contact you about expiring
    # certificates, and issues related to your account.
    email: cert@antn.in
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Secret resource that will be used to store the account's private key.
      name: letsencrypt-staging
    # Add a single challenge solver, HTTP01 using nginx
    solvers:
    - http01:
        ingress:
          class: nginx
---

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod # <--
spec:
  acme:
    # You must replace this email address with your own.
    # Let's Encrypt will use this to contact you about expiring
    # certificates, and issues related to your account.
    email: cert@antn.in
    server: https://acme-v02.api.letsencrypt.org/directory # <--
    privateKeySecretRef:
      # Secret resource that will be used to store the account's private key.
      name: letsencrypt-prod
    # Add a single challenge solver, HTTP01 using nginx
    solvers:
    - http01:
        ingress:
          class: nginx
EOF

Сначала проверил на staging, потом переключился на prod. Из-за кеширования в браузере удобнее проверять в incognito mode. Еще можен понадобиться удалить прежний сертифкат (kubectl delete secret -n sysmon grafana-tls)

Соответственно обновленный ingress, теперь с сертификатом. Просто помимо секции tls добавили аннотацию, «призывающую» cert-manager.

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
  namespace: sysmon

  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod" # <--

spec:
  ingressClassName: nginx

  tls:
  - hosts:
    - grafana.antn.in
    secretName: grafana-tls

  rules:
  - host: grafana.antn.in
    http:
      paths:
      - backend:
          service:
            name: grafana
            port:
              number: 3000
        path: /
        pathType: ImplementationSpecific
EOF

Все подключилось. Добавил источник данных (тип prometheus, адрес/URL с именем сервиса, который мне helm chart victoria-metrics выдал)

Добавил первую попавшуюся на сайте графаны Dashboard ID 747 и при импорте указал вышенастроенный источник данных. Сразу отобразились данные:

К VictoriaMetrics TLS не прикручивал, ибо не нужно это публиковать. Также терзает сомнение, не помешает ли basic authentication подтверждению сертифката. Не разбирался еще.

 Заключение

Очень понравилось, что я вообще не задумавался, что нода на ARM. Ни в helm чартах, ни в обычных yaml. Только kubectl ставил под разные платформы (Windows, Oracle Linux8 arm64, Ubuntu 20.04 amd64), дальше все само.

Было бы интересно добавить в этот кластер еще и x86 ноду. Но выбрать бесплатную E1.Micrо в визарде невозможно. А развернуть на отдельной машинке и потом kubeadm join – вопрос, достойный разбирательства. Боюсь, Managed кластеру это не понравится.

Но какой-нибудь service mesh c домашним кубером со временем погоняю. Может, кстати, упростит решение давней проблемы – как публиковать «домашние» сервисы в Интернете (теперь через Ingress).

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

Публикации

Истории

Работа

DevOps инженер
43 вакансии

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань