Serverless PHP: Membuat API dengan AWS Lambda dan Runtime Bref

5 min read

Disclaimer
Saya bekerja di AWS, semua opini adalah dari saya pribadi. (I work for AWS, my opinions are my own.)
Serverless PHP: Membuat API dengan AWS Lambda
Serverless PHP dengan AWS Lambda (Logo Hak Cipta Masing-masing)

TeknoCerdas.com – Salam cerdas untuk kita semua. Pada tutorial kali ini kita akan membahas tentang Serverless PHP. Tulisan ini akan memandu bagaimana membuat API dengan AWS Lambda dan Runtime Bref PHP.

API yang dibuat adalah sebuah API yang akan mengembalikan alamat IP dari pengguna dalam bentuk JSON. API dapat diakses melalui protokol HTTP dengan metode GET.

Berikut contoh API yang dibuat ketika digunakan.

$ curl -H "Content-Type: application/json" API_URL
{
  "ipv4_address": "USER_IP_ADDR",
  "user_agent": "USER_AGENT",
  "time": "SERVER_TIME_IN_UTC"
}

Dalam membangun serverless API ini kita akan menggunakan layanan dari AWS yaitu Lambda dan API Gateway. AWS Lambda tidak menyediakan runtime khusus untuk PHP. Untuk itu akan digunakan Lambda Layer dari Bref PHP.

Daftar Isi

Sekilas Tentang Runtime Bref PHP

Bref adalah sebuah project untuk menjalankan PHP pada lingkungan Serverless. Untuk saat ini Bref hanya mendukung layanan serverless dari AWS Lambda.

Bref menyediakan dukungan untuk menjalankan aplikasi PHP murni atau framework seperti Laravel dan Symfony pada AWS Lambda.

Bref menyediakan runtime untuk beberapa varian dan setiap varian juga memiliki versi PHP yang berbeda. Daftar dari runtime yang didukung oleh Bref per tulisan dibuat adalah php-74, php-74-fpm, php-73, php-73-fpm, php-72, php-72-fpm dan console.

Pada tutorial ini TeknoCerdas tidak menggunakan Serverless Framework dan hanya memanfaatkan AWS Console untuk membuat API. Runtime yang digunakan pada tutorial ini adalah php-74.

Persiapan Membuat API My IP

Sebelum mulai membuat API My IP 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 HTTP dan JSON
  • Memiliki pemahaman dasar tentang PHP 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 Baru di Lambda

Pada bagian ini kita akan membuat sebuah fungsi Lambda yang berfungsi untuk mendapatkan alamat IP dari pengguna.

  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 “PHPMyIP”
  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 PHP file yaitu main.php.

  1. Pada daftar fungsi yang ada klik “PHPMyIP” untuk masuk ke halaman editor.
  2. Pada window Function Code klik kanan hello.sh lalu ganti menjadi main.php.
  3. Konfigurasi dari Handler juga harus diubah dari hello.handler menjadi main.handler.

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 PHP binary yang telah disediakan oleh Bref PHP runtime. File binary PHP berlokasi di /opt/bin/php.

File bootstrap akan mengeksekusi PHP dan akan menjalankan file main.php.

#!/bin/sh

# Fail on error
set -e

# Get Handler name
# 0 => File name
# 1 => Function name
IFS="." read -ra FN <<< "$_HANDLER"

while true
do
  # All errors to STDOUT so it can be captured to CloudWatch
  /opt/bin/php "${FN[0]}.php" 2>&1
done

Isi dari bootstrap sangat sederhana. Singat, padat dan jelas karena fungsinya hanya memanggil file PHP handler. Semua logika pemrosesan event di Lambda dilakukan langsung di script PHP.

File PHP ini adalah file utama yang berisi logika untuk memproses HTTP request dari API Gateway dan mengembalikan informasi alamat IP pengguna yang melakukan request.

<?php
namespace App;

/**
 * Handler function which return a response and will be send back to Lambda
 *
 * @array $event Lambda Event
 * @return String
 */
function handler($event)
{
  $response = [];
  $response['ipv4_address'] = $event['requestContext']['http']['sourceIp'];
  $response['user_agent'] = $event['requestContext']['http']['userAgent'];
  $response['time'] = gmdate('Y-m-d H:i:s') . ' UTC';

  return json_encode($response);
}

/**
 * Simple abstraction for HTTP GET Lambda invocation
 *
 * @param String $url
 * @return Array
 */
function fetch($url)
{
  $headers = [
    'Content-Type: application/json',
    'User-Agent: TeknoCerdas/1.0',
    'Accept: application/json'
  ];
  $options = [
    'http' => [
      'method' => 'GET',
      'header' => implode("\r\n", $headers)
    ]
  ];
  $context = stream_context_create($options);
  $response = file_get_contents($url, false, $context);

  // Parse special variables called $http_response_header
  $requestId = null;

  foreach ($http_response_header as $value) {
    if (preg_match('/Lambda-Runtime-Aws-Request-Id/', $value)) {
      $requestId = trim(explode('Lambda-Runtime-Aws-Request-Id:', $value)[1]);
    }
  }

  return [
    'body' => json_decode($response, $toArray = true),
    'request_id' => $requestId
  ];
}

/**
 * Simple abstraction for HTTP POST
 *
 * @param String $url
 * @param String $data
 * @return String
 */
function post($url, $data)
{
  $headers = [
    'Content-Type: application/json',
    'User-Agent: TeknoCerdas/1.0',
    'Content-Length: ' . strlen($data)
  ];
  $options = [
    'http' => [
      'method' => 'POST',
      'header' => implode("\r\n", $headers),
      'content' => $data
    ]
  ];

  $context = stream_context_create($options);
  return file_get_contents($url, false, $context);
}

/**
 * Lambda main loop
 */
function main()
{
  $lambdaRuntimeApi = getenv('AWS_LAMBDA_RUNTIME_API');
  $lambdaBaseUrl = sprintf('http://%s/2018-06-01/runtime/invocation/', $lambdaRuntimeApi);
  $maxLoop = getenv('MAX_LOOP') ?: 10;
  $currentLoop = 0;

  while (true)
  {
    if (++$currentLoop > $maxLoop) {
      break;
    }

    $nextEvent = fetch($lambdaBaseUrl . 'next');
    $handlerResponse = handler($nextEvent['body']);

    $responseUrl = sprintf('%s%s/response', $lambdaBaseUrl, $nextEvent['request_id']);
    $lambdaResponse = post($responseUrl, $handlerResponse);
  }
}

// Run for Lambda
if (getenv('AWS_LAMBDA_RUNTIME_API')) {
  main();
  exit(0);
}

// Should be run from CLI
$payload = file_get_contents(__DIR__ . '/event.json');
echo handler(json_decode($payload, $toArray = true));

Fungsi handler() akan memproses Lambda event payload dan mengekstrak informasi berupa alamat IP dan user agent. Fungsi ini mengembalikan JSON string.

Fungsi fetch() melakukan HTTP request dengan metode GET ke URL Lambda yang akan mengembalikan event payload. Fungsi ini tidak menggunakan cURL dan hanya memanfaatkan file_get_contents dan stream.

Fungsi post() melakukan HTTP request dengan metode POST ke URL Lambda berisi response yang harusnya diterima oleh pengguna. Response yang dikirim dalam bentuk JSON string.

Fungsi main() adalah fungsi utama yang berfungsi untuk mengorganisasikan pemrosesan yang harus dilakukan. Disini juga terdapat loop dengan konfigurasi tertentu untuk kepentingan performa.

Dimana jika tanpa loop maka yang bertindak untuk eksekusi berikutnya adalah bootstrap yang otomatis akan ada pemanggilan ulang file main.php. Dengan adanya loop disini maka jika ada request baru yang datang akan langsung ditangani karena script main.php belum dihentikan.

Menggunakan Bref PHP Runtime pada Lambda Layer

Sampai disini fungsi Lambda yang dibuat sebelumnya belum bisa dijalankan karena ketergantungan pada file binary PHP yang berlokasi di /opt/bin/php.

Fungsi ini harus memerlukan Bref PHP Runtime sebagai Lambda Layer agar PHP binary tersedia ketika fungsi Lambda dieksekusi.

  1. Pada Designer window pilih Layers dibawah PHPMyIP
  2. Scroll kebawah kemudian tekan Add a Layer
  3. Pada pilihan Layer Selection pilih Provide a layer version ARN
  4. Pada Layer version ARN isikan ARN dari Bref PHP versi 7.4 yaitu arn:aws:lambda:us-east-1:209497400698:layer:php-74:11
  5. Tekan tombol Add untuk menambahkan Layer.

Sesuaikan ARN dengan region tempat fungsi Lambda ditempatkan. Daftar lengkapnya ada di https://runtimes.bref.sh.

Deploy 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: “PhpMyIpApi”
  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-east-1.amazonaws.com/myip.

Tes PHP API MyIP

Untuk melakukan test kita cukup menggunakan utilitas CLI HTTP client seperti curl atau POSTman jika lebih suka menggunakan GUI. Kita cukup melakukan request ke endpoint dari API Gateway yang telah dibuat sebelumnya.

$ curl -s -H "Content-Type: application/json" 'https://d768z7u49a.execute-api.us-east-1.amazonaws.com/myip' | jq .
{
  "ipv4_address": "36.82.99.121",
  "user_agent": "curl/7.54.0",
  "time": "2020-07-03 14:46:16 UTC"
}

Pada perintah diatas digunakan utilitas jq untuk melakukan format JSON. Jika tidak memiliki jq maka tidak perlu melakukan piping | jq . seperti diatas.

Dalam puluhan kali percobaan yang TeknoCerdas lakukan dengan memory 512MB rata-rata durasi ekesekusi berkisar antara 0.8 – 1.4ms. Memory yang digunakan rata-rata 57MB. Sebuah angka yang cukup impresif meskipun tes ini hanya sebuah tes sederhana.

Kode Sumber

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

https://github.com/rioastamal-examples/aws-lambda-php-bref-runtime

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