Tutorial Serverless: Membuat API Menggunakan Deno pada AWS Lambda

4 min read

Disclaimer
Saya bekerja di AWS, semua opini adalah dari saya pribadi. (I work for AWS, my opinions are my own.)
Membuat API Menggunakan Deno pada AWS Lambda
Membuat API Menggunakan Deno pada AWS Lambda (Deno Logo: Wikipedia)

TeknoCerdas.com – Salam cerdas untuk kita semua. Tulisan ini melanjutkan tulisan sebelumnya yaitu membuat Deno runtime pada AWS Lambda. Pada tulisan ini kita akan membuat API menggunakan Deno pada AWS Lambda dan API Gateway.

API yang dibuat adalah sebuah API sederhana untuk membalik urutan huruf yang dikirimkan melalui HTTP POST. Berikut contoh request yang akan diterima oleh API.

$ curl -X POST \
> -H 'Content-Type: application/json' \
> -d '{ "words": "TeknoCerdas" }' \
> END_POINT_URL

API akan ditulis menggunakan Typescript dan dijalankan menggunakan custom Deno runtime yang telah dibuat pada tulisan sebelumnya.

Daftar Isi:

Persiapan Membuat API Menggunakan Deno

Sebelum mulai membuat API Deno menggunakan AWS Lambda terdapat beberapa prasyarat yang harus anda penuhi.

  • Memiliki akun AWS yang aktif
  • Memiliki pemahaman dasar tentang AWS Lambda
  • Memiliki pemahaman dasar tentang AWS IAM
  • Memiliki pemahaman dasar tentang AWS CLI
  • Memiliki pemahaman dasar tentang AWS S3
  • Memiliki pemahaman dasar tentang HTTP dan JSON
  • Memiliki pemahaman dasar tentang Deno dan Shell script

Jika anda tidak memiliki prasyarat diatas silahkan lanjutkan membaca. Karena mungkin banyak informasi baru yang diperoleh meskipun tanpa mencoba langsung tutorial ini.

Membuat Fungsi Lambda Baru untuk Deno API

Pada bagian ini kita akan membuat fungsi Lambda yang akan menggunakan Deno runtime yang disediakan oleh Lambda Layer yang dibuat pada tulisan sebelumnya.

  1. Masuk pada halaman Functions pada console AWS Lambda
  2. Tekan tombol Create Function untuk membuat Lambda baru
  3. Pada Function Name isikan nama API sebagai contoh “MyDenoAPI”
  4. Pada pilihan Runtime gunakan “Use default bootstrap”
  5. Pada Execution role pilh “Create a new role with basic Lambda permissions”. Permission ini diperlukan agar Lambda dapat melakukan logging ke CloudWatch.
  6. Akhiri dengan menekan tombol Create function.

Langkah berikutnya adalah melakukan editing file yang diperlukan. Tapi sebelumnya mari kita ubah nama file hello.sh menjadi Typescript file yaitu main.ts.

  1. Pada daftar fungsi yang ada klik “MyDenoAPI” untuk masuk ke halaman editor.
  2. Pada window Function Code klik kanan hello.sh lalu ganti menjadi main.ts.
  3. Konfigurasi dari Handler juga harus diubah dari hello.handler menjadi main.handler.
Membuat Fungsi Lambda Baru untuk API Deno
Membuat Fungsi Lambda Baru untuk Deno API

Kita akan mengubah bootstrap bawaan dari Lambda dengan milik kita sendiri. File bootstrap adalah file entry point yang akan dieksekusi pertama kali oleh Lambda. File bootstrap dapat berupa file binary atau sebuah shell script.

File bootstrap yang kita tulis berfungsi untuk memanggil Deno runtime yang berlokasi di /opt/bin/deno. Runtime deno tersebut hanya tersedia jika menggunakan Lambda Layer yang dibuat pada tulisan sebelumnya.

File bootstrap akan mengeksekusi Deno dan akan menjalankan Typescript file main.ts.

#!/bin/bash
# Resolve to /opt/bin/deno
export PATH=/opt/bin:$PATH

# Handler format: .
#
# The script file .sh  must be located at the root of your
# function's deployment package, alongside this bootstrap executable.

# Split filename and function name using '.'
# 0 => File name, 1 => Func name (not used)
IFS="." read -ra FN <<< "$_HANDLER"

# Cached files will be saved to DENO_DIR
[ -z "$DENO_DIR" ] && export DENO_DIR=/tmp/deno_dir
[ -z "$NO_COLOR" ] && export NO_COLOR=true
deno run --allow-read --allow-net --allow-env "${FN[0]}.ts"

File Typescript ini adalah file utama yang berisi logika untuk memproses JSON payload yang dikirimkan dan membalik susunan huruf yang ada.

/**
 * Declare minimal interfaces for properies that used in Lambda
 */
interface RequestBody {
  words: string;
}

interface ResponseMeta {
  ip_addr: string;
  user_agent: string;
}

interface ResponseBody {
  original: string;
  reversed: string | null;
  meta: ResponseMeta;
}

interface LambdaEvent {
  body: string;
}

interface HttpContext {
  sourceIp: string;
  userAgent: string;
}

interface LambdaContextHttp {
  identity: HttpContext;
}

/**
 * Main handler for Lambda event and request context
 */
async function handler(lambda_event: LambdaEvent, lambda_context: LambdaContextHttp) {
  // Get request body
  let body: RequestBody = JSON.parse(lambda_event.body);
  let response: ResponseBody = {
    reversed: null,
    original: body.words,
    meta: {
      ip_addr: lambda_context.identity.sourceIp,
      user_agent: lambda_context.identity.userAgent
    }
  };

  try {
    response.reversed = body.words.split('').reverse().join('');
  } catch (e) {}

  return JSON.stringify(response);
}

async function lambda_main_loop()
{
  const AWS_LAMBDA_RUNTIME_API = Deno.env.get('AWS_LAMBDA_RUNTIME_API');
  const LAMBDA_BASE_URL = "http://" + AWS_LAMBDA_RUNTIME_API + "/2018-06-01/runtime/invocation";
  let resp: Response | null = null;

  while (true) {
    try {
      resp = await fetch(LAMBDA_BASE_URL + "/next", {
          headers: {
              'Content-Type': 'application/json'
          }
      });
      const evt = await resp.json();
      const invocation_id = resp.headers.get('Lambda-Runtime-Aws-Request-Id');
      const http_context = {
        identity: {
          sourceIp: evt.requestContext.http.sourceIp,
          userAgent: evt.requestContext.http.userAgent,
        }
      }
      const handler_resp = await handler(evt, http_context);

      resp = await fetch(LAMBDA_BASE_URL + "/" + invocation_id + "/response", {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: handler_resp
      });

      if (!resp.ok) { console.error(resp) };
    } catch (e) {
      console.error(e);
    }
  }
}

// Run from AWS Lambda
if (Deno.env.get('AWS_LAMBDA_RUNTIME_API')) {
  lambda_main_loop();
}

// Run from CLI
if (!Deno.env.get('AWS_LAMBDA_RUNTIME_API')) {
  const decoder = new TextDecoder('utf-8');
  const json_input = JSON.parse(decoder.decode(Deno.readFileSync('./event.json')));
  // console.log(json_input);

  const evt = {
    body: json_input.body
  };

  const ctx = {
    identity: {
      sourceIp: json_input.requestContext.http.sourceIp,
      userAgent: json_input.requestContext.http.userAgent
    }
  };

  const output = await handler(evt, ctx);
  console.log(output);
}

Terdapat fungsi lambda_main_loop() yang berfungsi untuk melakukan komunikasi ke AWS Lambda. Secara terus menerus fungsi tersebut akan 1) Mengambil request dari Lambda 2) Memproses request tersebut 3) Mengirimkan hasilnya kembali ke Lambda.

Response JSON yang dikembalikan oleh fungsi ini tidak hanya teks yang dibalik tapi juga IP address dan User Agent.

{
  "reversed": "0202 moc.sadreConkeT",
  "original": "TeknoCerdas.com 2020",
  "meta": {
    "ip_addr": "127.0.0.1",
    "user_agent": "curl/7.54.0"
  }
}

Menggunakan Deno Runtime Lambda Layer

Sampai disini fungsi Lambda yang dibuat sebelumnya belum bisa dijalankan karena ketergantungan pada file executable /opt/bin/deno. Fungsi ini harus menggunakan Layer “MyDenoLayer” yang dibuat sebelumnya agar dapat menjalankan runtime Deno.

  1. Pada Designer window pilih Layers dibawah MyDenoAPI
  2. Scroll kebawah kemudian tekan Add a Layer
  3. Pada pilihan Layer Selection pilih Select from list of runtime compatible layers
  4. Pada Name pilih “MyDenoLayer” dan Version 1.
  5. Tekan tombol Add untuk menambahkan Layer.
Menambahkan Lambda Layer Deno Runtime
Menambahkan Lambda Layer Deno Runtime

Deploy Deno API Menggunakan API Gateway

Setelah menyelesaikan fungsi pada Lambda langkah berikutnya adalah menghubungkan Lambda dengan API Gateway agar bisa diakses dari internet.

Untuk itu kita perlu menambahkan trigger pada Lambda yang dibuat. Pada window Designer klik tombol Add Trigger dan ikuti langkah berikut untuk menambahkan API Gateway.

  1. Pada trigger pilih “API Gateway”
  2. API: “Create an API” untuk membuat API Gateway baru
  3. API type: “HTTP API”
  4. Security: “Open”
  5. API name: “DenoAPI”
  6. Deployment stage: “$default” (dengan dollar didepan)
  7. Centang “Cross-origin resource sharing (CORS)”

Klik tombol Add untuk membuat API Gateway. Anda dapat melihat URL API endpoint  pada bagian bawah halaman. Contoh sebuah end point dari API Gateway adalah https://RANDOM_STRING.execute-api.us-west-1.amazonaws.com/words.

Test Deno API

Untuk melakukan test kita cukup menggunakan utilitas CLI seperti curl atau POSTman jika lebih suka menggunakan GUI. Pada test ini JSON payload yang akan dikirimkan seperti berikut.

{
   "words": "TeknoCerdas.com Berita Teknologi yang Mencerdaskan"
}

Lakukan HTTP POST pada endpoint URL yang ditampilkan oleh API Gateway.

$ cat < -H "Content-Type: application/json" -d @- \
> 'https://jm8550zz73.execute-api.us-east-1.amazonaws.com/words'
> {
>    "words": "TeknoCerdas.com Berita Teknologi yang Mencerdaskan"
> }
> EOF
{
  "reversed": "naksadrecneM gnay igolonkeT atireB moc.sadreConkeT",
  "original": "TeknoCerdas.com Berita Teknologi yang Mencerdaskan",
  "meta": {
    "ip_addr": "36.84.145.252",
    "user_agent": "curl/7.54.0"
  }
}

Dari beberapa kali percobaan penulis waktu yang diperlukan untuk Cold Start dari API sekitar 1 detik. Setelah itu waktu eksekusi Deno API yang dibuat konsisten pada angka 2 – 3 ms. Cukup impresif.

Kode Sumber

Kode sumber untuk tutorial ini dapat anda lihat pada tautan github berikut:

https://github.com/rioastamal-examples/lambda-custom-deno-runtime

Pada kode sumber tersebut terdapat Terraform script yang digunakan untuk membangun semua resources yang dibutuhkan. Juga terdapat kode Typescript untuk API yang digunakan pada tulisan ini.