URL shortener
With this script, you'll 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. A 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:
- Before you start.
- Set up hosting for the URL shortener page.
- Create a service account.
- Create a database in Managed Service for YDB.
- Set up a function in Cloud Functions.
- Publish the service via API Gateway.
- Test the URL shortener.
If you no longer need the created resources, delete them.
Before you start
Before deploying the service, sign up for Yandex Cloud and create a billing account:
- Go to the management console. Then log in to Yandex Cloud or sign up if don't already have an account.
- On the billing page, make sure you linked a billing account, and it has the
ACTIVE
orTRIAL_ACTIVE
status. If you don't have a billing account, create one.
If you have an active billing account, you can create or select a folder to run your service components in using the Yandex Cloud page.
Learn more about clouds and folders.
Required paid resources
The cost of resources for the script includes:
- A fee for using the storage (see Yandex Object Storage pricing).
- A fee for accessing the database (see Yandex Managed Service for YDB pricing).
- A fee for function calls (see Yandex Cloud Functions pricing).
- A fee for requests to the API gateway (see Yandex 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 like
for-serverless-shortener
.Warning
Bucket names are unique throughout Object Storage, so you can't create two buckets with the same name (even in different folders in 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 the URL:</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>The ${error} error occurred, please try again</p>` }) } </script> </html>
-
Click on the name of the created bucket.
-
Click Upload objects.
-
Specify the prepared
index.html
file. -
Click Upload.
-
In the left pane, select Website.
-
Select Hosting.
-
Specify the website's homepage:
index.html
. -
Click Save.
Create a service account
To create a service account for the service components to interact:
-
Go to your working folder.
-
In the left pane, select Service accounts.
-
Click Create service account.
-
Enter the name of the service account:
serverless-shortener
. -
Click Add role and choose the
editor
role. -
Click Create.
-
Click on the name of the created service account.
Save the service account ID, you'll need it in the next steps.
Create a database in Managed Service for YDB
To create a 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 database.
-
Enter the DB name:
for-serverless-shortener
. -
Select the Serverless database type.
-
Click Create database.
-
Wait until the database starts.
When a database is being created, it has the
Provisioning
status. When it's ready for use, the status changes toRunning
. -
Click on the name of the created database.
Save the values of the Endpoint, Database, and Protocol fields of the YDB endpoint section. You'll need them in the next steps.
-
In the left pane, select Navigation.
-
Click 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 function name:
for-serverless-shortener
. -
Click Create.
-
In the Python drop-down list, choose the
python37
runtime environment. -
Click Next.
-
Copy the function code and paste it into the
index.py
file under Function code.Function codefrom kikimr.public.sdk.python import client as 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.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] # the URL may contain encoded characters like %, this will hinder the api-gateway to work properly when making a redirect, # 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 necessary because we have only one function # ideally, each path in the api-gw should have its own function 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 the 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 called using the api-gateway')
-
Copy the following text and paste it into the
requirements.txt
file under Function code.ydb==0.0.41
-
Specify the entry point:
index.handler
. -
Set the timeout value to
5
. -
Select the
serverless-shortener
service account. -
Add environment variables:
-
endpoint
: Enter a string that is generated from the database protocol and endpoint.For example, if the protocol is
grpcs
and the endpoint isydb.serverless.yandexcloud.net:2135
, entergrpcs://ydb.serverless.yandexcloud.net:2135
. -
database
: Enter the previously saved Database field value. -
USE_METADATA_CREDENTIALS
: Enter1
.
-
-
In the upper-right part of the Editor section, click Create version.
-
Under General information, enable Public function.
Save the function ID, you'll need it in 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:
Specificationopenapi: 3.0.0 info: title: for-serverless-shortener version: 1.0.0 paths: /: get: x-yc-apigateway-integration: type: object_storage bucket: for-serverless-shortener # <-- 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
-
Click Create.
-
Click on the name of the created API gateway.
-
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'll see the shortened URL below.
-
Follow this link. As a result, the same page should open as when using the full URL.
Delete the service components
To delete all the created service components:
- Delete the API gateway:
- Go to your working folder.
- In the list of services, select API Gateway.
- To the right of the API gateway name, click and select Delete.
- Click Delete.
- Delete the function:
- Go to your working folder.
- In the list of services, select Cloud Functions.
- To the right of the function name, click and select Delete.
- Click Delete.
- Delete the database:
- Go to your working folder.
- In the list of services, select Managed Service for YDB.
- To the right of the database name, click and select Delete.
- Click Delete.
- Delete the service account:
- Go to your working folder.
- In the left pane, select Service accounts.
- To the right of the service account name, click and select Delete.
- Click Delete.
- Delete the bucket:
- Go to your working folder.
- Select Object Storage.
- Click on the name of the created bucket.
- To the right of the bucket name, click and select Delete.
- Click Delete.
- Return to the Buckets page.
- To the right of the bucket name, click and select Delete.
- Click Delete.