Thanks to visit codestin.com
Credit goes to www.sambaiz.net

Aurora PostgreSQL のトリガーを Lambda でハンドリングする

databasepostgresqlaws

PostgreSQL は MySQL などと同様に BEFORE/AFTER トリガーをサポートしており SQL や PL/Python がインストールされていれば Python を実行できる。また、Redisのような Pub/Sub機能(NOTIFY/LISTEN)があり、トリガーと組み合わせることでデータ変更をリアルタイムで通知できる。

CREATE FUNCTION notify_change() RETURNS TRIGGER AS $$
  BEGIN
    PERFORM pg_notify('data_changes', json_build_object('table', TG_TABLE_NAME, 'id', NEW.id)::text);
    RETURN NEW;
  END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_notify 
  AFTER INSERT ON users 
  FOR EACH ROW 
  EXECUTE FUNCTION notify_change();

LISTEN data_changes; -- from another session

INSERT INTO users (name) VALUES ('Alice');
-- Notified: {"table": "users", "id": 1}

これにより Aurora のような PL/Python をインストールできない DB でもプログラマティックにトリガーをハンドリングできる。

$ PGPASSWORD='*****' psql -h *****.cluster-*****.ap-northeast-1.rds.amazonaws.com -U postgres -d testdb -c "SELECT * FROM pg_extension;"
  oid  | extname | extowner | extnamespace | extrelocatable | extversion | extconfig | extcondition
-------+---------+----------+--------------+----------------+------------+-----------+--------------
 14501 | plpgsql |       10 |           11 | f              | 1.0        |           |
(1 row)

$ cat listen.py
import psycopg

conn = psycopg.connect(
  host="*****.cluster-*****.ap-northeast-1.rds.amazonaws.com",
  dbname="testdb",
  user="postgres",
  password="*****",
  autocommit=True
)

conn.execute("LISTEN data_changes;")

try:
  for notify in conn.notifies():
    print(f"Received: {notify.payload}")
except KeyboardInterrupt:
  print("\nStopped listening")
finally:
  conn.close()

ただこの方法で変更を受け取ると常に通知を待ち構えている必要があり接続が切れると取り漏らすことになる。

Aurora には aws_lambda extension があって、これをインストールすると Lambda を呼び出すことができる。

CREATE IF NOT EXISTS EXTENSION aws_lambda CASCADE;

invoke するための権限を付与する。

const auroraLambdaRole = new iam.Role(this, 'AuroraLambdaRole', {
  assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'),
});

testFunction.grantInvoke(auroraLambdaRole);

const cfnDbCluster = dbCluster.node.defaultChild as rds.CfnDBCluster;
cfnDbCluster.associatedRoles = [{
  roleArn: auroraLambdaRole.roleArn,
  featureName: 'Lambda',
}];

あとはトリガーで aws_lambda.invoke() を呼べば常に待ち構えて接続を張りっぱなしにする必要がなくなる。

\x
SELECT * FROM aws_lambda.invoke(
  aws_commons.create_lambda_function_arn('(function_name)', 'ap-northeast-1'),
  '{"message": "Hello from Aurora"}'::json
  -- , 'Event' /* async */ 
);

-[ RECORD 1 ]----+----------------------------------------------------------------------------------------------------------------------
status_code      | 200
payload          | {"statusCode": 200, "body": "{\"message\": \"Hello from Lambda!\", \"event\": {\"message\": \"Hello from Aurora\"}}"}
executed_version | $LATEST
log_result       |