Сокращатель ссылок с помощью Terraform
Важно
Часть ресурсов, необходимых для прохождения практического руководства, доступны только в регионе Россия.
Чтобы создать сокращатель ссылок с помощью Terraform:
Если созданные ресурсы вам больше не нужны, удалите их.
Подготовьте облако к работе
Зарегистрируйтесь в Yandex Cloud и создайте платежный аккаунт:
- Перейдите в консоль управления
, затем войдите в Yandex Cloud или зарегистрируйтесь. - На странице Yandex Cloud Billing
убедитесь, что у вас подключен платежный аккаунт, и он находится в статусеACTIVEилиTRIAL_ACTIVE. Если платежного аккаунта нет, создайте его и привяжите к нему облако.
Если у вас есть активный платежный аккаунт, вы можете создать или выбрать каталог, в котором будет работать ваша инфраструктура, на странице облака
Подробнее об облаках и каталогах.
Необходимые платные ресурсы
В стоимость поддержки инфраструктуры для сокращателя ссылок входят:
- плата за хранение данных (см. тарифы Yandex Object Storage);
- плата за операции с базой данных YDB и хранение данных (см. тарифы Managed Service for YDB);
- плата за количество вызовов функции, вычислительные ресурсы, выделенные для выполнения функции, и исходящий трафик (см. тарифы Cloud Functions);
- плата за количество запросов к API-шлюзу и исходящий трафик (см. тарифы API Gateway).
Создайте инфраструктуру
Terraform
Terraform распространяется под лицензией Business Source License
Подробную информацию о ресурсах провайдера смотрите в документации на сайте Terraform
Для создания инфраструктуры с помощью Terraform:
-
Установите Terraform, получите данные для аутентификации и укажите источник для установки провайдера Yandex Cloud (раздел Настройте провайдер, шаг 1).
-
Подготовьте файлы с описанием инфраструктуры:
Готовая конфигурацияВручную-
Клонируйте репозиторий с конфигурационными файлами.
git clone https://github.com/yandex-cloud-examples/yc-serverless-url-shortener.git -
Перейдите в директорию с репозиторием. В ней должны появиться файлы:
serverless-url-shortener.tf— конфигурация создаваемой инфраструктуры;serverless-url-shortener.auto.tfvars— файл с пользовательскими данными;index.html— HTML-страница вашего сервиса;function.zip— архив с кодом функции Cloud Functions.
-
Создайте папку для конфигурационных файлов.
-
Создайте в папке:
-
Конфигурационный файл
serverless-url-shortener.tf:serverless-url-shortener.tf
# Объявление переменных для конфиденциальных параметров variable "cloud_id" { type = string } variable "folder_id" { type = string } variable "bucket_name" { type = string } # Настройка провайдера terraform { required_providers { yandex = { source = "yandex-cloud/yandex" } } } provider "yandex" { cloud_id = var.cloud_id folder_id = var.folder_id } # Создание сервисного аккаунта resource "yandex_iam_service_account" "shortener_sa" { name = "serverless-shortener" description = "Service account for the URL shortener" } # Назначение роли сервисному аккаунту resource "yandex_resourcemanager_folder_iam_member" "shortener_role" { folder_id = var.folder_id role = "editor" member = "serviceAccount:${yandex_iam_service_account.shortener_sa.id}" } # Создание статического ключа resource "yandex_iam_service_account_static_access_key" "shortener_sa_key" { service_account_id = yandex_iam_service_account.shortener_sa.id description = "Static access key for the service account" } # Создание бакета resource "yandex_storage_bucket" "shortener_bucket" { bucket = var.bucket_name access_key = yandex_iam_service_account_static_access_key.shortener_sa_key.access_key secret_key = yandex_iam_service_account_static_access_key.shortener_sa_key.secret_key max_size = 1073741824 anonymous_access_flags { read = true list = false config_read = false } website { index_document = "index.html" } } # Загрузка объекта в бакет resource "yandex_storage_object" "shortener_index" { bucket = yandex_storage_bucket.shortener_bucket.bucket key = "index.html" source = "index.html" acl = "public-read" access_key = yandex_iam_service_account_static_access_key.shortener_sa_key.access_key secret_key = yandex_iam_service_account_static_access_key.shortener_sa_key.secret_key content_type = "text/html" } # Создание базы данных YDB resource "yandex_ydb_database_serverless" "shortener_db" { name = "shortener-ydb-main" location_id = "kz1" } # Создание таблицы YDB resource "yandex_ydb_table" "test_table" { path = "links" connection_string = yandex_ydb_database_serverless.shortener_db.ydb_full_endpoint column { name = "id" type = "Utf8" not_null = true } column { name = "link" type = "Utf8" not_null = true } primary_key = ["id"] depends_on = [ yandex_ydb_database_serverless.shortener_db ] } # Создание функции resource "yandex_function" "shortener_function" { name = "shortener-function-main" description = "Function for the URL shortener" runtime = "python312" entrypoint = "index.handler" memory = 256 execution_timeout = 5 service_account_id = yandex_iam_service_account.shortener_sa.id content { zip_filename = "function.zip" } user_hash = filesha256("function.zip") environment = { endpoint = "grpcs://${yandex_ydb_database_serverless.shortener_db.ydb_api_endpoint}" database = yandex_ydb_database_serverless.shortener_db.database_path } } resource "yandex_function_iam_binding" "public_invoker" { function_id = yandex_function.shortener_function.id role = "functions.functionInvoker" members = ["system:allUsers"] } # Создание API-шлюза resource "yandex_api_gateway" "shortener_gateway" { name = "shortener-gateway-main" spec = jsonencode({ openapi = "3.0.0" info = { title = "Shortener API Gateway" version = "1.0.0" } paths = { "/" = { get = { "x-yc-apigateway-integration" = { type = "object_storage" bucket = yandex_storage_bucket.shortener_bucket.bucket object = "index.html" presigned_redirect = false service_account_id = yandex_iam_service_account.shortener_sa.id } operationId = "static" } } "/shorten" = { post = { "x-yc-apigateway-integration" = { type = "cloud_functions" function_id = yandex_function.shortener_function.id } operationId = "shorten" } } "/r/{id}" = { get = { "x-yc-apigateway-integration" = { type = "cloud_functions" function_id = yandex_function.shortener_function.id } operationId = "redirect" parameters = [ { description = "ID of the shortened link" explode = false in = "path" name = "id" required = true schema = { type = "string" } style = "simple" } ] } } } }) } # URL для работы с сокращателем ссылок output "url" { value = "https://${yandex_api_gateway.shortener_gateway.domain}" } -
Файл с пользовательскими данными
serverless-url-shortener.auto.tfvars:serverless-url-shortener.auto.tfvars
cloud_id = "<идентификатор_облака>" folder_id = "<идентификатор_каталога>" bucket_name = "<имя_бакета>" -
HTML-страница вашего сервиса
index.html:index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Сокращатель URL</title> <!-- предостережет от лишнего GET запроса на адрес /favicon.ico --> <link rel="icon" href="data:;base64,iVBORw0KGgo="> </head> <body> <h1>Добро пожаловать</h1> <form action="javascript:shorten()"> <label for="url">Введите ссылку:</label><br> <input id="url" name="url" type="text"><br> <input type="submit" value="Сократить"> </form> <p id="shortened"></p> </body> <script> function shorten() { const link = document.getElementById("url").value fetch("/shorten", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: link }) .then(response => response.json()) .then(data => { const url = data.url document.getElementById("shortened").innerHTML = `<a href=${url}>${url}</a>` }) .catch(error => { document.getElementById("shortened").innerHTML = `<p>Произошла ошибка ${error}, попробуйте еще раз</p>` }) } </script> </html> -
Файл с кодом функции Cloud Functions
index.py:index.py
import ydb import urllib.parse import hashlib import base64 import json import os import traceback def decode(event, body): is_base64_encoded = event.get('isBase64Encoded') if is_base64_encoded: body = str(base64.b64decode(body), 'utf-8') return body def response(statusCode, headers, isBase64Encoded, body): # Всегда отдаём строку в body if not isinstance(body, str): body = json.dumps(body, ensure_ascii=False) return { 'statusCode': statusCode, 'headers': headers, 'isBase64Encoded': isBase64Encoded, 'body': body, } def get_config(): endpoint = os.getenv("endpoint") database = os.getenv("database") if endpoint is None or database is None: raise AssertionError("Нужно указать обе переменные окружения: endpoint и database") credentials = ydb.iam.MetadataUrlCredentials() return ydb.DriverConfig(endpoint, database, credentials=credentials) def execute(config, query, params): with ydb.Driver(config) as driver: try: driver.wait(timeout=5, fail_fast=True) except Exception as e: print("Connect failed to YDB:", e) print(driver.discovery_debug_details()) raise session = driver.table_client.session().create() prepared_query = session.prepare(query) return session.transaction(ydb.SerializableReadWrite()).execute( prepared_query, params, commit_tx=True ) def insert_link(id, link): config = get_config() query = """ DECLARE $id AS Utf8; DECLARE $link AS Utf8; UPSERT INTO links (id, link) VALUES ($id, $link); """ params = {'$id': id, '$link': link} execute(config, query, params) def find_link(id): config = get_config() query = """ DECLARE $id AS Utf8; SELECT link FROM links where id=$id; """ params = {'$id': id} result_set = execute(config, query, params) if not result_set or not result_set[0].rows: return None # Учитываем структуру результата от ydb return result_set[0].rows[0].link def shorten(event): try: body = event.get('body') if body is None: return response(400, {'Content-Type': 'application/json'}, False, {'error': 'В теле запроса отсутствует тело'}) body = decode(event, body) # Попробуем распарсить JSON с ключом url, иначе считаем body как plain string url_value = None try: parsed = json.loads(body) if isinstance(parsed, dict): url_value = parsed.get('url') else: # если отправили не-объект JSON, игнорируем url_value = None except Exception: # body не JSON — считаем, что это plain URL url_value = body if not url_value: return response(400, {'Content-Type': 'application/json'}, False, {'error': 'Ожидался параметр url в теле запроса'}) # Очищаем URL от эвентуальных кодированных символов clean_url = urllib.parse.unquote(url_value).strip() if not clean_url: return response(400, {'Content-Type': 'application/json'}, False, {'error': 'Пустой url'}) link_id = hashlib.sha256(clean_url.encode('utf8')).hexdigest()[:6] insert_link(link_id, clean_url) # Возвращаем относительный путь — фронт сам допишет origin return response(200, {'Content-Type': 'application/json'}, False, {'url': f'/r/{link_id}'}) except Exception as e: print("Exception in shorten():", e) traceback.print_exc() return response(500, {'Content-Type': 'application/json'}, False, {'error': 'internal server error'}) def redirect(event): try: # защитно доставать path params path_params = event.get('pathParams') or event.get('pathParameters') or {} link_id = path_params.get('id') if not link_id: # возможно пришёл полный путь в event['url'] или ['path'] url = event.get('url') or event.get('path') or '' if url and url.startswith('/r/'): link_id = url.split('/r/')[-1].split('?')[0] if not link_id: return response(400, {'Content-Type': 'application/json'}, False, {'error': 'id отсутствует'}) redirect_to = find_link(link_id) if redirect_to: return response(302, {'Location': redirect_to}, False, '') return response(404, {'Content-Type': 'application/json'}, False, {'error': 'Данной ссылки не существует'}) except Exception as e: print("Exception in redirect():", e) traceback.print_exc() return response(500, {'Content-Type': 'application/json'}, False, {'error': 'internal server error'}) def get_result(url, event): if url == "/shorten" or url.startswith("/shorten"): return shorten(event) if url.startswith("/r/"): return redirect(event) return response(404, {'Content-Type': 'application/json'}, False, {'error': 'Данного пути не существует'}) def handler(event, context): url = event.get('url') or event.get('path') or '' if url: # Иногда URL из шлюза приходит с вопросительным знаком на конце if url.endswith('?'): url = url[:-1] return get_result(url, event) return response(404, {'Content-Type': 'application/json'}, False, {'error': 'Функция должна вызываться через API Gateway.'}) -
Файл с параметрами окружения функции Cloud Functions
requirements.txt:ydb
-
-
Создайте в папке архив
function.zip, содержащий файлыindex.pyиrequirements.txt. -
В файле
serverless-url-shortener.auto.tfvarsзадайте пользовательские параметры:cloud_id— идентификатор облака.folder_id— идентификатор каталога.bucket_name— имя бакета, в котором будут создаваться ресурсы.
Более подробную информацию о параметрах используемых ресурсов в Terraform см. в документации провайдера:
- Сервисный аккаунт — yandex_iam_service_account.
- Статический ключ — yandex_iam_service_account_static_access_key.
- Бакет — yandex_storage_bucket.
- Объект — yandex_storage_object.
- База данных Managed Service for YDB — yandex_ydb_database_serverless.
- Таблица Managed Service for YDB — yandex_ydb_table.
- Функция — yandex_function.
- API-шлюз — yandex_api_gateway.
-
-
Создайте ресурсы:
-
В терминале перейдите в директорию с конфигурационным файлом.
-
Проверьте корректность конфигурации с помощью команды:
terraform validateЕсли конфигурация является корректной, появится сообщение:
Success! The configuration is valid. -
Выполните команду:
terraform planВ терминале будет выведен список ресурсов с параметрами. На этом этапе изменения не будут внесены. Если в конфигурации есть ошибки, Terraform на них укажет.
-
Примените изменения конфигурации:
terraform apply -
Подтвердите изменения: введите в терминале слово
yesи нажмите Enter.
-
-
Скопируйте
URL, полученный в результате создания инфраструктуры, чтобы проверить работу сокращателя ссылок.
Проверьте работу сокращателя ссылок
Чтобы проверить правильность взаимодействия компонентов сервиса:
-
Откройте в браузере скопированный ранее URL сокращателя.
-
В поле для ввода введите ссылку, которую вы хотите сократить.
-
Нажмите кнопку Сократить.
Ниже отобразится сокращенная ссылка.
-
Перейдите по сокращенной ссылке — должна открыться та же страница, что и по ссылке до сокращения.
Как удалить созданные ресурсы
Чтобы перестать платить за созданные ресурсы:
-
Откройте конфигурационный файл
serverless-url-shortener.tfи удалите описание создаваемой инфраструктуры из файла. -
Примените изменения:
-
В терминале перейдите в директорию с конфигурационным файлом.
-
Проверьте корректность конфигурации с помощью команды:
terraform validateЕсли конфигурация является корректной, появится сообщение:
Success! The configuration is valid. -
Выполните команду:
terraform planВ терминале будет выведен список ресурсов с параметрами. На этом этапе изменения не будут внесены. Если в конфигурации есть ошибки, Terraform на них укажет.
-
Примените изменения конфигурации:
terraform apply -
Подтвердите изменения: введите в терминале слово
yesи нажмите Enter.
-