{Pengenalan Serverless} {Pengenalan AWS Lambda} {Serverless PHP} {Serverless Deployment} {Serverless Demo} {Profiling Lambda Function}*
Menjalankan kode aplikasi tanpa perlu memikirkan server (autoscaling dkk).
Dalam konteks ini adalah Function as a Service (FaaS).
{Zero Administration} {Bayar per Eksekusi} {Idle = Gratis} {Auto-Scaling} {Microservice sejak awal} {Event-Driven} {Faster time to market}
Yang akan kamu kerjakan
Task | VM | Serverless |
Manage Auto-Scaling | ✓ | ⤫ |
Konfigurasi Server | ✓ | ⤫ |
Manage OS | ✓ | ⤫ |
Bayar Running Cost | ✓ | ⤫ |
Upload Kode | ✓ | ✓ |
Layanan Serverless dari AWS yang mendukung penggunaan kode NodeJS, Python, Java, Go, .NET Core, Ruby dan Custom Runtime.
Biaya dihitung per 100ms durasi waktu dan Σ eksekusi.
Gabungan dari sistem operasi, bahasa pemrograman dan pustaka-pustaka lain yang dibutuhkan untuk menjalankan kode.
def my_handler(event, context):
return {
"message": "Hello World"
}
{
"resource": "/",
"path": "/",
"httpMethod": "GET",
"requestContext": {
"resourcePath": "/",
"httpMethod": "GET",
"path": "/Prod/",
...
},
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding": "gzip, deflate, br",
"Host": "70ixmpl4fl.execute-api.us-east-2.amazonaws.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-5e66d96f-7491f09xmpl79d18acf3d050",
...
},
"multiValueHeaders": {
"accept": [
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
],
"accept-encoding": [
"gzip, deflate, br"
],
...
},
"queryStringParameters": null,
"multiValueQueryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"body": null,
"isBase64Encoded": false
}
Tidak ada :( #justkidding
Bref adalah Runtime PHP, Framework dan deployment tool agar PHP dapat dijalankan di AWS Lambda.
Dibuat dari amazonlinux:2018.03 Docker image
Layer | Cocok untuk |
---|---|
php-* | PHP Function (from scratch) |
php-*-fpm | Website/Framework |
console | Console (CLI) |
App | Tipe | Lambda Layer |
Terbilang | HTTP API | bref php-73 |
SQL Query Browser | Normal Web | bref php-73-fpm |
New File Notification* | S3 Event | bref php-73 |
What is My IP* | HTTP API + Normal Web | bref php-73 |
*) Hanya menggunakan Bref PHP Runtime tanpa Bref Framework
Sebuah layanan HTTP API yang akan mengubah angka "123" menjadi bentuk terbilang "seratus dua puluh tiga".
$ cat composer.json
{
"require": {
"bref/bref": "^0.5.29",
"rioastamal/terbilang": "^1.0"
}
}
$ composer install -vvv
$ ./vendor/bin/bref init
What kind of lambda do you want to create? (you will be able to add more functions later by editing `serverless.yml`) [PHP function]:
[0] PHP function
[1] HTTP application
[2] Console application
> 0
<?php declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use RioAstamal\AngkaTerbilang\Terbilang;
/**
* @param array $event
* @see https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html for example
* @return callable
*/
return function ($event)
{
$angka = $event['queryStringParameters']['angka'] ?? '0';
$options = isset($event['queryStringParameters']['pretty']) ? JSON_PRETTY_PRINT : 0 ;
$response = [
'angka' => $angka,
'terbilang' => Terbilang::create()->t($angka)
];
return json_encode($response, $options);
};
$ serverless deploy
$ curl --get -d 'angka=1500,004,300' -d 'pretty' [END_POINT]
{
"angka": "1500,004,300",
"terbilang": "satu milyar lima ratus juta empat ribu tiga ratus"
}
Sebuah web app untuk melakukan query pada MySQL dan menampilkan hasilnya secara langsung.
$ cd terraform/
$ export AWS_PROFILE=YOUR_PROFILE AWS_DEFAULT_REGION=YOUR_REGION
$ terraform apply
Outputs:
database = {
"connect" = "mysql -u awsug -h [HOST_NAME] -p serverlessphp"
"endpoint" = "[HOST_NAME]:3306"
"engine" = "mysql 5.7.26"
}
$ cat composer.json
{
"require": {
"bref/bref": "^0.5.29",
}
}
$ composer install -vvv
$ ./vendor/bin/bref init
What kind of lambda do you want to create? (you will be able to add more functions later by editing `serverless.yml`) [PHP function]:
[0] PHP function
[1] HTTP application
[2] Console application
> 1
<?php
$dsn = $_POST['dsn'] ?? '';
$username = $_POST['username'] ?? 'root';
$password = $_POST['password'] ?? '';
$query = $_POST['query'] ?? '';
$pdo = null;
function exec_query($pdo, $query)
{
if (empty($query)) {
return '// Result will be shown here';
}
$counter = 0;
try {
echo '<table>';
foreach ($pdo->query($query, PDO::FETCH_ASSOC) as $row) {
// --- HEADING --- //
if ($counter === 0) {
echo '<thead><tr>';
foreach ($row as $key => $val) {
printf('<td>%s</td>', $key);
}
echo '</tr></thead><tbody>';
}
// --- /HEADING --- //
echo '<tr>';
foreach ($row as $val) {
printf('<td>%s</td>', $val);
}
echo '</tr>';
$counter++;
}
echo '</tbody></table>';
} catch (PDOException $e) {
printf('<pre>Error: %s</pre>', $e->getMessage());
}
}
if ($dsn) {
try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Query Browser on AWS Lambda</title>
<style type="text/css">
label {
display: block;
}
div.label-wrapper {
width: 300px;
}
table {
border-collapse: collapse;
width: 100%;
}
thead td {
background-color: #f1f1f1;
font-weight: bold;
}
tr td {
border: 1px solid #ccc;
padding: 4px 8px;
}
</style>
</head>
<body>
<h2>Query Browser on AWS Lambda</h2>
<form method="post">
<label>MySQL DSN</label>
<input type="text" name="dsn" placeholder="mysql:dbname=testdb;host=127.0.0.1" style="width: 300px;" value="<?= $dsn ?>">
<label>Username / Password</label>
<input type="text" name="username" placeholder="MySQL Username" value="<?= $username; ?>">
<input type="password" name="password" placeholder="MySQL Password" value="<?= $password; ?>">
<br><br>
<label>SQL Query</label>
<textarea style="width: 99%;height: 200px; font-size: 24px; color: blue;" name="query"><?= htmlentities($query) ?></textarea>
<input type="submit" value="RUN">
</form>
<hr>
<?php exec_query($pdo, $query) ?>
</body>
</html>
$ serverless deploy
Sebuah fungsi Lambda yang akan mengirimkan notifikasi ke email jika ada file baru pada bucket tertentu.
$ bash build.sh
> Creating zip file into build/function.zip ...
Generating autoload files
Generated autoload files
< Lambda function has been zipped.
$ bash deploy.sh
...
Outputs:
demo = {
"mailhog" = {
"ec2_ip" = "18.139.0.199"
"mailhog_auth" = "user = teknocerdas | password = orang.cerdas"
"ssh_access" = "ssh -i ~/.ssh/teknocerdas.key -o LogLevel=quiet -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null [email protected]"
"url" = "https://mailhog.teknocerdas.com"
}
"s3_bucket" = {
"butcket_name" = "awsug06-sub-demo"
"create_example" = "aws s3 cp ./YOUR_FILE.ext s3://awsug06-sub-demo/"
}
}
#!/bin/sh
# Main entry point for Lambda execution
# Fail on error
set -e
# Outer main loop for execution.
# So if there's next event it will get execute it immediately
while true
do
# All errors to STDOUT so it can be captured to CloudWatch
/opt/bin/php "$_HANDLER" 2>&1
done
<script type="text/template"><?php
namespace App;
use App\Handler;
class LambdaRuntime
{
/**
* Create object from static calls
*
* @return App\LambdaRuntime
*/
public static function create()
{
return new static();
}
/**
* Simple abstraction for HTTP GET Lambda invocation
*
* @param String $url
* @return Array
*/
public function fetch($url)
{
$headers = [
'Content-Type: application/json',
'User-Agent: BelajarAWS/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
*/
public function post($url, $data)
{
$headers = [
'Content-Type: application/json',
'User-Agent: BelajarAWS/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
*/
public function main()
{
// AWS_LAMBDA_RUNTIME_API are automatically set by Lambda
$lambdaRuntimeApi = getenv('AWS_LAMBDA_RUNTIME_API');
$lambdaBaseUrl = sprintf('http://%s/2018-06-01/runtime/invocation/', $lambdaRuntimeApi);
$maxLoop = getenv('MAX_LOOP') ?: 10;
$currentLoop = 0;
$handler = Handler::create();
// Inner main loop for execution.
// So if there's next event it will get execute it immediately
while (true)
{
if (++$currentLoop > $maxLoop) {
break;
}
$nextEvent = $this->fetch($lambdaBaseUrl . 'next');
$handlerResponse = $handler->handle($nextEvent['body']);
$responseUrl = sprintf('%s%s/response', $lambdaBaseUrl, $nextEvent['request_id']);
$lambdaResponse = $this->post($responseUrl, $handlerResponse);
}
}
}</script>
<script type="text/php"><?php
/**
* Lambda function to handle notification from S3 event and then
* send the info via email (SMTP).
*/
namespace App;
use Tx\Mailer;
class Handler
{
/**
* @var array $config - Configuration for the Handler such as
* region, endpoint, etc.
*/
public $config = [];
/**
* Constructor
*
* @param array $config
* @return void
*/
public function __construct(array $config = [])
{
$default = [
'region' => 'ap-southeast-1',
'version' => 'latest',
'email_addr' => getenv('AWSUG_EMAIL') ? getenv('AWSUG_EMAIL') : '[email protected]',
'smtp' => [
'host' => getenv('AWSUG_SMTP_HOST') ? getenv('AWSUG_SMTP_HOST') : '[email protected]',
'port' => getenv('AWSUG_SMTP_PORT') ? getenv('AWSUG_SMTP_PORT') : 587,
'username' => getenv('AWSUG_SMTP_USER') ? getenv('AWSUG_SMTP_USER') : null,
'password' => getenv('AWSUG_SMTP_PASSWD') ? getenv('AWSUG_SMTP_PASSWD') : null,
]
];
$this->config = array_replace_recursive($default, $config);
}
/**
* Create object from static calls
*
* @param array $config
* @return App\Handler
*/
public static function create(array $config = [])
{
return new static($config);
}
/**
* Handler function which return a response and will be send back to Lambda
*
* @param array $event Lambda Event
* @return String
*/
public function handle($event)
{
$eventTime = $event['Records'][0]['eventTime'];
$body = sprintf('<h3>Here is the the event info.</h3><pre>%s</pre>', json_encode($event, JSON_PRETTY_PRINT));
// Send email notification for new file
$mailer = (new Mailer())
->setServer($this->config['smtp']['host'], $this->config['smtp']['port'])
->setFrom('Rio Astamal', '[email protected]')
->addTo('AWS UG Surabaya', $this->config['email_addr'])
->setSubject( sprintf('New file has been uploaded at %s', $eventTime) )
->setBody($body)
->send();
$response = [
'email_status' => (bool)$mailer,
'requestId' => $event['Records'][0]['responseElements']['x-amz-request-id']
];
return json_encode($response);
}
}
</script>
$ aws s3 cp /some/file.ext s3://[BUCKET_NAME]/