Cloud Functions  —  это масштабируемая реализация от Google Cloud Platform (GCP) архитектурного шаблона function-as-a-service, FaaS («функция как услуга») с оплатой по мере использования, позволяющая запускать код без управления сервером.

Ради большего удобства восприятия напишем Cloud Function на Python, хотя это можно сделать также на Node.js, Go, Java.NET, Ruby и PHP. Так что смело выбирайте любимый язык и вперед!

Terraform  —  это инструмент разработки, применяющий подход Infrastructure as Code (IaC), то есть «инфраструктура как код». Он позволяет создавать и менять инфраструктуру, а также обновлять ее версии безопасно, эффективно и с возможностью воспроизводимости. В Terraform используется декларативный способ описания инфраструктуры. Для этого облачные API (GCP, AWS, Azure…) переносятся в конфигурационные файлы.

В итоге, загрузив файл в Google Cloud Storage bucket, мы запустим Cloud Function.


Что потребуется?

Затем проходим аутентификацию в GCP на локальном компьютере, введя в терминале gcloud auth application-default login.

Цели

Цель  —  с помощью Terraform развернуть в GCP-проекте:

  • bucket для загрузки файлов;
  • bucket для хранения исходного кода Cloud Function;
  • Cloud Function, которая запускается каждый раз, когда файл загружается в первый bucket, и исходный код которой находится во втором bucket.

Структура проекта

Создаем новую папку cloud_function_project, а в ней  —  файлы:

.cloud_function_project/

├── terraform/
│ │
│ ├── backend.tf
│ ├── function.tf
│ ├── main.tf
│ ├── storage.tf
│ └── variables.tf

└── src/

├── main.py
└── requirements.txt

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

В папке src находится исходный код Cloud Function. Структура src характерна для Python:

  • main.py: исходный код Cloud Functions.
  • requirements.txt: список библиотек Python для запуска main.py (здесь они нам не понадобятся).

В папке terraform содержатся конфигурационные файлы среды для развертывания:

  • backend.tf: объявление бэкенда Terraform.
  • main.tf: основные объявления среды.
  • variables.tf: определение переменных.
  • storage.tf: объявление Google Cloud Storage bucket.
  • function.tf: объявление Cloud Function.

Создаем Cloud Function

Написание и запуск Cloud Function здесь не так важны. Развернем функцию, в которой будет записываться полезная информация о файле, загруженном в bucket для отслеживания:

def hello_gcs(event, context):
    """Background Cloud Function to be triggered by Cloud Storage.
       This generic function logs relevant data when a file is changed.
    Args:
        event (dict):  The dictionary with data specific to this type of event.
                       The `data` field contains a description of the event in
                       the Cloud Storage `object` format described here:
                       https://cloud.google.com/storage/docs/json_api/v1/objects#resource
        context (google.cloud.functions.Context): Metadata of triggering event.
    Returns:
        None; the output is written to Stackdriver Logging
    """
    print('Event ID:' , context.event_id)
    print('Event type:', context.event_type)
    print('Bucket:', event['bucket'])
    print('File:',  event['name'])
    print('Metageneration:',  event['metageneration'])
    print('Created:',  event['timeCreated'])
    print('Updated:',  event['updated'])

# пустой

Примечание № 1: эта функция не имеет необходимых требований, поэтому requirements.txt остается пустым.
Примечание № 2: это Cloud Function для Python, для других языков источник кода можно поменять.


Создаем инфраструктуру в Terraform

Backend

Начнем объявлять среду, указав бэкенд Terraform. Выбираем local, чтобы все файлы состояния сохранялись в локальном каталоге. Можно также выбрать бэкенд gcs, но не будем усложнять:

terraform {
backend "local" {}
}

Variables

Объявляем переменные, используемые в файлах Terraform. Нужно поменять переменную project_id на идентификатор проекта, в котором будут развернуты ресурсы. region тоже можно поменять:

variable "project_id" {
default = "<YOUR-PROJECT-ID>"
}

variable "region" {
default = "europe-west1"
}

Main

Объявляем подключение к провайдеру Google:

provider "google" {
project = var.project_id
region = var.region
}

Google Cloud Storage

Объявляем один Google Cloud Storage bucket для хранения кода Cloud Function, а другой  —  для загрузки файлов:

resource "google_storage_bucket" "function_bucket" {
name = "${var.project_id}-function"
location = var.region
}

resource "google_storage_bucket" "input_bucket" {
name = "${var.project_id}-input"
location = var.region
}

Примечание № 3: уникальность названий bucket обеспечивается префиксами project_id.

Google Cloud Function

Последний шаг: объявляем Cloud Function. Для этого сжимаем исходный код в zip-файл и загружаем его в bucket для хранения. Затем получаем доступ к исходному коду при создании Cloud Function с помощью Terraform.

Примечание № 4: файл длинноват, так что для понимания логики смотрите комментарии:

# Создает архив исходного кода в виде сжатого .zip file.
data "archive_file" "source" {
type = "zip"
source_dir = "../src"
output_path = "/tmp/function.zip"
}

# Добавляем zip-файл с исходным кодом в bucket для Cloud Function
resource "google_storage_bucket_object" "zip" {
source = data.archive_file.source.output_path
content_type = "application/zip"

# Добавляем к контрольной сумме MD5 содержимого файлов,
# чтобы после изменения zip-файл обновлялся принудительно
name = "src-${data.archive_file.source.output_md5}.zip"
bucket = google_storage_bucket.function_bucket.name

# Зависимости выводятся автоматически, поэтому эти строки можно удалить
depends_on = [
google_storage_bucket.function_bucket, # объявленный в «storage.tf»
data.archive_file.source
]
}

# Создаем Cloud Function, запускаемую с помощью события «Finalize» в bucket
resource "google_cloudfunctions_function" "function" {
name = "function-trigger-on-gcs"
runtime = "python37" # можно поменять, конечно

# Получаем исходный код Cloud Function в zip-файле
source_archive_bucket = google_storage_bucket.function_bucket.name
source_archive_object = google_storage_bucket_object.zip.name

# Должно совпадать с именем функции в исходном коде Cloud Function «main.py»
entry_point = "hello_gcs"

#
event_trigger {
event_type = "google.storage.object.finalize"
resource = "${var.project_id}-input"
}

# Зависимости выводятся автоматически, поэтому эти строки можно удалить
depends_on = [
google_storage_bucket.function_bucket, # объявленный в «storage.tf»
google_storage_bucket_object.zip
]
}

Развёртываем среду

К развертыванию все готово. Переходим в корневой каталог проекта в терминале, затем в корневой каталог папки terraform:

$ cd ./terraform

Инициализируем код, чтобы загрузить указанные в нем требования:

$ terraform init

Просматриваем изменения:

$ terraform plan

Принимаем их и применяем к реальной инфраструктуре:

$ terraform apply

Тестируем Cloud Function

Проверим, что все работает нормально:

  • Открываем консоль Google Cloud Console и подключаемся к проекту.
  • Переходим в браузер Google Cloud Storage browser.
  • Видим оба bucket: <YOUR-PROJECT-ID>-function и <YOUR-PROJECT-ID>-input. Нажимаем на bucket <YOUR-PROJECT-ID>-input и, загрузив в него любой файл, запускаем Cloud Function.
  • Чтобы убедиться, что запуск произошел, переходим в список Cloud Functions. Там должна быть Cloud Function с именем function-trigger-on-gcs. Нажимаем на этом названии и переходим на вкладку LOGS, чтобы увидеть, что Cloud Function запущена с помощью загруженного нами файла.

Что дальше?

Процесс развертывания Cloud Function можно дополнить, например:

  • установить политику IAM;
  • добавить переменные и сделать из этого кода модуль Terraform, чтобы использовать его многократно;
  • интегрировать в процесс облачной сборки, чтобы развертывание было непрерывным.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Axel Thevenot: Deploy Cloud Functions on GCP with Terraform

Предыдущая статьяЧистая архитектура: руководство для начинающих
Следующая статьяСтруктуры данных: подход «разделяй и властвуй»