Как визуализировать ежедневные траты на облачные решения GCP


    Клиенты не любят платить больше, чем планировалось — подробное обоснование расходов неотъемлемая и важная часть внедрения облачных технологий.

    Google Cloud Platform предоставляет различные тарифные планы для используемых ресурсов. Например, стоимость GCE зависит от конфигурации компьютера (CPU, память, сетевые модули, жесткие диски). Расходы на Google Kubernetes Engine (GKE) и Google Cloud Dataproc основываются на всех узлах, которые работают в Google Compute Engine (GCE). Остальные затраты могут вычисляться по сложной и замысловатой формуле. Планировать бюджет становится всё сложнее, особенно если вы пользуетесь несколькими облачными технологиями. Мониторинг и своевременное информирование становятся тем ценнее по мере увеличения трат на инфраструктуру.

    Возможность ежедневной проверки отчетов о тратах так же позволит своевременно скорректировать распределяемые мощности, а итоговый счет в конце месяца не вызовет удивления.

    Предпосылки


    Проекты часто нуждаются в расширенных отчетах для повышения эффективности и отслеживания расходов. Несмотря на то, что GCP позволяет экспортировать данные о тратах в BigQuery для дальнейшего всестороннего анализа, ручной обработки BigQuery часто бывает недостаточно. В идеале мы должны иметь возможность визуализировать данные, фильтровать их и аккумулировать по дополнительным параметрам.

    Распространенные фильтры:

    • Тип ресурса;
    • Временной период;
    • Отдел и команда;
    • Сравнение с предыдущими периодами.

    Первое решение, которое основывается на рекомендации Google — использовать Google Data Studio. Реализация очень проста, — требуется только настройка источника данных. Примеры отчетов и панель мониторинга предустановлены, — но само решение недостаточно гибкое. При создании диаграмм и графиков в Google Data Studio, нельзя ввести формулу, приходится выбирать все параметры вручную.

    Grafana — простой и понятный инструмент мониторинга и анализа данных. Он хорошо зарекомендовал себя. Такой дашборд отлично подошел бы для визуализации данных о платежах. Остаётся открытым вопрос о том, как подключить веб-интерфейс к BigQuery (BQ). Плагин BQ, с открытым исходным кодом, чрезмерно забагован. В итоге, связка BQ-Grafana получается недостаточно стабильной. Кроме того, есть еще один неудобный момент — запросы BQ возвращают данные слишком долго.

    Хорошим решением оказалась загрузка данных в PostgreSQL через CloudSQL, эта СУБД имеет официальный плагин PostgreSQL для Grafana. А последующие результаты тестирования показали, что выбранный способ имеет явные преимущества по скорости работы.

    Скрытый текст
    Технология CloudSQL может быть выбрана и для других кейсов, как самый простой способ управлять службами реляционных баз данных.

    Обзор решения


    Рабочий процесс может быть схематически описан следующим образом. Инструмент отложенных заданий в Kubernetes кластере, каждые 4 часа запускает задачу в Cloud Dataflow. Эта задача, в свою очередь, запускает процесс загрузки данных из BigQuery в PostgreSQL. Выгрузка происходит только тех данных, которые были найдены после последнего экспорта из BQ. После чего последние данные можно будет увидеть в Grafana, подключенной к PostgreSQL.



    Настройка Базы Данных


    В первую очередь необходимо настроить экспорт данных о платежах в BigQuery и создать базу данных PostgreSQL.

    Скрытый текст
    Подробнее о настройке BigQuery можно прочитать в документации. А базу данных можно создать при помощи Cloud SQL.

    После подготовительного этапа создаём несколько объектов баз данных, включая таблицу с имитацией структуры BQ:

    Скрытый текст
    CREATE DATABASE billing;
    USE billing;
     
    CREATE TABLE public.billing_export_2 (
    	id serial NOT NULL,
    	sku_id varchar NULL,
    	labels varchar NULL,
    	export_time varchar NULL,
    	currency varchar NULL,
    	sku_description varchar NULL,
    	location_zone varchar NULL,
        currency_conversion_rate float8 NULL,
    	project_labels varchar NULL,
    	location_country varchar NULL,
    	usage_start_time varchar NULL,
    	billing_account_id varchar NULL,
    	location_region varchar NULL,
    	usage_pricing_unit varchar NULL,
    	usage_amount_in_pricing_units float8 NULL,
    	cost_type varchar NULL,
    	project_id varchar NULL,
    	system_labels varchar NULL,
    	project_description varchar NULL,
    	location_location varchar NULL,
    	project_ancestry_numbers varchar NULL,
    	credits varchar NULL,
    	service_description varchar NULL,
    	usage_amount float8 NULL,
    	invoice_month varchar NULL,
    	usage_unit varchar NULL,
    	usage_end_time varchar NULL,
    	"cost" float8 NULL,
    	service_id varchar NULL,
    	CONSTRAINT billing_export_2_pkey PRIMARY KEY (id)
    );
    

    Также создаём материализованное представление со значениями, которые будут использоваться для создания подключения PostgreSQL через Grafana:

    
    CREATE MATERIALIZED VIEW vw_billing_export AS
    	SELECT
        	id,
            sku_id,
            labels,
            export_time::timestamp,
            currency,
            sku_description,
            location_zone,
            currency_conversion_rate,
            project_labels,
            location_country,
            usage_start_time::timestamp,
            billing_account_id,
            location_region,
            usage_pricing_unit,
            usage_amount_in_pricing_units,
            cost_type, project_id,
            system_labels,
            project_description,
            location_location,
            project_ancestry_numbers,
            credits,
            service_description,
            usage_amount,
            invoice_month,
            usage_unit,
            usage_end_time::timestamp,
            "cost",
            service_id,
        	l_label1 ->> 'value' as label1,
        	l_label2 ->> 'value' as label2,
       	...
        	FROM billing_export_2
            	LEFT  JOIN jsonb_array_elements(labels::jsonb) AS l_label1
    on l_label1 ->> 'key' = ‘label1’
            	LEFT  JOIN jsonb_array_elements(labels::jsonb) AS l_label2
    on l_label2 ->> 'key' = ‘label2’
            	...
    


    Ко всем ресурсам необходимо добавить набор меток, которые будут использоваться. Каждая метка из этого набора — это отдельный столбец в представлении. Для увеличения скорости работы Grafana, стоит создать индексы для этих столбцов.

    Скрытый текст
    Индексы для представления можно сделать позднее — важнее понять, как будут выглядеть запросы.
    
    CREATE INDEX vw_billing_export_label1
    ON vw_billing_export (label1);
    


    DataFlow


    Создаём учётную запись с доступом к DataFlow и BigQuery. Это нужно для того, чтобы задачи DataFlow могли получать данные от BigQuery.

    export  project=myproject
    gcloud iam service-accounts create "bq-to-sql-dataflow" --project ${project}
     
    gcloud projects add-iam-policy-binding ${project} \
    --member serviceAccount:"bq-to-sql-dataflow@${project}.iam.gserviceaccount.com" \
    --role roles/dataflow.admin
     
    gcloud projects add-iam-policy-binding ${project} \
    --member serviceAccount:"bq-to-sql-dataflow@${project}.iam.gserviceaccount.com" \
    --role roles/bigquery.dataViewer
    

    Для задач DataFlow необходимо создать два контейнера: один для временных, второй для выходных данных.

    gsutil mb gs://some-bucket-staging
    gsutil mb gs://some-bucket-temp
    

    Скрипт для загрузки данных


    Скрипт потребуется для загрузки данных из BigQuery в CloudSQL. Облачный DataFlow поддерживает разработку на Python, можно воспользоваться им. Также потребуется библиотека Apache Beam, json-файл (установленный в переменную среды GOOGLE_APPLICATION_CREDENTIALS с предварительно настроенными полномочиями учетной записи) и файл requirements.txt, который содержит список пакетов для инсталляции (в нашем случае нужен только один beam-nuggets пакет). Скрипт на Python представлен ниже:

    Скрытый текст
    Основная часть скрипта bq-to-sql выглядит следующим образом:

    args = parser.parse_args()
    project = args.project
    job_name = args.job_name + str(uuid.uuid4())
    bigquery_source = args.bigquery_source
    postgresql_user = args.postgresql_user
    postgresql_password = args.postgresql_password
    postgresql_host = args.postgresql_host
    postgresql_port = args.postgresql_port
    postgresql_db = args.postgresql_db
    postgresql_table = args.postgresql_table
    staging_location = args.staging_location
    temp_location = args.temp_location
    subnetwork = args.subnetwork
     
    options = PipelineOptions(
            	flags=["--requirements_file", "/opt/python/requirements.txt"])
    # For Cloud execution, set the Cloud Platform project, job_name,
    # staging location, temp_location and specify DataflowRunner.
     
    google_cloud_options = options.view_as(GoogleCloudOptions)
    google_cloud_options.project = project
    google_cloud_options.job_name = job_name
    google_cloud_options.staging_location = staging_location
    google_cloud_options.temp_location = temp_location
    google_cloud_options.region = "us-west1"
    worker_options = options.view_as(WorkerOptions)
    worker_options.zone = "us-west1-a"
    worker_options.subnetwork = subnetwork
    worker_options.max_num_workers = 20
     
    options.view_as(StandardOptions).runner = 'DataflowRunner'
     
    start_date = define_start_date()
    with beam.Pipeline(options=options) as p:
    	rows = p | 'QueryTableStdSQL' >> beam.io.Read(beam.io.BigQuerySource(
                        	query='SELECT \
                            	billing_account_id, \
                            	service.id as service_id, \
                            	service.description as service_description, \
                            	sku.id as sku_id, \
                            	sku.description as sku_description, \
                            	usage_start_time, \
                            	usage_end_time, \
                            	project.id as project_id, \
                            	project.name as project_description, \
                            	TO_JSON_STRING(project.labels) \
                                	as project_labels, \
                            	project.ancestry_numbers \
                                	as project_ancestry_numbers, \
                            	TO_JSON_STRING(labels) as labels, \
                            	TO_JSON_STRING(system_labels) as system_labels, \
                            	location.location as location_location, \
                            	location.country as location_country, \
                            	location.region as location_region, \
                            	location.zone as location_zone, \
                            	export_time, \
                            	cost, \
                            	currency, \
                            	currency_conversion_rate, \
                            	usage.amount as usage_amount, \
                            	usage.unit as usage_unit, \
                            	usage.amount_in_pricing_units as \
                             	usage_amount_in_pricing_units, \
                            	usage.pricing_unit as usage_pricing_unit, \
                            	TO_JSON_STRING(credits) as credits, \
                            	invoice.month as invoice_month, \
                       	     cost_type \
    FROM `' + project + '.' + bigquery_source + '` \
                            	WHERE export_time >= "' + start_date + '"',
                        	use_standard_sql=True))
    	source_config = relational_db.SourceConfiguration(
              	              drivername='postgresql+pg8000',
                            	host=postgresql_host,
                            	port=postgresql_port,
                            	username=postgresql_user,
                            	password=postgresql_password,
                            	database=postgresql_db,
                            	create_if_missing=True,
                            	)
    	table_config = relational_db.TableConfiguration(
                            	name=postgresql_table,
                	            create_if_missing=True
                            	)
    	rows | 'Writing to DB' >> relational_db.Write(
        	source_config=source_config,
        	table_config=table_config
    	)
    


    P.S: Поддержка Python, как верно меня скорректировали в комментариях, в скором времени будет прекращена.
    On October 7, 2020, Dataflow will stop supporting pipelines using Python 2.
    Для долгосрочной работы стоит задуматься над портированием этой части.

    Для согласованности данных необходимо определить максимальное export_time, время экспорта в PostgreSQL, после чего загрузить записи из BigQuery, которые начинались бы с этой временной отсечки.

    После того, как данные будут загружены, нужно обновить материализованное представление. Так как этот процесс занимает некоторое время, это позволит создать новое материализованное представление с идентичной структурой и индексами, удалить старое и переименовать новое.

    Создание файла JSON с разрешениями SA


    Учетная запись, созданная ранее для рабочего процесса Cloud Dataflow, также используется в задачах Cron. Нужно задать команду, которая создаст личный ключ-пароль для учетной записи, в дальнейшем загруженный в кластер Kubernetes, где он будет выступать как скрытый пароль для доступа в Cron задачи.

    gcloud iam service-accounts keys create ./cloud-sa.json \
    --iam-account "bq-to-sql-dataflow@${project}.iam.gserviceaccount.com" \
    --project ${project}
    

    Развертывание секретного пароля в кластере K8s:

    kubectl create secret generic bq-to-sql-creds --from-file=./cloud-sa.json
    

    Создание Docker образа


    Так как основной целью является ежедневное автоматическое выполнение задач DataFlow, создаём Docker образ со всеми нужными переменными среды и скриптом на Python:

    Скрытый текст
    Dockerfile:

    FROM python:latest
    RUN \
      bin/bash -c " \
      apt-get update && \
      apt-get install python2.7-dev -y && \
      pip install virtualenv && \
      virtualenv -p /usr/bin/python2.7 --distribute temp-python && \
      source temp-python/bin/activate && \
      pip2 install --upgrade setuptools && \
      pip2 install pip==9.0.3 && \
      pip2 install requests && \
      pip2 install Cython && \
      pip2 install apache_beam && \
      pip2 install apache_beam[gcp] && \
      pip2 install beam-nuggets && \
      pip2 install psycopg2-binary && \
      pip2 install uuid"
    
    COPY ./bq-to-sql.py /opt/python/bq-to-sql.py
    COPY ./requirements.txt /opt/python/requirements.txt
    COPY ./main.sh /opt/python/main.sh
    
    FROM python:latest
    RUN \
      bin/bash -c " \
      apt-get update && \
      apt-get install python2.7-dev -y && \
      pip install virtualenv && \
      virtualenv -p /usr/bin/python2.7 --distribute temp-python && \
      source temp-python/bin/activate && \
      pip2 install --upgrade setuptools && \
      pip2 install pip==9.0.3 && \
      pip2 install requests && \
      pip2 install Cython && \
      pip2 install apache_beam && \
      pip2 install apache_beam[gcp] && \
      pip2 install beam-nuggets && \
      pip2 install psycopg2-binary && \
      pip2 install uuid"
    
    COPY ./bq-to-sql.py /opt/python/bq-to-sql.py
    COPY ./requirements.txt /opt/python/requirements.txt
    COPY ./main.sh /opt/python/main.sh
    
    image: 
    imageTag: latest
    imagePullPolicy: IfNotPresent
    project: 
    job_name: "bq-to-sql"
    bigquery_source: "[dataset].[table]”
    postgresql:
      user: 
      password: 
      host: 
      port: "5432"
      db: "billing"
      table: "billing_export"
    staging_location: "gs://my-bucket-stg"
    temp_location: "gs://my-bucket-tmp"  
    subnetwork: "regions/us-west1/subnetworks/default"
    


    Для того чтобы облегчить процесс развертывания и повторного использования Cron задач, используем установщик пакетов Kubernetes, Helm:

    Скрытый текст
    cronjob.yaml:

    apiVersion: batch/v1beta1
    kind: CronJob
    metadata:
      name: {{ template "bq-to-sql.fullname" . }}
    spec:
      schedule: "0 0 * * *"
      jobTemplate:
    	spec:
      	template:
        	spec:
        	  restartPolicy: OnFailure
          	containers:
          	- name: {{ template "bq-to-sql.name" . }}
            	image: "{{ .Values.image }}:{{ .Values.imageTag }}"
            	imagePullPolicy: "{{ .Values.imagePullPolicy }}"
            	command: [ "/bin/bash", "-c", "bash /opt/python/main.sh \
                	{{ .Values.project }} \
                	{{ .Values.job_name }} \
                	{{ .Values.bigquery_source }} \
                	{{ .Values.postgresql.user }} \
                	{{ .Values.postgresql.password }} \
                	{{ .Values.postgresql.host }} \
                	{{ .Values.postgresql.port }} \
                	{{ .Values.postgresql.db }} \
                	{{ .Values.postgresql.table }} \
                	{{ .Values.staging_location }} \
                	{{ .Values.temp_location }} \
                    {{ .Values.subnetwork }}"]
            	volumeMounts:
            	- name: creds
              	mountPath: /root/.config/gcloud
              	readOnly: true
            	env:
            	- name: GOOGLE_APPLICATION_CREDENTIALS
              	value: /root/.config/gcloud/creds.json
          	volumes:
            	- name: creds
              	secret:
                	secretName: bq-to-sql-creds
    


    Визуализация данных с помощью Grafana


    Последним шагом будет создание долгожданных дашбордов в Grafana. На этом этапе нет каких-то ограничений, можно использовать любой понравившийся стиль для отображения. Как пример, это может быть Data Studio Billing Report Demo.

    Скрытый текст
    Конечно, все SQL запросы придётся написать с нуля :)

    Из важного на что хотелось бы обратить еще внимание. Из соображений безопасности лучше назначать пользователям разные права:

    • В режиме чтения (для просмотра данных счетов);
    • Для соединения с Grafana.

    Заключение


    Описанное руководство даёт возможность взглянуть на рабочий процесс целью которого является визуализация данных счетов. Этот процесс поддерживает добавление новых фильтров и метрик. В итоге клиент получает набор полезных и быстрых дашбардов, которые помогают контролировать и оптимизировать в дальнейшем затраты на Google Cloud.
    OpsGuru
    Компания

    Комментарии 12

      0

      Неужели CloudSQL для этого кейса вам обходится дешевле, чем self hosted PostgreSQL?

        0
        Подобные вопросы часто возникают когда речь заходит про облачные решения. Этот кейс некорректно было бы рассматривать и высчитывать в отрыве от основного процесса. На определённых объемах данных, задействованных решениях и используемых инструментах self hosted становится не прагматично и не целесообразно использовать.
          0

          Вы можете поделиться цифрами и ответить на вопрос?


          Объём данных, нагрузка на базу, какой CloudSQL machine type используете, ha или нет и т.д.?


          Уточню, что оценку трудозатрат на настройку и поддержку self hosted PostgresSQL не трогаем, только затраты на CloudSQL vs self hosted PostgresSQL в вашем конкретном кейсе.


          Не вижу проблем рассмотреть это "в отрыве от основного процесса".

          0
          а «self hosted» — это где?
          0

          Поправлюсь: имел ввиду не self hosted, а self managed, конечно. Т.е. машина(ы) на линуксе с PostgresSQL в том же GCP.

            0
            А зачем? Мне не очень ясна мотивация — зачем тратить время, силы на строительсто (а главное поддержку и ремонт) чего-то, если уже есть готовый блок для использования.
              0

              Меня интересует анализировали ли стоимость решения одно vs другое и что получилось.


              CloudSQL это классно, но далеко не всегда экономически целесообразно, а время на строительство, поддержку и т.п. зависит многих факторов, в т.ч. нагрузки на бд, прямоту рук девопса(ов), наличие девопсов в принципе и т.д.


              Например, если база совсем не нагруженная и небольшая, то поднять небольшой instance с медленными дисками, которому в принципе не от чего падать, без high availability, настроить бэкапы на s3 и т.п. скорее всего будет дешевле использования CloudSQL.


              Если DevOps в ладах с Terraform, Ansible и инфраструктурой как код в принципе, то развернуть и поддерживать такое не сложно и не затратно в человекочасах, но всё зависит от параметров проекта.


              И теоретически если собственный PostgresSQL оказывается даже немного дешевле, то на длительном промежутке времени эта выгода будет накапливаться, а если таких клиентов много, то суммарно это приличная экономия.


              Так-то по каждому чиху можно rds поднимать, но в тегах к статье же finops.

          +1
          Может через каое-то время перестать работать, так как
          «On October 7, 2020, Dataflow will stop supporting pipelines using Python 2.»
          from here: cloud.google.com/dataflow/docs/support/sdk-version-support-status#python
            0
            Да, спасибо. Добавил про это уточнение.
            0
            У нас была похожая проблема, да и есть ещё собственно. Как вы разделяете ресурсы на команды/отделы, точнее говоря, как вы определяете, какая команда/отдел, что и сколько использовала?
            У нас(в aws) есть три больших K8s кластера(production, staging, integration) разделённых на разные аккаунты и в них все запускают свои ресурсы или у вас как-то по другому происходит.
            Кстати может кому-нибудь пригодится prometheues exporter, который показвает дневную цену в aws, разделённые по регионам, аккам и сервисам. Вот тут лежит
              0
              из моего опыта
              naming conventions:
              — префиксы для имён и идентификаторов проектов и ресурсов
              — labels

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

            Самое читаемое