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 -y2) Установка базовых инструментов:
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;
}
EOF2) Сборка генераторов (общая логика):
Вам нужно сгенерировать код под 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 internal1) Установим генераторы 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
}
EOF4) Зависимости Go:
go get github.com/hashicorp/consul/api@latest
go get google.golang.org/grpc@latest
go get google.golang.org/protobuf@latest5) Запуск:
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 proto2) Добавим зависимости в 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"
EOF3) 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(())
}
EOF4) Реализация сервера:
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(®).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(())
}
EOF5) Запуск:
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 consul2) Протобуф загрузка и клиент 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);
});
EOF3) Запуск:
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 и микросервисным системам.