URL shortener
With this script, you will create a URL shortening service using serverless technologies available in Yandex Cloud.
The service accepts user requests via a public API gateway. The hosting service sends the user an HTML page with a field for entering the URL. The function sends the entered URL for storage in a serverless database, shortens it, and returns it to the user. When the user enters the shortened URL, the function finds the full URL in the database and redirects the user's request to it.
To configure and test the service:
- Prepare your cloud.
- Set up hosting for the URL shortener page.
- Create a service account.
- Create a database in Yandex Managed Service for YDB.
- Set up a function in Yandex Cloud Functions.
- Publish the service via Yandex API Gateway.
- Test the URL shortener.
If you no longer need the resources you created, delete them.
Prepare your cloud
Sign up for Yandex Cloud and create a billing account:
- Go to the management console
and log in to Yandex Cloud or create an account if you do not have one yet. - On the Yandex Cloud Billing
page, make sure you have a billing account linked and it has theACTIVE
orTRIAL_ACTIVE
status. If you do not have a billing account, create one.
If you have an active billing account, you can go to the cloud page
Learn more about clouds and folders.
Required paid resources
The cost of resources for the script includes:
- Fee for using the storage (see Yandex Object Storage pricing).
- Fee for accessing the database (see Managed Service for YDB pricing).
- Fee for function calls (see Cloud Functions pricing).
- Fee for requests to the API gateway (see API Gateway pricing).
Set up hosting for the URL shortener page
To create a bucket to place the HTML page of your service in and configure it for hosting static websites:
-
In the management console
, select your working folder. -
Select Object Storage.
-
Click Create bucket.
-
On the bucket creation page:
-
Enter a name for the bucket.
Warning
Bucket names are unique throughout Object Storage, which means you cannot create two buckets with the same name, even in different folders belonging to different clouds.
-
Set the maximum size to
1 GB
. -
Choose
Public
access to read objects. -
Click Create bucket to complete the operation.
-
-
Copy the HTML code and paste it into the
index.html
file:HTML code
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>URL shortener</title> <!-- warns against sending unnecessary GET requests to /favicon.ico --> <link rel="icon" href="data:;base64,iVBORw0KGgo="> </head> <body> <h1>Welcome</h1> <form action="javascript:shorten()"> <label for="url">Enter a link:</label><br> <input id="url" name="url" type="text"><br> <input type="submit" value="Shorten"> </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>An error (${error}) occurred. Try again.</p>` }) } </script> </html>
-
Click the name of the created bucket.
-
Click Upload objects.
-
Specify the
index.html
file you prepared. -
Click Upload.
-
In the left-hand panel, select Settings.
-
On the Website tab:
- Select
Hosting
. - Specify the website's home page:
index.html
.
- Select
-
Click Save.
Create a service account
To create a service account for the service components to interact:
-
Go to your working folder.
-
In the list of services, select Identity and Access Management.
-
Click Create service account.
-
Enter a name for the service account:
serverless-shortener
. -
Click Add role and select the
editor
role. -
Click Create.
-
Click on the name of the created service account.
Save the service account ID, you will need it in the next steps.
Create a database in Managed Service for YDB
To create a Managed Service for YDB database and configure it to store URLs:
-
Go to your working folder.
-
In the list of services, select Managed Service for YDB.
-
Click Create a database.
-
Enter the database name:
for-serverless-shortener
. -
Select the Serverless database type.
-
Click Create a database.
-
Wait until the database starts.
While being created, your database will have the
Provisioning
status. Once it is ready for use, its status will change toRunning
. -
Click the name of the database you created.
Save the Endpoint field value, you will need it in the next steps.
-
In the left-hand panel, select the Navigation tab.
-
Click New SQL query.
-
Copy the SQL query and paste it into the Query field:
CREATE TABLE links ( id Utf8, link Utf8, PRIMARY KEY (id) );
-
Click Run.
Set up a function in Cloud Functions
To create and set up a URL shortening function:
-
Go to your working folder.
-
In the list of services, select Cloud Functions.
-
Click Create function.
-
Enter the name:
for-serverless-shortener
. -
Click Create.
-
In the Python drop-down list, choose the
python312
runtime environment. -
Click Continue.
-
Copy the function code and paste it into the
index.py
file under Function code.Function code
import ydb import urllib.parse import hashlib import base64 import json import os def decode(event, body): # The request body can be encoded. 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("You need to specify both environment variables") 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 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] # The URL may contain encoded characters, e.g., %. This will interfere with API Gateway when redirecting, # therefore, you should get rid of these characters by invoking 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, 'The url parameter is missing in the request body') 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, 'This link does not exist') # These checks are required because we have only one function. # Ideally, each path in API Gateway should have a function of its own. def get_result(url, event): if url == "/shorten": return shorten(event) if url.startswith("/r/"): return redirect(event) return response(404, {}, False, 'This path does not exist') def handler(event, context): url = event.get('url') if url: # The URL may come from API Gateway with a question mark at the end. if url[-1] == '?': url = url[:-1] return get_result(url, event) return response(404, {}, False, 'This function should be invoked using API Gateway.')
-
Under Function code, create a file named
requirements.txt
and paste the following text into it:ydb
-
Specify the entry point:
index.handler
. -
Set the timeout value to
5
. -
Select the
serverless-shortener
service account. -
Add these environment variables:
endpoint
: Enter the first part of the previously saved Endpoint field value (preceding/?database=
), e.g.,grpcs://ydb.serverless.yandexcloud.net:2135
.database
: Enter the second part of the previously saved Endpoint field value (following/?database=
), e.g.,/ru-central1/r1gra875baom********/g5n22e7ejfr1********
.
-
Click Save changes.
-
Under Overview, enable the Public function option.
Save the function ID, you will need it at the next steps.
Publish the service via API Gateway
To publish the service via API Gateway:
-
Go to your working folder.
-
In the list of services, select API Gateway.
-
Click Create API gateway.
-
In the Name field, enter
for-serverless-shortener
. -
Copy and paste the following code into the Specification section:
Specification
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> # <-- bucket name object: index.html # <-- HTML file name presigned_redirect: false service_account: <service_account_id> # <-- service account ID operationId: static /shorten: post: x-yc-apigateway-integration: type: cloud_functions function_id: <function_id> # <-- function ID operationId: shorten /r/{id}: get: x-yc-apigateway-integration: type: cloud_functions function_id: <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
Edit the specification code:
- Replace
<service_account_id>
with the ID of the previously created service account. - Replace
<function_id>
with the ID of the previously created function. - Replace
<bucket_name>
with the name of the previously created bucket.
- Replace
-
Click Create.
-
Click the name of the API gateway you created.
-
Copy the
url
value from the specification.Use this URL to work with the created service.
Test the URL shortener
To check that the service components interact properly:
-
Open the copied URL in the browser.
-
In the input field, enter the URL that you want to shorten.
-
Click Shorten.
You will see the shortened URL below.
-
Follow this link. As a result, the same page should open as when using the full URL.
How to delete the resources you created
To stop paying for the resources you created: