[Yandex Cloud documentation](../../../index.md) > [Tutorials](../../index.md) > [Serverless technologies](../index.md) > Serverless-based backend > [URL shortener](index.md) > Terraform

# Creating a URL shortener with Terraform


To create a [URL shortener](index.md) with the help of Terraform:

1. [Get your cloud ready](#before-begin).
1. [Create the infrastructure](#deploy).
1. [Test the URL shortener](#test).

If you no longer need the resources you created, [delete them](#clear-out).

## Get your cloud ready {#before-begin}

Sign up for Yandex Cloud and create a [billing account](../../../billing/concepts/billing-account.md):
1. Navigate to the [management console](https://console.yandex.cloud) and log in to Yandex Cloud or create a new account.
1. On the **[Yandex Cloud Billing](https://center.yandex.cloud/billing/accounts)** page, make sure you have a billing account linked and it has the `ACTIVE` or `TRIAL_ACTIVE` [status](../../../billing/concepts/billing-account-statuses.md). If you do not have a billing account, [create one](../../../billing/quickstart/index.md) and [link](../../../billing/operations/pin-cloud.md) a cloud to it.

If you have an active billing account, you can create or select a [folder](../../../resource-manager/concepts/resources-hierarchy.md#folder) for your infrastructure on the [cloud page](https://console.yandex.cloud/cloud).

[Learn more about clouds and folders here](../../../resource-manager/concepts/resources-hierarchy.md).

### Required paid resources {#paid-resources}

The infrastructure support cost for the URL shortener includes:
* Fee for data storage (see [Yandex Object Storage pricing](../../../storage/pricing.md)).
* Fee for [YDB database](../../../ydb/concepts/resources.md#database) operations and data storage (see [Managed Service for YDB pricing](../../../ydb/pricing/serverless.md)).
* Fee for the number of [function](../../../functions/concepts/function.md) calls, computing resources allocated for the function, and outgoing traffic (see [Cloud Functions pricing](../../../functions/pricing.md)).
* Fee for the number of requests to the [API gateway](../../../api-gateway/concepts/index.md) and outgoing traffic (see [API Gateway pricing](../../../api-gateway/pricing.md)).

## Create an infrastructure {#deploy}

With [Terraform](https://www.terraform.io/), 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](https://github.com/hashicorp/terraform/blob/main/LICENSE). The [Yandex Cloud provider for Terraform](https://github.com/yandex-cloud/terraform-provider-yandex) is distributed under the [MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/) license.

For more information about the provider resources, see the guides on the [Terraform](https://www.terraform.io/docs/providers/yandex/index.html) website or [its mirror](../../../terraform/index.md).

To create your infrastructure via Terraform:
1. [Install Terraform](../../infrastructure-management/terraform-quickstart.md#install-terraform), [get authentication credentials](../../infrastructure-management/terraform-quickstart.md#get-credentials), and specify the source for installing the Yandex Cloud provider. For details, see [Configure your provider](../../infrastructure-management/terraform-quickstart.md#configure-provider), step 1.
1. Set up your infrastructure description files:

   {% list tabs group=infrastructure_description %}

   - Ready-made configuration {#ready}

     1. Clone the repository containing the configuration files.

        ```bash
        git clone https://github.com/yandex-cloud-examples/yc-serverless-url-shortener.git
        ```

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

   - Manually {#manual}

     1. Create a folder for configuration files.
     1. In the folder, create:
        1. `serverless-url-shortener.tf` configuration file:

           {% cut "serverless-url-shortener.tf" %}

           ```hcl
           # 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}"
           }
           ```

           {% endcut %}

        1. `serverless-url-shortener.auto.tfvars` user data file:

           {% cut "serverless-url-shortener.auto.tfvars" %}

           ```hcl
           cloud_id    = "<cloud_ID>" 
           folder_id   = "<folder_ID>"
           bucket_name = "<bucket_name>"
           ```

           {% endcut %}

        1. The `index.html` page of your service:

           {% cut "index.html" %}

           ```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>
           ```

           {% endcut %}

        1. `index.py` file with the Cloud Functions function code:

           {% cut "index.py" %}

           ```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.'})
           ```

           {% endcut %}

        1. The `requirements.txt` file with environment parameters for the function in Cloud Functions:

           ```text
           ydb
           ```

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

     1. In the `serverless-url-shortener.auto.tfvars` file, set the following user-defined properties:

          * `cloud_id`: [Cloud ID](../../../resource-manager/operations/cloud/get-id.md).
          * `folder_id`: [Folder ID](../../../resource-manager/operations/folder/get-id.md).
          * `bucket_name`: Name of the [bucket](../../../storage/concepts/bucket.md) to create the resources in.

   {% endlist %}
   
   For more on the properties of resources used in Terraform, see these provider guides:
   
   * [Service account](../../../iam/concepts/users/service-accounts.md): [yandex_iam_service_account](../../../terraform/resources/iam_service_account.md).
   * [Static key](../../../iam/concepts/authorization/access-key.md): [yandex_iam_service_account_static_access_key](../../../terraform/resources/iam_service_account_static_access_key.md).
   * [Bucket](../../../storage/concepts/bucket.md): [yandex_storage_bucket](../../../terraform/resources/storage_bucket.md)
   * [Object](../../../storage/concepts/object.md): [yandex_storage_object](../../../terraform/resources/storage_object.md)
   * [Managed Service for YDB database](../../../ydb/concepts/resources.md#database): [yandex_ydb_database_serverless](../../../terraform/resources/ydb_database_serverless.md).
   * [Managed Service for YDB table](../../../ydb/concepts/dynamodb-tables.md): [yandex_ydb_table](../../../terraform/resources/ydb_table.md).
   * [Function](../../../functions/concepts/function.md): [yandex_function](../../../terraform/resources/function.md).
   * [API gateway](../../../api-gateway/concepts/index.md): [yandex_api_gateway](../../../terraform/resources/api_gateway.md).

1. Create the resources:

   1. In the terminal, navigate to the configuration file directory.
   1. Make sure the configuration is correct using this command:
   
      ```bash
      terraform validate
      ```
   
      If the configuration is valid, you will get this message:
   
      ```bash
      Success! The configuration is valid.
      ```
   
   1. Run this command:
   
      ```bash
      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.
   1. Apply the configuration changes:
   
      ```bash
      terraform apply
      ```
   
   1. Type `yes` and press **Enter** to confirm the changes.

1. Copy the `URL` you got after creating the infrastructure to test the [URL shortener](#test).

## Test the URL shortener {#test}

To make sure your shortener components interact properly:
1. In your browser, open the previously copied shortener URL.
1. In the input field, enter the URL you want to shorten.
1. Click **Shorten**.

   You will see the shortened URL below.
1. Open the shortened URL. It should lead to the same page as the original link.

## How to delete the resources you created {#clear-out}

To stop paying for the resources you created:

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

    1. In the terminal, navigate to the configuration file directory.
    1. Make sure the configuration is correct using this command:
    
       ```bash
       terraform validate
       ```
    
       If the configuration is valid, you will get this message:
    
       ```bash
       Success! The configuration is valid.
       ```
    
    1. Run this command:
    
       ```bash
       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.
    1. Apply the configuration changes:
    
       ```bash
       terraform apply
       ```
    
    1. Type `yes` and press **Enter** to confirm the changes.

#### Useful links {#see-also}

* [Link shortener from the management console](console.md)