ПР. Сокращатель ссылок
Кратко:
- Создание сокращателя ссылок с использованием сервисов Yandex Cloud.
- Создание сервисного аккаунта, добавление ролей и создание бекета в Object Storage.
- Создание бессерверной базы данных YDB и создание таблицы с помощью SQL-скрипта.
- Создание функции для обработки ссылок и вставка кода в файл index.py.
- Настройка Yandex API Gateway с использованием спецификации for-serverless-shortener.yml.
- Запуск и проверка работоспособности созданного приложения.
- Возможность дальнейшего развития и расширения функциональности приложения.
Практическая работа. Сокращатель ссылок
В рамках этого курса вы изучили несколько ключевых сервисов Yandex Cloud, относящихся к группе Serverless. Давайте объединим их для решения ещё одной практической задачи и создадим сервис, который конвертирует длинные ссылки в короткие.
Шаг 1. Сервисный аккаунт
Создание аккаунта
Создайте сервисный аккаунт с именем
serverless-shortener
:
export SERVICE_ACCOUNT_SHORTENER_ID=$(yc iam service-account create --name serverless-shortener \
--description "service account for serverless" \
--format json | jq -r .)
Проверьте текущий список сервисных аккаунтов:
yc iam service-account list
После проверки запишите идентификатор созданного сервисного аккаунта в переменную
SERVICE_ACCOUNT_SHORTENER_ID
:
echo "export SERVICE_ACCOUNT_SHORTENER_ID=<идентификатор сервисного аккаунта>" >> ~/.bashrc && . ~/.bashrc
echo $SERVICE_ACCOUNT_SHORTENER_ID
Назначение ролей
Добавьте созданному сервисному аккаунту роли
editor
, storage.viewer
и ydb.admin
:
echo "export FOLDER_ID=$(yc config get folder-id)" >> ~/.bashrc && . ~/.bashrc
echo $FOLDER_ID
echo "export OAUTH_TOKEN=$(yc config get token)" >> ~/.bashrc && . ~/.bashrc
echo $OAUTH_TOKEN
echo "export CLOUD_ID=$(yc config get cloud-id)" >> ~/.bashrc && . ~/.bashrc
echo $CLOUD_ID
yc resource-manager folder add-access-binding $FOLDER_ID \
--subject serviceAccount:$SERVICE_ACCOUNT_SHORTENER_ID \
--role editor
yc resource-manager folder add-access-binding $FOLDER_ID \
--subject serviceAccount:$SERVICE_ACCOUNT_SHORTENER_ID \
--role ydb.admin
yc resource-manager folder add-access-binding $FOLDER_ID \
--subject serviceAccount:$SERVICE_ACCOUNT_SHORTENER_ID \
--role storage.viewer
Шаг 2. Создание бакета в Object Storage
Сделаем для нашего сервиса веб-интерфейс. Поскольку это будет статическая веб-страница, разместим её в объектном хранилище.
В консоли управления в вашем рабочем каталоге выберите сервис Object Storage. Нажмите кнопку Создать бакет.
На странице создания бакета:
- Введите имя бакета. В нашем примере это будет
storage-for-serverless-shortener
. - Ограничьте максимальный размер бакета (например 1 ГБ).
- Выберите тип доступа
Публичный
во всех случаях. - Выберите класс хранилища
Стандартное
.
Нажмите кнопку Создать бакет для завершения операции.

Создайте файл
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>
Шаг 3. Создание базы данных
Создадим бессерверную базу данных YDB с именем
for-serverless-shortener
. Чтобы не переключаться из терминала, снова воспользуемся CLI. Обязательно укажите флаг --serverless
для выбора типа создаваемой базы данных.
yc ydb database create for-serverless-shortener \
--serverless \
--folder-id $FOLDER_ID
yc ydb database list
Далее выполните команду:
yc ydb database get --name for-serverless-shortener
В выводе вы увидите значение
endpoint
. Оно состоит из двух частей: собственно эндпоинта (обычно это ydb.serverless.yandexcloud.net:2135
) и пути базы данных (он указывается после ключевого слова database
и начинается с символа /
, например /ru-central1/...
).Сохраним адрес эндпоинта в переменную
YDB_ENDPOINT
, а путь базы данных — в переменную YDB_DATABASE
. Они пригодятся нам для подключения функции.
echo "export YDB_ENDPOINT=<YDB_ENDPOINT>" >> ~/.bashrc && . ~/.bashrc
echo $YDB_ENDPOINT
echo "export YDB_DATABASE=<YDB_DATABASE>" >> ~/.bashrc && . ~/.bashrc
echo $YDB_DATABASE
Для дальнейшей работы нам понадобится утилита интерфейса командной строки YDB CLI:
curl https://storage.yandexcloud.net/yandexcloud-ydb/install.sh | bash
С помощью CLI создадим авторизованный ключ сервисного аккаунта
serverless-shortener
:
yc iam key create \
--service-account-name serverless-shortener \
--output serverless-shortener.sa
Сохраним путь к файлу с ключом в переменную окружения:
echo "export SA_KEY_FILE=$PWD/serverless-shortener.sa" >> ~/.bashrc && . ~/.bashrc
echo $SA_KEY_FILE
Проверим работоспособность с помощью команды:
ydb \
--endpoint $YDB_ENDPOINT \
--database $YDB_DATABASE \
--sa-key-file $SA_KEY_FILE \
discovery whoami \
--groups
- Сохраним в файл
links.yql
SQL-скрипт для создания таблицы:CREATE TABLE links ( id Utf8, link Utf8, PRIMARY KEY (id) ); COMMIT;
Запустите создание таблицы, а затем проверьте результат:
ydb \
--endpoint $YDB_ENDPOINT \
--database $YDB_DATABASE \
--sa-key-file $SA_KEY_FILE \
scripting yql --file links.yql
ydb \
--endpoint $YDB_ENDPOINT \
--database $YDB_DATABASE \
--sa-key-file $SA_KEY_FILE \
scheme describe links
Шаг 4. Создание функции
В рабочем каталоге создайте файл
index.py
:
import ydb
import urllib.parse
import hashlib
import base64
import json
import os
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):
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("Нужно указать обе переменные окружения")
credentials = ydb.construct_credentials_from_environ()
return ydb.DriverConfig(endpoint, database, credentials=credentials)
def execute(config, query, params):
with ydb.Driver(config) as driver:
try:
driver.wait(timeout=5)
except TimeoutError:
print("Connect failed to YDB")
print("Last reported errors by discovery:")
print(driver.discovery_debug_details())
return None
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):
print(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
return result_set[0].rows[0].link
def shorten(event):
body = event.get('body')
if body:
body = decode(event, body)
original_host = event.get('headers').get('Origin')
link_id = hashlib.sha256(body.encode('utf8')).hexdigest()[:6]
# в ссылке могут быть закодированные символы, например, %. это помешает работе api-gateway при редиректе,
# поэтому следует избавиться от них вызовом urllib.parse.unquote
insert_link(link_id, urllib.parse.unquote(body))
return response(200, {'Content-Type': 'application/json'}, False, json.dumps({'url': f'{original_host}/r/{link_id}'}))
return response(400, {}, False, 'В теле запроса отсутствует параметр url')
def redirect(event):
link_id = event.get('pathParams').get('id')
redirect_to = find_link(link_id)
if redirect_to:
return response(302, {'Location': redirect_to}, False, '')
return response(404, {}, False, 'Данной ссылки не существует')
# эти проверки нужны, поскольку функция у нас одна
# в идеале сделать по функции на каждый путь в api-gw
def get_result(url, event):
if url == "/shorten":
return shorten(event)
if url.startswith("/r/"):
return redirect(event)
return response(404, {}, False, 'Данного пути не существует')
def handler(event, context):
url = event.get('url')
if url:
# из API-gateway url может прийти со знаком вопроса на конце
if url[-1] == '?':
url = url[:-1]
return get_result(url, event)
return response(404, {}, False, 'Эту функцию следует вызывать при помощи api-gateway')
Создайте файл
requirements.txt
со следующим содержимым:
ydb==2.13.3
Находясь в директории с исходными файлами, упакуйте их в zip-архив:
zip src.zip index.py requirements.txt
Создадим нашу функцию
for-serverless-shortener
, задав все необходимые переменные. В переменные окружения функции необходимо добавить:-
endpoint
— нужно указать протоколgrpcs://
и добавить значение Эндпоинт из секции YDB эндпоинт, обычно получаетсяgrpcs://ydb.serverless.yandexcloud.net:2135
; -
database
— это значение поля База данных из секции YDB эндпоинт (начинается с/ru-central1/....
); -
USE_METADATA_CREDENTIALS
— выставите значение переменной в1
.
Также сразу сделаем функцию публичной.
yc serverless function create \
--name for-serverless-shortener \
--description "function for serverless-shortener"
yc serverless function version create \
--function-name for-serverless-shortener \
--memory=256m \
--execution-timeout=5s \
--runtime=python39 \
--entrypoint=index.handler \
--service-account-id $SERVICE_ACCOUNT_SHORTENER_ID \
--environment USE_METADATA_CREDENTIALS=1 \
--environment endpoint=grpcs://ydb.serverless.yandexcloud.net:2135 \
--environment database=$YDB_DATABASE \
--source-path src.zip
yc serverless function allow-unauthenticated-invoke for-serverless-shortener
Шаг 5. Конфигурирование Yandex API Gateway
Создадим спецификацию
for-serverless-shortener.yml
со следующим содержанием:
openapi: 3.0.0
info:
title: for-serverless-shortener
version: 1.0.0
paths:
/:
get:
x-yc-apigateway-integration:
type: object_storage
bucket: <bucket_name> # <-- имя бакета
object: <html_file> # <-- имя html-файла
presigned_redirect: false
service_account: <service_account_id> # <-- идентификатор сервисного аккаунта
operationId: static
/shorten:
post:
x-yc-apigateway-integration:
type: cloud_functions
function_id: <function_id> # <-- идентификатор функции
operationId: shorten
/r/{id}:
get:
x-yc-apigateway-integration:
type: cloud_functions
function_id: <function_id> # <-- идентификатор функции
operationId: redirect
parameters:
- description: id of the url
explode: false
in: path
name: id
required: true
schema:
type: string
style: simple
Не забудьте подставить в спецификацию актуальные для вас значения переменных.
Используем спецификацию для инициализации:
yc serverless api-gateway create \
--name for-serverless-shortener \
--spec=for-serverless-shortener.yml \
--description "for serverless shortener"
В результате успешного создания API-шлюза получим значение параметра
domain
:
yc serverless api-gateway list
yc serverless api-gateway get --name for-serverless-shortener
Чтобы проверить работоспособность API-шлюза и созданного приложения целиком, скопируйте служебный домен (вида
https://<идентификатор API Gateway>.apigw.yandexcloud.net/
) и вставьте адрес в браузер.Добавляйте адреса сайтов в форму, они будут сохранятся в базу данных. А вам будет доступна ссылка, за которой будет скрываться оригинальный адрес. Ваше приложение полностью работоспособно. Теперь вы умеете использовать serverless-стек технологий Yandex Cloud.

Итак, вы создали приложение с использованием Cloud Functions, API Gateway, Object Storage и YDB. Конечно, вы можете развивать его и дальше, расширяя функциональность.
Вводный курс по serverless-разработке на этом завершён. Осталось пройти лишь заключительный тест, который проверит ваши знания по всем рассмотренным в курсе сервисам.