We detected you are likely not from a Russian-speaking region. Would you like to switch to the international version of the site?

  Назад к списку статей

Создание и управление микросервисной архитектурой в Termux: сервисы на Go, Rust и Node.js, связь через gRPC и Consul‑service‑discovery

Termux — удобная среда для разработки и локального тестирования сервисов на мобильном устройстве или в контейнероподобной среде. При грамотной организации вы получаете мини‑платформу, где можно быстро собрать несколько микросервисов, связать их по gRPC и организовать service discovery через Consul.

В этой статье мы развернём архитектуру из трёх сервисов (Go, Rust, Node.js), настроим их взаимодействие через gRPC и реализуем discovery через Consul. Акцент будет на практических шагах: структуры проекта, сборка, запуск, конфигурация и типовые ошибки.

Архитектура и компоненты

Предлагаемая схема:

  • service‑a (Go) — сервис, который инициирует запросы к другим сервисам.
  • service‑b (Rust) — gRPC сервер, который предоставляет метод обработки запроса.
  • service‑c (Node.js) — gRPC клиент/обработчик для демонстрации межъязыкового взаимодействия.
  • Consul — каталог сервисов и discovery (регистрация узлов, проверка health, получение адресов).
  • gRPC — транспорт и контракты через protobuf.

Цель: чтобы клиентские сервисы находили адреса серверов через Consul, а не “зашивали” host/port в код.

Подготовка Termux: базовые зависимости

Ниже команды для установки инструментов в Termux. В зависимости от версии Termux и выбранного репозитория пакетов названия могут незначительно отличаться. Рекомендуется запускать команды последовательно.

1) Обновление пакетов:

pkg update && pkg upgrade -y

2) Установка базовых инструментов:

pkg install -y git curl wget unzip tar nano protobuf clang build-essential openjdk-17

Для генерации protobuf-кода дополнительно потребуется protoc и плагины под языки.

3) Установка Consul (вариант: бинарь):

Удобно скачивать актуальный консул-бинарь. Например, для архитектуры на мобильных устройствах часто подходит arm64.

CONSUL_VERSION=1.18.5
curl -L -o consul.zip https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_arm64.zip
unzip -o consul.zip
mv consul /data/data/com.termux/files/usr/bin/consul
chmod +x /data/data/com.termux/files/usr/bin/consul

Если у вашего окружения другая архитектура, поменяйте linux_arm64 на корректную.

Локальная сеть и взаимодействие процессов

Для корректного discovery и тестирования удобно иметь предсказуемые адреса. Если вы планируете подключать несколько устройств или контейнеров, используйте локальную сеть (например, Wi‑Fi) и обеспечьте доступность портов внутри неё. Для целей “обхода блокировок” VPN не используйте — это не требуется для локального discovery.

Создаём рабочее пространство

Соберём проекты в одном каталоге.

mkdir -p ~/microtermuz/{proto,go-service,rust-service,node-service}
cd ~/microtermuz

Протокол: protobuf и gRPC контракт

Создадим один набор контрактов: например, “Echo” или “ProcessRequest”. Для демонстрации возьмём метод Process, который принимает сообщение и возвращает ответ с метаданными.

1) Файл proto:

cat > proto/service.proto <<'EOF'
syntax = "proto3";

package demo.v1;

option go_package = "github.com/yourorg/microtermuz/go-service/gen/demo/v1;demov1";

service Processor {
  rpc Process(Request) returns (Response);
}

message Request {
  string message = 1;
  string from = 2;
}

message Response {
  string result = 1;
  string processed_by = 2;
}
EOF

2) Сборка генераторов (общая логика):

Вам нужно сгенерировать код под Go, Rust и Node.js. Методы установки зависят от того, чем вы пользуетесь (статические бинарники, cargo, npm). В рамках Termux обычно проще поставить нужные инструменты отдельно.

Примерный подход:

  • Go: protoc-gen-go и protoc-gen-go-grpc
  • Rust: tonic-build через crate или отдельный вызов
  • Node.js: grpc-tools или protoc-gen-grpc-js

Дальше покажем генерацию в каждом проекте.

Сервис на Go: service-a (gRPC клиент/инициатор)

Сначала подготовим Go‑проект.

cd ~/microtermuz/go-service
go mod init github.com/yourorg/microtermuz/go-service
mkdir -p gen/demo/v1
mkdir -p cmd/service-a
mkdir -p internal

1) Установим генераторы Go:

pkg install -y golang
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Убедитесь, что путь к бинарникам попадает в $PATH. Часто достаточно:

export PATH="$PATH:$(go env GOPATH)/bin"

2) Сгенерируем Go-код из proto:

protoc --go_out=. --go-grpc_out=. -I=../proto ../proto/service.proto

Если protoc ещё не установлен, нужно поставить его. В Termux проще использовать пакет:

pkg install -y protobuf

Обычно после этого protoc доступен.

3) Код клиента: discovery через Consul

Go-сервис будет запрашивать адрес “service-b” из Consul по имени сервиса. Реализация может быть минимальной: запросы к HTTP API Consul (или через готовый клиент).

Примерно:

cat > cmd/service-a/main.go <<'EOF'
package main

import (
  "context"
  "fmt"
  "log"
  "os"
  "time"

  "github.com/hashicorp/consul/api"
  "google.golang.org/grpc"
  "google.golang.org/grpc/credentials/insecure"

  demov1 "github.com/yourorg/microtermuz/go-service/gen/demo/v1"
)

func main() {
  consulAddr := envOr("CONSUL_ADDR", "http://127.0.0.1:8500")
  targetService := envOr("TARGET_SERVICE", "service-b")
  listenID := envOr("SERVICE_ID", "service-a-go")

  _ = listenID

  consulConfig := api.DefaultConfig()
  consulConfig.Address = consulAddr

  consulClient, err := api.NewClient(consulConfig)
  if err != nil {
    log.Fatalf("consul client: %v", err)
  }

  // Контекст для поиска и RPC
  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()

  // Service health check обычно проще фильтровать по health.
  // Для примера используем Catalog.Service, затем выбираем первый инстанс.
  entries, _, err := consulClient.Health().Service(targetService, "", true, &api.QueryOptions{})
  if err != nil {
    log.Fatalf("consul discovery failed: %v", err)
  }
  if len(entries) == 0 {
    log.Fatalf("no healthy instances for %s", targetService)
  }

  addr := fmt.Sprintf("%s:%d", entries[0].Service.Address, entries[0].Service.Port)
  if entries[0].Service.Address == "" {
    addr = fmt.Sprintf("%s:%d", entries[0].Node.Address, entries[0].Service.Port)
  }

  log.Printf("Discovered %s at %s", targetService, addr)

  conn, err := grpc.DialContext(ctx, addr,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithBlock(),
  )
  if err != nil {
    log.Fatalf("grpc dial: %v", err)
  }
  defer conn.Close()

  client := demov1.NewProcessorClient(conn)
  resp, err := client.Process(ctx, &demov1.Request{
    Message: "Hello from service-a (Go)",
    From:    "service-a-go",
  })
  if err != nil {
    log.Fatalf("rpc call: %v", err)
  }

  log.Printf("Response: result=%s processed_by=%s", resp.Result, resp.ProcessedBy)
}

func envOr(key, fallback string) string {
  v := os.Getenv(key)
  if v == "" {
    return fallback
  }
  return v
}
EOF

4) Зависимости Go:

go get github.com/hashicorp/consul/api@latest
go get google.golang.org/grpc@latest
go get google.golang.org/protobuf@latest

5) Запуск:

cd ~/microtermuz/go-service
go run ./cmd/service-a

Сервис на Rust: service-b (gRPC сервер + регистрация в Consul)

Rust‑сервис поднимет gRPC сервер и зарегистрируется в Consul как service-b.

1) Инициализация проекта:

cd ~/microtermuz/rust-service
cargo init --bin service-b
mkdir -p proto

2) Добавим зависимости в Cargo.toml:

cat >> Cargo.toml <<'EOF'

[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tonic = { version = "0.11", features = ["transport"] }
prost = "0.12"
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }

[build-dependencies]
tonic-build = "0.11"
prost-build = "0.12"
EOF

3) build.rs для генерации protobuf:

cat > build.rs <<'EOF'
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .build_server(true)
        .build_client(false)
        .compile(&["../proto/service.proto"], &["../proto"])?;
    Ok(())
}
EOF

4) Реализация сервера:

cat > src/main.rs <<'EOF'
use tonic::{transport::Server, Request, Response, Status};

pub mod demo {
    tonic::include_proto!("demo.v1");
}

use demo::processor_server::{Processor, ProcessorServer};
use demo::{Request as DemoRequest, Response as DemoResponse};

#[derive(Default)]
pub struct MyProcessor {}

#[tonic::async_trait]
impl Processor for MyProcessor {
    async fn process(&self, req: Request<DemoRequest>) -> Result<Response<DemoResponse>, Status> {
        let r = req.into_inner();
        let result = format!("Processed: {}", r.message);

        Ok(Response::new(DemoResponse {
            result,
            processed_by: "service-b-rust".to_string(),
        }))
    }
}

#[derive(serde::Serialize)]
struct AgentServiceRegistration {
    #[serde(rename = "Name")]
    name: String,
    #[serde(rename = "ID")]
    id: String,
    #[serde(rename = "Address")]
    address: String,
    #[serde(rename = "Port")]
    port: u16,
    #[serde(rename = "Tags")]
    tags: Vec<String>,
    #[serde(rename = "Check")]
    check: Option<serde_json::Value>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let consul_addr = std::env::var("CONSUL_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8500".to_string());
    let grpc_addr = std::env::var("GRPC_ADDR").unwrap_or_else(|_| "0.0.0.0:50051".to_string());

    // Разбиваем host:port
    let mut it = grpc_addr.split(':');
    let host = it.next().unwrap_or("0.0.0.0").to_string();
    let port: u16 = it.next().unwrap_or("50051").parse().unwrap_or(50051);

    // В Consul часто удобнее указывать адрес узла, доступный клиентам.
    // Для простоты возьмём host. В реальных сценариях замените на IP Termux/узла.
    let address = if host == "0.0.0.0" { "127.0.0.1".to_string() } else { host.clone() };

    let service_name = "service-b".to_string();
    let service_id = std::env::var("SERVICE_ID").unwrap_or_else(|_| "service-b-rust-1".to_string());

    // Регистрация в Consul
    let reg = AgentServiceRegistration {
        name: service_name,
        id: service_id.clone(),
        address: address.clone(),
        port,
        tags: vec!["grpc".to_string()],
        // Health check можно усложнить: grpc health или http.
        // Здесь оставим без Check, чтобы не углубляться.
        check: None,
    };

    let client = reqwest::Client::new();
    let url = format!("{}/v1/agent/service/register", consul_addr);
    client.post(url).json(&reg).send().await?.error_for_status()?;

    println!("Registered service-b in Consul. Starting gRPC server on {}", grpc_addr);

    let addr = grpc_addr.parse()?;
    Server::builder()
        .add_service(ProcessorServer::new(MyProcessor::default()))
        .serve(addr)
        .await?;

    Ok(())
}
EOF

5) Запуск:

cd ~/microtermuz/rust-service
CONSUL_ADDR=http://127.0.0.1:8500 GRPC_ADDR=0.0.0.0:50051 cargo run

Важно: если Go-сервис работает на другом сетевом интерфейсе/устройстве, укажите в Consul корректный Address (не 127.0.0.1).

Сервис на Node.js: service-c (gRPC клиент)

Node.js‑сервис будет демонстрировать ещё один клиент, чтобы показать межъязыковое взаимодействие.

1) Подготовим проект:

cd ~/microtermuz/node-service
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
npm install consul

2) Протобуф загрузка и клиент gRPC

Создадим index.js:

cat > index.js <<'EOF'
const path = require('path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const Consul = require('consul');

const CONSUL_ADDR = process.env.CONSUL_ADDR || 'http://127.0.0.1:8500';
const TARGET_SERVICE = process.env.TARGET_SERVICE || 'service-b';

async function main() {
  const consul = Consul({
    host: '127.0.0.1',
    port: 8500,
    // Если у вас CONSUL_ADDR не локальный — адаптируйте параметры host/port.
  });

  // Загружаем protobuf
  const packageDefinition = protoLoader.loadSync(path.join(__dirname, '../proto/service.proto'), {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
  });
  const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
  const Processor = protoDescriptor.demo.v1.Processor;

  // Service discovery через Consul
  const services = await new Promise((resolve, reject) => {
    consul.health.service(TARGET_SERVICE, '', { passing: true }, (err, result) => {
      if (err) return reject(err);
      resolve(result);
    });
  });

  if (!services || services.length === 0) {
    throw new Error(No healthy instances found for ${TARGET_SERVICE});
  }

  const svc = services[0].Service;
  const node = services[0].Node;

  const address = svc.Address || node.Address;
  const port = svc.Port;

  const target = ${address}:${port};
  console.log(Discovered ${TARGET_SERVICE} at ${target});

  const client = new Processor(target, grpc.credentials.createInsecure());

  client.Process(
    { message: 'Hello from service-c (Node.js)', from: 'service-c-node' },
    (err, response) => {
      if (err) {
        console.error('RPC error:', err);
        process.exit(1);
      }
      console.log(Response: result=${response.result}, processed_by=${response.processed_by});
    }
  );
}

main().catch(e => {
  console.error(e);
  process.exit(1);
});
EOF

3) Запуск:

CONSUL_ADDR=http://127.0.0.1:8500 TARGET_SERVICE=service-b node index.js

Если консул не видит сервисы из-за адресации, это обычно решается корректной конфигурацией Address при регистрации в Consul.

Разворачиваем Consul и регистрируем сервисы

Запуск Consul в режиме разработки (для локального теста):

consul agent -dev -bind=0.0.0.0 -client=0.0.0.0

В таком режиме Consul поднимется и будет доступен по http://127.0.0.1:8500 внутри среды Termux. Если вам нужно, чтобы клиенты обращались по адресу из локальной сети, корректно настройте -bind и -client, а также firewall/доступность портов.

Проверка в интерфейсе:

По умолчанию Consul в dev часто открывает UI: проверьте доступность на устройстве и в браузере. Если UI недоступен, используйте API:

curl http://127.0.0.1:8500/v1/catalog/services

Затем убедитесь, что service-b зарегистрирован.

gRPC адресация и типовые ошибки

  • Адрес в Consul неправильный: если при регистрации сервис-b указал 127.0.0.1, а клиент в другой среде — запросы не дойдут. Нужен адрес, доступный с точки зрения клиента.
  • Порты не проброшены: если сервис слушает на 0.0.0.0, но клиент обращается к неправильному IP — связи не будет.
  • Несовпадение protobuf пакета: важно, чтобы protobuf namespace (package demo.v1) и генерация совпадали между сервисами. Контракт должен быть единым.
  • Health checks: если вы будете фильтровать “passing only”, сервис без health check может не попадать в выдачу. В примерах выше это упрощено.

Управление жизненным циклом: запуск, остановка, обновления

Для удобства управления в Termux полезно держать процессы в отдельных сессиях (терминальных вкладках) и использовать переменные окружения. Рекомендуемый минимум:

  • Переменная CONSUL_ADDR для всех сервисов
  • Переменные GRPC_ADDR (для серверов) и TARGET_SERVICE (для клиентов)
  • Уникальные SERVICE_ID при регистрации в Consul

Пример запуска серверного Rust сервиса с ожидаемым адресом:

CONSUL_ADDR=http://127.0.0.1:8500 GRPC_ADDR=0.0.0.0:50051 SERVICE_ID=service-b-rust-1 cargo run

Пример запуска Go-клиента:

CONSUL_ADDR=http://127.0.0.1:8500 TARGET_SERVICE=service-b go run ./cmd/service-a

Наблюдаемость: логи и трассировка на старте

На ранней стадии достаточно:

  • Логировать найденный адрес из Consul (в Go и Node.js).
  • Логировать старт и идентификатор сервиса (в Rust).
  • Добавить корреляцию через поле from в protobuf (мы это уже заложили).

Дальше можно расширять: метрики, OpenTelemetry и централизованные логи. Но для целей “как собрать работающую архитектуру” важно сначала довести связность и discovery до стабильного состояния.

Практические советы по репозиторию и контрактам

Чтобы команда не “разъехалась” по версиям:

  • Храните proto/ как единый источник правды и используйте одинаковый файл в каждом сервисе.
  • Вводите версионирование методов/сообщений (например, demo.v1, далее demo.v2).
  • Старайтесь держать “контракт” неизменным: при изменениях добавляйте новые поля, а не меняйте существующие типы.

Заключение

Мы построили микросервисную архитектуру в Termux с тремя сервисами на разных языках: Go, Rust и Node.js. Связь реализована через gRPC, а адреса сервисов клиентам выдаёт Consul‑service‑discovery. Такой подход даёт понятную управляемость, снижает количество “зашитых” параметров и облегчает локальное тестирование распределённых сценариев.

Если хотите, чтобы мы помогли вам адаптировать пример под вашу сеть (корректная адресация в Consul, health checks, единый build процесс, скрипты запуска/остановки и наблюдаемость), обращайтесь в РыбинскЛАБ — мы ведём внедрения и обучение по Termux и микросервисным системам.

* Текст статьи подготовлен и структурирован с использованием технологий искусственного интеллекта. Проверен и доработан перед публикацией.

Нужна помощь с настройкой Termux, Linux и серверов?

Я оказываю ИТ-услуги: настройка серверов, автоматизация, безопасность, помощь с Linux и инфраструктурой. Материалы сайта — только в ознакомительных и образовательных целях.

Связаться со мной
Поддержать проект