URL shortener with the help of Terraform
To create a URL shortener with the help of Terraform:
If you no longer need the resources you created, delete them.
Get your cloud ready
Sign up for Yandex Cloud and create a billing account:
- Navigate to the management console
and log in to Yandex Cloud or create a new account. - On the Yandex Cloud Billing
page, make sure you have a billing account linked and it has theACTIVEorTRIAL_ACTIVEstatus. If you do not have a billing account, create one and link a cloud to it.
If you have an active billing account, you can create or select a folder for your infrastructure on the cloud page
Learn more about clouds and folders here.
Required paid resources
The infrastructure support cost for the URL shortener includes:
- Fee for data storage (see Yandex Object Storage pricing).
- Fee for YDB database operations and data storage (see Managed Service for YDB pricing).
- Fee for the number of function calls, computing resources allocated to run the function, and outgoing traffic (see Cloud Functions pricing).
- Fee for the number of requests to the API gateway and outgoing traffic (see API Gateway pricing).
Create an infrastructure
With Terraform
Terraform is distributed under the Business Source License
For more information about the provider resources, see the relevant documentation on the Terraform
To create your infrastructure via Terraform:
-
Install Terraform, obtain authentication credentials, and specify the source for installing the Yandex Cloud provider. For details, see Configure your provider, step 1.
-
Set up your infrastructure description files:
Ready-made configurationManually-
Clone the repository containing the configuration files.
git clone https://github.com/yandex-cloud-examples/yc-serverless-url-shortener.git -
Navigate to the repository directory. It should now contain the following files:
serverless-url-shortener.tf: New infrastructure configuration.serverless-url-shortener.auto.tfvars: User data file.index.html: Your service HTML page.function.zip: Cloud Functions function code archive.
-
Create a folder for configuration files.
-
In the folder, create:
-
serverless-url-shortener.tfconfiguration file:serverless-url-shortener.tf
# Declaring variables with sensitive data variable "cloud_id" { type = string } variable "folder_id" { type = string } variable "bucket_name" { type = string } # Configuring the provider terraform { required_providers { yandex = { source = "yandex-cloud/yandex" } } } provider "yandex" { cloud_id = var.cloud_id folder_id = var.folder_id } # Creating a service account resource "yandex_iam_service_account" "shortener_sa" { name = "serverless-shortener" description = "Service account for the URL shortener" } # Assigning roles to a service account resource "yandex_resourcemanager_folder_iam_member" "shortener_role" { folder_id = var.folder_id role = "editor" member = "serviceAccount:${yandex_iam_service_account.shortener_sa.id}" } # Creating a static key 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" } # Creating a bucket 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" } } # Upload an object to a bucket 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" } # Creating a YDB database resource "yandex_ydb_database_serverless" "shortener_db" { name = "shortener-ydb-main" location_id = "ru-central1" } # Creating a YDB table 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 ] } # Creating a function 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"] } # Creating an API gateway 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 for work with URL shortener output "url" { value = "https://${yandex_api_gateway.shortener_gateway.domain}" } -
serverless-url-shortener.auto.tfvarsuser data file:serverless-url-shortener.auto.tfvars
cloud_id = "<cloud_ID>" folder_id = "<folder_ID>" bucket_name = "<bucket_name>" -
The
index.htmlpage of your service:index.html
<!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>${error} occurred. Try again.</p>` }) } </script> </html> -
index.pyfile with the Cloud Functions function code: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): # Always returning string to 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("You need to specify both environment variables: endpoint and 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 # Paying attention to the structure of the result from 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': 'No body in the request body'}) body = decode(event, body) # Attempting to parse JSON with a url key; otherwise, treating body as a plain string url_value = None try: parsed = json.loads(body) if isinstance(parsed, dict): url_value = parsed.get('url') else: # If not a JSON object is sent, ignoring url_value = None except Exception: # If body is not JSON, treating it as plain URL url_value = body if not url_value: return response(400, {'Content-Type': 'application/json'}, False, {'error': 'The url parameter was expected in the request body'}) # Cleaning the URL from eventual coded characters clean_url = urllib.parse.unquote(url_value).strip() if not clean_url: return response(400, {'Content-Type': 'application/json'}, False, {'error': 'Empty url'}) link_id = hashlib.sha256(clean_url.encode('utf8')).hexdigest()[:6] insert_link(link_id, clean_url) # Returning the relative path; the frontend will add the origin by themselves 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: # Securely retrieving path params path_params = event.get('pathParams') or event.get('pathParameters') or {} link_id = path_params.get('id') if not link_id: # Probably, the full path was received in event['url'] or ['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': 'No 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': 'The link does not exist'}) 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': 'The path does not exist'}) def handler(event, context): url = event.get('url') or event.get('path') or '' if url: # Sometimes, the URL from the gateway comes with a question mark at the end if url.endswith('?'): url = url[:-1] return get_result(url, event) return response(404, {'Content-Type': 'application/json'}, False, {'error': 'The function must be called via API Gateway.'}) -
The
requirements.txtfile with environment parameters for the function in Cloud Functions:ydb
-
-
In the folder, create an archive named
function.zipcontaining the filesrequirements.txtandindex.py. -
In the
serverless-url-shortener.auto.tfvarsfile, specify these custom settings:
For more information about Terraform resource properties, see the relevant provider guides:
- Service account: yandex_iam_service_account.
- Static key: yandex_iam_service_account_static_access_key.
- Bucket: yandex_storage_bucket
- Object: yandex_storage_object
- Managed Service for YDB database: yandex_ydb_database_serverless.
- Managed Service for YDB table: yandex_ydb_table.
- Function: yandex_function.
- API gateway: yandex_api_gateway.
-
-
Create the resources:
-
In the terminal, navigate to the configuration file directory.
-
Make sure the configuration is correct using this command:
terraform validateIf the configuration is valid, you will get this message:
Success! The configuration is valid. -
Run this command:
terraform planYou will see a list of resources and their properties. No changes will be made at this step. Terraform will show any errors in the configuration.
-
Apply the configuration changes:
terraform apply -
Type
yesand press Enter to confirm the changes.
-
-
Copy the
URLyou got after creating the infrastructure to test the URL shortener.
Test the URL shortener
To make sure your shortener components interact properly:
-
In your browser, open the previously copied shortener URL.
-
In the input field, enter the URL you want to shorten.
-
Click Shorten.
You will see the shortened URL below.
-
Open the shortened URL. It should lead to the same page as the original link.
How to delete the resources you created
To stop incurring charges for the resources you created:
-
Open the
serverless-url-shortener.tffile and delete your infrastructure description from it. -
Apply the changes:
-
In the terminal, navigate to the configuration file directory.
-
Make sure the configuration is correct using this command:
terraform validateIf the configuration is valid, you will get this message:
Success! The configuration is valid. -
Run this command:
terraform planYou will see a list of resources and their properties. No changes will be made at this step. Terraform will show any errors in the configuration.
-
Apply the configuration changes:
terraform apply -
Type
yesand press Enter to confirm the changes.
-