Данные, Golang, Python, Android, Docker, gRPC, Firebase, Cloud, BigQuery, о боже!

Воды. Агуа. H2O. Эликсир жизни. Зависть других планет.

Тем не менее, насколько важна эта божественная жидкость для нас, людей, мы, и под нами, я имею в виду я, обычно забываем ее потреблять. На момент написания я путешествую по Азии с рюкзаком. Среди волнений, удовольствий и приключений моей новой жизни мысль о питьевой воде почти не приходит мне в голову. И когда это происходит, я обычно не обращаю на это внимания, говоря себе: «Нет, со мной все будет хорошо. Это просто вода. Я все равно не хочу пить ». Но затем наступает ночь, и в конце того, что было еще одним успешным днем, приходят головные боли и усталость, напоминая мне, что я должен был снова налить свою бутылку. Очевидно, мне нужно было найти решение.

Конечно, для практикующего специалиста по обработке данных решение должно было включать данные. Но я хотел большего - я хотел чего-то преувеличенного. Мою проблему можно было легко решить, установив таймер или загрузив одно из множества приложений, связанных с питьем воды. Но нет - опять же, я хотел еще. Внезапно проблема стала больше вопросом о том, как далеко я могу зайти в своем решении, чем просто напоминанием мне немного освежить мою жизнь. Но более реальная причина, по которой я построил это, заключается в том, что я честно скучал по работе с такой системой. Когда я работал, я имел обыкновение иметь дело с данными, Golang, облачными и производственными системами каждый день. Теперь нет. Итак, я хотел собрать систему, в которой я мог бы использовать все эти инструменты (также я не хочу заржаветь, так как знаю, что мне придется произвести впечатление на рекрутеров, когда я решу попрощаться со своими приключениями на природе и замените их на офисное кресло).

Моя слишком сложная платформа, которую я назвал Team Aqua в честь знаменитой злодейской команды покемонов, которая хотела расширить море, уничтожить всю человеческую цивилизацию и вернуть мир в его первоначальное состояние (покемоны могут быть очень и очень жестокими), использует следующие платформы / услуги / компоненты:

  • Устройство Fitbit
  • API Fitbit
  • Один сервис Python
  • Две услуги на Голанге
  • gRPC (и, следовательно, Protobufs)
  • Firebase (служба обмена сообщениями)
  • Приложение для Android
  • Докер
  • Реестр контейнеров Google Cloud
  • Compute Engine Google Cloud
  • BigQuery
  • Студия данных Google

В этой статье я опишу, как я построил систему, как она работает и, конечно же, исходный код, лежащий в ее основе. Однако из соображений простоты и для того, чтобы эта статья была как можно более короткой и сфокусированной, я не буду объяснять все уголки платформы - например, что такое Gradle и почему он используется на Android. Тем не менее, в конце статьи вы найдете ссылку на репо системы.

Теперь к объяснению.

Обзор

Прежде чем я начну объяснять каждый компонент, я хочу дать вам небольшое резюме, показывающее архитектуру системы, все движущиеся части и то, что они делают. Начало этой прекрасной сказки происходит на моих часах Fitbit (или приложении). Здесь я собираюсь вводить каждый сеанс воды (назовем его так), генерируя в процессе данные, необходимые платформе (вот почему я называю это решением на основе данных). После регистрации водного сеанса эти данные будут отправлены в Fitbit и сохранены там.

Чтобы получить эти данные, у меня есть служба Python (называемая Water Retriever), которая общается с API Fitbit каждые X минут и получает все сеансы воды, которые я зарегистрировал сегодня. Затем клиент Python будет взаимодействовать через gRPC с серверной частью (написанной на Golang), которую я назвал Арчи в честь лидера Team Aqua. Роль Арчи - сохранять в памяти сеансы воды и записывать их в BigQuery.

Помимо Арчи, есть второй компонент Golang, который называется The Reminder. Этот маленький парень каждые Х минут спрашивает Арчи, сколько воды я выпил за это время. Если я не выпил ни капли воды за это время, оно отправит уведомление в приложение для Android, которое я написал как часть платформы, напоминая мне, что у меня не было воды последние X минут. В противном случае, если я выпил жидкость, в уведомлении будет указано, что я выпил Y мл. воды за последние X минут. Push-уведомление обрабатывается Firebase Cloud Messaging.

Эти три службы содержатся в одном образе Docker, который размещен в Реестре контейнеров Google Cloud. Упомянутый образ выполняется на «обычном» компьютере Compute Engine (не на кластере Kubernetes, Cloud Run и т. Д.). Наконец, чтобы проанализировать данные, поскольку они уже находятся в BigQuery, я просто выполняю несколько запросов и визуализирую их с помощью Google Data Studio.

Следующее изображение представляет собой диаграмму полной архитектуры.

Во-первых, я регистрирую данные о воде в Fitbit и отправляю их на их серверы. Затем Water Retriever (WR) собирает эти данные и отправляет их Арчи, который хранит данные и записывает копию в BigQuery. Рядом с ним находится Напоминание, которое получает данные от Арчи и отправляет уведомление, которое я в конечном итоге прочитал.

В маршруте ниже Арчи есть BigQuery, который предоставляет данные в Data Studio, чтобы я мог их проанализировать.

Теперь, без лишних слов, давайте рассмотрим детали.

Устройство Fitbit

Точкой входа в систему и местом, где генерируются данные, является мой Fitbit. Здесь каждый раз, когда я буду пить воду, я буду вводить ее количество. Как мне узнать точное количество? Ну, нет! Если я не пью из бутылки, на которой написан ее объем, я обычно просто оцениваю это.

Сервис Python (водный ретривер)

Первым из сервисов, которые я хочу представить, является Water Retriever, программа Python, которая запрашивает API Fitbit каждые X минут, чтобы узнать, сколько воды я потребил в текущий день. Для его разработки я использовал феноменальную библиотеку python-fitbit для взаимодействия с API. Это код:

import grpc
import fitbit
import os
import sys
import time
import logging
import app.api.v1.endpoint_pb2 as endpoint_pb2
import app.api.v1.endpoint_pb2_grpc as endpoint_pb2_grpc

from google.protobuf.timestamp_pb2 import Timestamp

waiting_time = 60 * 30


def run():
    starttime = time.time()
    # keep track of the last water consumption id
    last_log_id = 0

    client = fitbit.Fitbit(os.environ['FITBIT_KEY'], os.environ['FITBIT_SECRET'],
                           access_token=os.environ['ACCESS_TOKEN'],
                           refresh_token=os.environ['REFRESH_TOKEN'],
                           system='en_DE')

    with grpc.insecure_channel('localhost:50051') as channel:
        while True:
            print('iterating...')
            # result looks like this: {'summary': {'water': 500}, 'water': [{'amount': 500, 'logId': 6630477481}]}
            result = client.foods_log_water(date='today').get('water', None)

            print(result)
            # if no water has been consumed...
            if result is None or len(result) == 0 or result[-1]['logId'] == last_log_id:
                time.sleep(waiting_time -
                           ((time.time() - starttime) % waiting_time))
                continue

            stub = endpoint_pb2_grpc.DrinkWaterStub(channel)
            try:
                timestamp = Timestamp()
                timestamp.GetCurrentTime()
                response = stub.LogSplash(endpoint_pb2.Splash(
                    amount=result[-1]['amount'],
                    ts=timestamp
                ))
                print("Splash logged. Response: {}".format(response))
            except Exception as e:
                print(e)

            time.sleep(waiting_time -
                       ((time.time() - starttime) % waiting_time))
            print('end')
            last_log_id = result[-1]['logId']


if __name__ == '__main__':
    logging.basicConfig()
    run()

Основная функциональность моей программы заключается в одной функции, в которой всего 40 строк.

Вначале мы инициализируем переменную с текущим временем, а другую - с последним использованным идентификатором «logId» (с именем last_log_id) или идентификатором сеанса воды Fitbit.

Затем мы создаем клиент Fitbit, для которого требуется ключ Fitbit, секрет, токен доступа и токен обновления. Чтобы получить их, вы должны создать учетную запись Fitbit dev и зарегистрировать приложение. При создании клиента вы также можете указать язык (некоторые ответы API включают текст, который может быть пригоден для отображения) и локаль (или страну; хотя список довольно ограничен). Эффект от этого параметра - язык некоторых текстовых полей, которые включены в различные ответы API, и система единиц измерения. В моем случае я использую en_DE, так как мне нужны поля на английском языке и чтобы мои единицы были в метрической системе.

После создания клиента следующим шагом будет подключение к серверной службе gRPC (я опишу это в следующем разделе). Тогда попадете в бесконечный цикл. На каждой итерации система вызывает конечную точку «Получить журналы учета воды», чтобы получить как сводку, так и список записей о воде за данный день (сегодня). Типичный ответ выглядит так:

{‘summary’: {‘water’: 500}, ‘water’: [{‘amount’: 500, ‘logId’: 1234567890}]}

Сначала я проверяю, есть ли вообще какие-либо журналы или последний журнал из ответа совпадает с журналом, полученным системой ранее в прошлой итерации (идентификатор журнала сохраняется в last_log_id). Если любое из этих условий выполнено, мы ничего не делаем, и система переходит в спящий режим на 30 минут. С другой стороны, если «если» ложно, мы создаем экземпляр чего-то, что я определил как объект Splash (поверьте, я скоро объясню) - класс, состоящий из двух полей: количество и отметка времени.

В этом случае мы сохраним количество потребленной воды (в миллилитрах) в самом последнем журнале и текущей отметке времени. Затем мы вызовем метод gRPC LogSplash, используя в качестве параметра только что созданный объект Splash, чтобы отправить на серверную часть воду, потребляемую в это время. Как только это будет сделано, мы обновим last_log_id только что опубликованным logId и поспим 30 минут.

Вы могли заметить здесь серьезный недостаток. Что, если я зарегистрирую более одного сеанса воды в течение 30 минут? В этом случае, к сожалению, мы пропустим их все, кроме последнего сеанса, поскольку система просматривает только самый последний сеанс. Это то, что я исправлю в следующей итерации. Однако, честно говоря, я почти уверен, что не буду пить больше одного раза за этот период!

Вот и все, что касается водного ретривера. Теперь посмотрим, что произойдет, когда серверная часть получит Splash.

Арчи и gRPC API

Archie, одна из серверных служб системы, написанная на Go, отвечает за отслеживание (в памяти) сеансов воды, отныне известных как Splash. Но прежде чем я доберусь туда, я хочу описать службу API моего проекта, основанную на gRPC.

Чтобы освежить вашу память, я быстро скажу, что gRPC - это фреймворк RPC (удаленный вызов процедур). Проще говоря, это протокол связи для выполнения процедуры (в данном случае функций) от одной системы к другой. Что мне действительно нравится в gRPC, так это то, что он определяется с помощью протокольных буферов (также известных как Protobuf), « не зависящего от языка, платформы, расширяемого механизма для сериализации структурированных данных .». здесь заключается в том, что после того, как служба и объекты определены в Protobuf, вы можете использовать один из многих генераторов кода, чтобы преобразовать их в один из многих языков, которые он поддерживает.

Все это немного сложно понять, но, надеюсь, код прояснит ситуацию:

syntax = "proto3";

package api;

import "google/protobuf/timestamp.proto";

service DrinkWater {
  rpc LogSplash(Splash) returns (LogSplashResponse);
  rpc WaterConsumed(Since) returns (WaterConsumedSince);
}

message LogSplashResponse {
	bool ok = 1;
	string error = 2;
}

message Splash {
	google.protobuf.Timestamp ts = 1;
	int32 amount = 2;
}

message Since {
	google.protobuf.Timestamp ts = 1;
}

message WaterConsumedSince {
	int32 amount = 1;
}

Это файл Protobuf проекта. В первых трех строках я просто устанавливаю синтаксис, имя пакета и импортирую внешнюю библиотеку, связанную со временем, которую мне нравится использовать. Затем я определяю службу gRPC с именем DrinkWater. Как вы можете видеть, служба состоит из двух методов: LogSplash и WaterConsumed. Первый, LogSplash, который мы видели ранее, принимает в качестве параметра Splash и возвращает LogSplashResponse, а второй, WaterConsumed, принимает объект Since и возвращает WaterConsumedSince (подробности я скоро получу). Но что это за вся эта вода и брызги-брызги?

Сразу после службы вы найдете определение этих сообщений. Первое из них, Splash, содержит два поля: объект Timestamp (то, что я импортировал) и целое число (вода в мл). Затем идет LogSplashResponse, который состоит из логического значения, которое будет ложным, если ошибка произошла во время LogSplash, и строки, объясняющей ошибку (если она есть). После этого следует сообщение Since, которое инкапсулирует метку времени. И, наконец, WaterConsumedSince, состоящий из одного целого числа.

Хотя эти пять структур определяют мой полный сервис, они не являются реальным исходным кодом, который я мог бы использовать. Итак, моим следующим шагом было создание кода Golang и Python.

По моему эксперименту и моему мнению, создание кода Go намного проще, чем Python. С помощью только одной команды $ go generate protoc -I=. — go_out=plugins=grpc:. endpoint.proto (выполняемой из каталога, в котором находится файл Proto) вы сможете сгенерировать код. Тем не менее, мой опыт работы с Python был не лучшим из-за некоторых проблем, связанных с путем, по которому находится сгенерированный код. Для тех из вас, кому интересно, как я создал код Python, это команда, которую я выполнил (из корневого каталога Water Retriever)

$ python3 -m grpc_tools.protoc -I.:${PROJ}api/v1/ \
— python_out=app/api/v1/ \ 
— grpc_python_out=app/api/v1/ ${PROJ}api/v1/endpoint.proto

Но на этом история не заканчивается. Если вы запустите Water Retriever как есть, он не найдет сгенерированный код. Итак, мне пришлось перейти к одному из множества __init__.py файлов и добавить текущий каталог в PYTHONPATH (я ненавидел эту часть).

OK! Теперь у нас есть сгенерированный код. Но здесь я этого не покажу, нет - это скучно!

Вместо этого я собираюсь представить, что функции на самом деле делают. Вы могли заметить, что определения Protobuf - это всего лишь определения - или скелет функций. Внутри вообще никакого функционала нет. Функциональность должна быть описана вами. Следующий фрагмент показывает это:

package endpoint

import (
	"context"
	"log"

	"cloud.google.com/go/bigquery"
	"github.com/golang/protobuf/ptypes"
	pb "github.com/juandes/teamaqua/api/v1"
)

// Service implement the gRPC endpoints
type Service struct {
	splashes []*pb.Splash
	uploader *bigquery.Uploader
}

// NewService creates a new Service
func NewService(u *bigquery.Uploader) *Service {
	return &Service{
		splashes: []*pb.Splash{},
		uploader: u,
	}
}

func (s *Service) LogSplash(ctx context.Context, in *pb.Splash) (*pb.LogSplashResponse, error) {
	log.Println(in)

	s.splashes = append(s.splashes, in)
	err := s.uploader.Put(ctx, in)
	if err != nil {
		// TODO: Handle error better and don't just exit :/
		log.Fatalf("Error uploading to BQ: %v", err)
	}

	return &pb.LogSplashResponse{
		Ok: true,
	}, nil
}

func (s *Service) WaterConsumed(ctx context.Context, in *pb.Since) (*pb.WaterConsumedSince, error) {
	var waterConsumed int32
	since, err := ptypes.Timestamp(in.Ts)
	if err != nil {
		log.Fatalf("Error converting ptypes.Timestamp to time: %v", err)
	}

	for _, splash := range s.splashes {
		splashTime, err := ptypes.Timestamp(splash.Ts)
		if err != nil {
			log.Fatalf("Error converting ptypes.Timestamp to time: %v", err)
		}

		if splashTime.After(since) {
			waterConsumed += splash.Amount
		}
	}

	return &pb.WaterConsumedSince{
		Amount: waterConsumed,
	}, nil
}

Этот фрагмент кода является моей структурой обслуживания и реализует метод gRPC, который мы определили ранее. Однако, помимо реализации функций, моя служебная структура отвечает за ведение списка Splash. Более того, эта структура также содержит BigQuery загрузчик, объект, функция которого записывает строки в BigQuery. Для простоты я не буду показывать основную функцию службы, в которой я инициализирую службу и загрузчик. Если хотите его увидеть, посмотрите файл здесь.

Первая функция NewService просто создает новый объект службы. Затем есть LogSplash, метод обслуживания. Как мы видели ранее, LogSplash принимает Splash (и контекст, который я не буду объяснять) и возвращает LogSplashResponse и ошибку (это что-то особенное для Golang, не обращайте на это внимания).

Содержание функции довольно простое. Сначала он печатает заставку, затем добавляет ее в список и записывает в BigQuery. В конце концов, он возвращает LogSplashResponse, где «ок» истинно. Второй метод структуры - WaterConsumed, и его функция заключается в суммировании всего количества воды, потребляемой после заданной отметки времени. Это конец Арчи.

Напоминание

Вторая служба Голанга - это Напоминание, и ее цель - напомнить мне, что нужно пить воду, или поздравить меня, когда я это сделаю. Эти напоминания и поздравления будут отправлены в виде push-уведомления в приложение для Android, которое я написал для этого единственного проекта. Следующий код - это завершенная услуга. В отличие от Арчи, эта полностью написана в функции main (и я этим не горжусь).

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"firebase.google.com/go/messaging"

	firebase "firebase.google.com/go"
	"github.com/golang/protobuf/ptypes"
	pb "github.com/juandes/teamaqua/api/v1"
	"github.com/spf13/pflag"
	"google.golang.org/api/option"
	"google.golang.org/grpc"
)

const (
	address         = "localhost:50051"
	minutesInterval = 30
)

var (
	token             *string = pflag.String("token", "", "Firebase Token")
	projectID         *string = pflag.String("project-id", "", "Firebase Project ID")
	googleCredentials *string = pflag.String("google-credentials", "", "Google Service Account")
)

func init() {
	pflag.Parse()
}

func main() {
	log.Println("Starting Reminder service...")
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}

	ctx := context.Background()
	c := pb.NewDrinkWaterClient(conn)
	opt := option.WithCredentialsFile(*googleCredentials)

	fb, err := firebase.NewApp(ctx, &firebase.Config{
		ProjectID: *projectID,
	}, opt)

	if err != nil {
		log.Panicf("error initializing app: %v", err)
	}

	fbMessaging, err := fb.Messaging(ctx)
	if err != nil {
		log.Panicf("error initializing Firebase Messaging client app: %v", err)
	}

	response, err := fbMessaging.Send(ctx, &messaging.Message{
		Token: *token,
		Notification: &messaging.Notification{
			Title: "Hello!",
			Body:  "Reminder service running ...",
		},
		Data: map[string]string{},
	})

	if err != nil {
		log.Printf("Error sending Firebase notification: %v", err)
	}
	log.Println(response)

	go func() {
		interval := time.NewTicker(minutesInterval * time.Minute)

		for {
			select {
			case <-interval.C:
				var title string
				var body string

				log.Println("Executing...")
				// current time minus minutes interval
				t := time.Now().Add(time.Duration(-minutesInterval) * time.Minute)

				timestampProto, err := ptypes.TimestampProto(t)
				if err != nil {
					log.Fatalf("Error converting time to proto Timestamp: %v", err)
					continue
				}

				waterConsumed, err := c.WaterConsumed(ctx, &pb.Since{
					Ts: timestampProto,
				})
				if err != nil {
					log.Printf("Error calling WaterConsumed: %v", err)
					continue
				}

				if waterConsumed.Amount == 0 {
					title = "Reminder!"
					body = fmt.Sprintf("You havent drink anything in the last %d minutes", minutesInterval)
				} else {
					title = "Good job!"
					body = fmt.Sprintf("You had drunk %d in the last %d minutes", waterConsumed.Amount, minutesInterval)
				}

				response, err := fbMessaging.Send(ctx, &messaging.Message{
					Token: *token,
					Notification: &messaging.Notification{
						Title: title,
						Body:  body,
					},
					Data: map[string]string{},
				})

				if err != nil {
					log.Printf("Error sending Firebase notification: %v", err)
				}
				log.Println(response)

			}

		}

	}()

	http.HandleFunc("/", handler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi!")
}

Первое, что сделает сервис, - это создаст соединение с серверной частью gRPC. Затем он создает клиент службы обмена сообщениями Firebase (FMS). Я отправлю push-уведомление в свое приложение, чтобы знать, когда все работает.

Пора сделать еще одно замечание: чтобы отправить сообщение на конкретное устройство, вам нужно знать токен Firebase телефона, который можно получить прямо из приложения (подробнее я расскажу позже). Как только это будет сделано, мы создадим новый Goroutine (воспринимайте его как фоновый поток) с бесконечным циклом for, который будет запускаться каждые X минут.

На каждой итерации цикла мы будем вычислять текущее время минус X минут. Затем, используя это новое время, система вызовет метод WaterConsumed gRPC, чтобы получить количество воды, которое я зарегистрировал с тех пор - вызов возвращает объект Since. Если количество воды, выпитое с момента времени t, равно нулю, он пришлет мне уведомление, в котором будет что-то вроде: «Вы ничего не пьете». В противном случае в сообщении будет сказано: «Вы выпили XXX мл воды за последние X минут». Вдобавок к этому служба прослушивает данный порт. В настоящее время он ничего не делает (кроме ответа «Привет!»), Но я планирую, чтобы он выполнял проверки работоспособности.

Приложение для Android

Последним важным компонентом этой платформы является приложение для Android, единственная цель которого - получать сообщения от FMS и пересылать их мне. Честно говоря, это все. На следующем снимке экрана вы увидите, что приложение представляет собой пустой экран с надписью «Hello World».

Что касается кода, то он настолько прост, насколько и выглядит приложение. Он содержит только одно действие (экран), целью которого является печать токена Firebase устройства, который необходим для указания цели уведомления. Перед тем как это сделать, нам нужно сначала установить Firebase SDK в приложение (я не буду касаться этого целого сюжета).

Это код активности.

package com.example.teamaqua

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.iid.FirebaseInstanceId

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        FirebaseInstanceId.getInstance().instanceId
            .addOnCompleteListener(OnCompleteListener { task ->
                if (!task.isSuccessful) {
                    Log.w("MyFirebaseMsgService", "getInstanceId failed", task.exception)
                    return@OnCompleteListener
                }

                // Get new Instance ID token
                val token = task.result?.token

                Log.d("This is token!", token)
                Toast.makeText(baseContext, token, Toast.LENGTH_SHORT).show()
            })
    }
}

BigQuery и Студия данных

Одной из raison d ‘être Арчи является запись сгенерированных данных в BigQuery, чтобы я мог вернуться позже и проанализировать их. Теперь вы можете подумать, почему? Если данные уже находятся на серверах Fitbit, почему я сохраняю их снова? Что ж, я хочу сделать это как можно сложнее! Но настоящая причина кроется в Data Studio, интерактивной панели инструментов, которая легко интегрируется с BigQuery и визуализирует данные после пары щелчков мышью.

В Data Studio я создал информационную панель, которая отображает линейную диаграмму с количеством воды, потребляемой за день, таблицу с количеством брызг и две карты показателей, которые отображают общее количество потребляемой воды и среднесуточное значение. Следующий снимок экрана является примером.

Да, мне все еще нужно пить больше воды….

Развертывание и докер

Мы почти на финише. Нам осталось сделать только одно: развернуть и запустить систему.

Как и ожидалось, это было не так прямо, как мне хотелось бы. Большинство проблем (это были не настоящие проблемы, а просто небольшая икота), с которыми я столкнулся, были связаны с микросервисами, архитектурным дизайном системы и тем, что я хотел сохранить все в одном образе Docker. Вдобавок ко всему, поскольку я использую несколько секретов и ключей, мне пришлось подумать об их управлении. Давайте посмотрим на весь (короткий) процесс.

Первый шаг - это создание образа Docker. Он основан на официальном Голанге. Затем, что касается первой инструкции, мы скопируем весь проект (да, все!) В изображение. После этого мы запускаем несколько тестов (очень важно!) И создаем двоичные файлы Go внутри контейнера, чтобы избежать возможных проблем кросс-компиляции. После этого нам нужно установить Python 3 (это 2019 год, люди), некоторые инструменты и библиотеки, необходимые Water Retriever. На последнем шаге мы выполним run(.)sh скрипт, запускающий службы. Два приведенных ниже скрипта - это файл Dockerfile и run.sh.

FROM golang:1.12

WORKDIR /go/src/github.com/juandes/teamaqua
COPY . .

RUN echo $GOPATH
RUN make test
RUN make go-build

RUN apt-get update
RUN DEBIAN_FRONTEND='noninteractive' apt-get install -y --no-install-recommends python3.6  python3-pip python3-setuptools screen

RUN pip3 install -r water_retriever/requirements.txt

CMD [ "./run.sh"]
#!/usr/bin/env bash

echo "Starting Server"
screen -d -m build/startserver --gcp-project=$GCP_PROJECT --bq-dataset=$BQ_DATASET --bq-table=$BQ_TABLE

echo "Starting Reminder"
screen -d -m build/reminder --project-id=$GCP_PROJECT --token=$FB_TOKEN --google-credentials=path/to/credentials.json 

screen -list

echo "Starting Water Retriever"
python3 water_retriever/client.py

Сценарий run.sh сначала выполнит обе службы Go в фоновом режиме с помощью GNU Screen (если кто-нибудь знает способ лучше, дайте мне знать). Затем на переднем плане запустим Water Retriever. Помните о различных аргументах (проект Google Cloud, токен Firebase и др.), Которые необходимы компонентам - они будут взяты из переменных среды. Более того, Water Retriever также требует ключей Fitbit, которые также поступают из переменных окружения, которые вы, да вы, должны объявить при запуске образа Docker. Следующая строка - это пример команды, которую я использую.

docker run -d -e FITBIT_KEY=ABCDEF \
 -e FITBIT_SECRET=abcdef1234567 \
 -e ACCESS_TOKEN=12345678 \
 -e REFRESH_TOKEN=12345678 \
 -e GOOGLE_APPLICATION_CREDENTIALS=path/to/credentials.json \
 -e GCP_PROJECT=xxx \
 -e BQ_DATASET=xxx \
 -e BQ_TABLE=xxx \
 -e FB_TOKEN=xxx \
 gcr.io/xxx/xxx:tag

Говоря о запуске Docker, я хотел бы упомянуть, что вместо того, чтобы размещать образ в общедоступном репозитории (извините, он содержит слишком много секретов), я поместил его в реестр контейнеров Google. Оглядываясь назад, это было правильное решение, потому что с помощью графического интерфейса реестра можно быстро создать новый экземпляр Compute Engine с уже загруженным в него образом. По умолчанию машина выполнит образ, но в моем случае он выйдет из строя из-за отсутствия учетных данных, ключей и так далее. Итак, вам придется использовать SSH и запускать его вручную.

Вау, думаю, это все!

Резюме и заключение

В последних 3000 словах я представил платформу, цель которой - напомнить мне, что нужно пить воду. Опять же, я знаю, что мог бы использовать одно из множества доступных приложений именно для этой цели. Однако я хотел создать свою собственную и посмотреть, как далеко я смогу зайти. На этом проект не закончится (ладно, может, и закончится). Есть список функций, таких как проверка работоспособности, о которой я упоминал, настраиваемая панель инструментов, показатели и оповещения, которые я хотел бы реализовать. А пока я выпью стакан воды (и зарегистрирую).

Спасибо за прочтение.

Код доступен по адресу https://github.com/juandes/team-aqua.