Yandex Cloud
Search
Discuss with expertTry it for free
  • Customer Stories
  • Documentation
  • Blog
  • All Services
  • System Status
  • Marketplace
    • Featured
    • Infrastructure & Network
    • Data Platform
    • AI for business
    • Security
    • DevOps tools
    • Serverless
    • Monitoring & Resources
  • All Solutions
    • By industry
    • By use case
    • Economics and Pricing
    • Security
    • Technical Support
    • Start testing with double trial credits
    • Cloud credits to scale your IT product
    • Gateway to Russia
    • Cloud for Startups
    • Center for Technologies and Society
    • Yandex Cloud Partner program
    • Price calculator
    • Pricing plans
  • Customer Stories
  • Documentation
  • Blog
© 2026 Direct Cursus Technology L.L.C.
Tutorials
    • All tutorials
      • Deploying a web application using the Java Servlet API
      • Developing a custom integration in API Gateway
        • Overview
        • Management console
        • Terraform
      • Developing CRUD APIs for a movie service
      • Creating an interactive serverless application using WebSocket
      • Working with an API gateway via WebSocket
      • Creating a Node.js function using TypeScript
      • Running a containerized app in Serverless Containers
      • Connecting to YDB from a Cloud Functions Python function
      • Connecting to a YDB database from a Node.js function in Cloud Functions
      • Setting up a Yandex Managed Service for PostgreSQL connection from a container in Serverless Containers
      • Deploying a web app with JWT authorization in API Gateway and authentication in Firebase
      • Developing functions in Functions Framework and deploying them to Yandex Serverless Containers
      • Canary release of a function in Cloud Functions
      • Interactive debugging of functions in Cloud Functions
      • Creating a Yandex Cloud Postbox address and a domain ownership check with Terraform
      • Configuring Postfix to send emails via Yandex Cloud Postbox

In this article:

  • Get your cloud ready
  • Required paid resources
  • Create an infrastructure
  • Test the URL shortener
  • How to delete the resources you created
  1. Serverless technologies
  2. Serverless-based backend
  3. URL shortener
  4. Terraform

URL shortener with the help of Terraform

Written by
Yandex Cloud
Updated at May 26, 2026
  • Get your cloud ready
    • Required paid resources
  • Create an infrastructure
  • Test the URL shortener
  • How to delete the resources you created

To create a URL shortener with the help of Terraform:

  1. Get your cloud ready.
  2. Create the infrastructure.
  3. Test the URL shortener.

If you no longer need the resources you created, delete them.

Get your cloud readyGet your cloud ready

Sign up for Yandex Cloud and create a billing account:

  1. Navigate to the management console and log in to Yandex Cloud or create a new account.
  2. On the Yandex Cloud Billing page, make sure you have a billing account linked and it has the ACTIVE or TRIAL_ACTIVE status. 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 resourcesRequired 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 infrastructureCreate an infrastructure

With Terraform, you can quickly create a cloud infrastructure in Yandex Cloud and manage it using configuration files. These files store the infrastructure description written in HashiCorp Configuration Language (HCL). If you change the configuration files, Terraform automatically detects which part of your configuration is already deployed, and what should be added or removed.

Terraform is distributed under the Business Source License. The Yandex Cloud provider for Terraform is distributed under the MPL-2.0 license.

For more information about the provider resources, see the relevant documentation on the Terraform website or its mirror.

To create your infrastructure via Terraform:

  1. Install Terraform, obtain authentication credentials, and specify the source for installing the Yandex Cloud provider. For details, see Configure your provider, step 1.

  2. Set up your infrastructure description files:

    Ready-made configuration
    Manually
    1. Clone the repository containing the configuration files.

      git clone https://github.com/yandex-cloud-examples/yc-serverless-url-shortener.git
      
    2. 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.
    1. Create a folder for configuration files.

    2. In the folder, create:

      1. serverless-url-shortener.tf configuration 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}"
        }
        
      2. serverless-url-shortener.auto.tfvars user data file:

        serverless-url-shortener.auto.tfvars
        cloud_id    = "<cloud_ID>" 
        folder_id   = "<folder_ID>"
        bucket_name = "<bucket_name>"
        
      3. The index.html page 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>
        
      4. index.py file 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.'})
        
      5. The requirements.txt file with environment parameters for the function in Cloud Functions:

        ydb
        
    3. In the folder, create an archive named function.zip containing the files requirements.txt and index.py.

    4. In the serverless-url-shortener.auto.tfvars file, specify these custom settings:

      • cloud_id: Cloud ID.
      • folder_id: Folder ID.
      • bucket_name: Name of the bucket to create the resources in.

    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.
  3. Create the resources:

    1. In the terminal, navigate to the configuration file directory.

    2. Make sure the configuration is correct using this command:

      terraform validate
      

      If the configuration is valid, you will get this message:

      Success! The configuration is valid.
      
    3. Run this command:

      terraform plan
      

      You 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.

    4. Apply the configuration changes:

      terraform apply
      
    5. Type yes and press Enter to confirm the changes.

  4. Copy the URL you got after creating the infrastructure to test the URL shortener.

Test the URL shortenerTest the URL shortener

To make sure your shortener components interact properly:

  1. In your browser, open the previously copied shortener URL.

  2. In the input field, enter the URL you want to shorten.

  3. Click Shorten.

    You will see the shortened URL below.

  4. Open the shortened URL. It should lead to the same page as the original link.

How to delete the resources you createdHow to delete the resources you created

To stop incurring charges for the resources you created:

  1. Open the serverless-url-shortener.tf file and delete your infrastructure description from it.

  2. Apply the changes:

    1. In the terminal, navigate to the configuration file directory.

    2. Make sure the configuration is correct using this command:

      terraform validate
      

      If the configuration is valid, you will get this message:

      Success! The configuration is valid.
      
    3. Run this command:

      terraform plan
      

      You 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.

    4. Apply the configuration changes:

      terraform apply
      
    5. Type yes and press Enter to confirm the changes.

See alsoSee also

  • Link shortener with the help of the management console

Was the article helpful?

Previous
Management console
Next
Developing CRUD APIs for a movie service
© 2026 Direct Cursus Technology L.L.C.