Amazon Web Services ブログ

AWS Lambdaによる進化的アーキテクチャの構築

この投稿は、メディアとエンターテインメントのプリンシパルソリューションアーキテクトであるLuca Mezzaliraによって書かれました。

俊敏性により、必要に応じてワークロードを迅速に進化させ、新機能を追加したり、新しいインフラストラクチャを導入したりできます。コードベースでアジリティを実現するための主な特徴は、疎結合コンポーネントと強力なカプセル化です。

疎結合は、テストカバレッジを改善し、一貫したリファクタリングを作成するのに役立ちます。カプセル化を使用すると、実装ロジックを明らかにすることなく、サービスとのやり取りに必要なものだけを公開できます。

進化的なアーキテクチャは、設計の俊敏性を実現するのに役立ちます。「進化的アーキテクチャの構築」という本の中で、このアーキテクチャは「複数の次元にわたるガイド付きの段階的な変更をサポートする」アーキテクチャとして定義されています。

このブログ投稿では、AWS Lambda 関数のコードをモジュール方式で構造化する方法に焦点を当てています。ヘキサゴナルアーキテクチャパターンによって提供される進化的側面を取り入れ、さまざまなユースケースに適用する方法を示します。

ポートとアダプタの紹介

ヘキサゴナルアーキテクチャは、ポートおよびアダプタアーキテクチャとも呼ばれます。これは、ドメインロジックをカプセル化し、インフラストラクチャやクライアント要求などの他の実装の詳細から切り離すために使用されるアーキテクチャパターンです。

  1. ドメインロジック:アプリケーションが実行すべきタスクを表し、外部とのやりとりを抽象化します。
  2. ポート: メインアクター (左側) がドメインロジックを介してアプリケーションと対話する方法を提供します。ドメインロジックでは、必要に応じてセカンダリアクター (右側) と対話するためにポートも使用します。
  3. アダプター:あるインターフェースを別のインターフェースに変換するためのデザインパターン。プライマリまたはセカンダリのアクターと対話するためのロジックをラップします。
  4. プライマリアクター:Webhook、UI リクエスト、テストスクリプトなどのシステムのユーザー。
  5. セカンダリアクター:アプリケーションによって使用されるこれらのサービスは、リポジトリ (データベースなど) または受信者 (メッセージキューなど) のいずれかです。

Lambda関数を使用したヘキサゴナルアーキテクチャ

Lambda 関数は、特定のタスクを実行する計算ロジックの単位です。たとえば、Amazon Kinesis Streamのデータを操作したり、Amazon SQS Queueからのメッセージを処理したりすることができます。

Lambda 関数では、ヘキサゴナルアーキテクチャによって新しいビジネス要件を実装し、ワークロードの俊敏性を向上させることができます。このアプローチは、関心を分離し、ドメインロジックをインフラストラクチャから分離するのに役立ちます。開発チームにとっては、新機能の実装を簡素化し、異なる開発者間で作業を並列化することもできます。

次の例では、株価を返すサービスを紹介します。このサービスは、ダッシュボードに情報を表示するフロントエンドアプリケーションに対して、さまざまな通貨をサポートしています。通貨間の株価の換算はリアルタイムで行われます。サービスは、クライアントからのリクエストごとに為替レートを取得する必要があります。

このサービスのアーキテクチャでは、REST API を公開する Amazon API Gateway エンドポイントを使用します。クライアントが API を呼び出すと、Lambda 関数がトリガーされます。DynamoDB テーブルから株価を取得し、サードパーティのエンドポイントから通貨情報を取得します。ドメインロジックは、クライアントのリクエストに応答する前に、為替レートを使用して株価を他の通貨に換算します。

完全な例は AWS GitHub サンプルリポジトリで入手できます。このサービスのアーキテクチャは次のとおりです。

  1. クライアントが API Gateway エンドポイントにリクエストを行い、Lambda 関数を呼び出します。
  2. プライマリアダプターがリクエストを受信します。ストック ID をキャプチャし、ポートに渡します。
exports.lambdaHandler = async (event) => {
    try{
	// retrieve the stockID from the request
        const stockID = event.pathParameters.StockID;
	// pass the stockID to the port
        const response = await getStocksRequest(stockID);
        return response
    } 
};
  1. ポートは、ドメインロジックと通信するためのインターフェイスです。アダプターとドメインロジックの分離を強制します。このアプローチにより、コードベースの他の部分に影響を与えることなく、インフラストラクチャとドメインロジックを単独で変更およびテストできます。
const retrieveStock = async (stockID) => {
    try{
	//use the port “stock” to access the domain logic
        const stockWithCurrencies = await stock.retrieveStockValues(stockID)
        return stockWithCurrencies;
    }
}
  1. ストック ID を渡すポートは、ドメインロジックのエントリポイントを呼び出します。ドメインロジックは DynamoDB テーブルから株価を取得し、為替レートをリクエストします。計算された値をポート経由でプライマリアダプタに返します。ドメインロジックは、ポートが外部世界とのインターフェースであるため、アダプターと対話するために常にポートを使用します。
const CURRENCIES = [“USD”, “CAD”, “AUD”]
const retrieveStockValues = async (stockID) => {
try {
//retrieve the stock value from DynamoDB using a port
        const stockValue = await Repository.getStockData(stockID);
//fetch the currencies value using a port
        const currencyList = await Currency.getCurrenciesData(CURRENCIES);
//calculate the stock value in different currencies
        const stockWithCurrencies = {
            stock: stockValue.STOCK_ID,
            values: {
                "EUR": stockValue.VALUE
            }
        };
        for(const currency in currencyList.rates){
            stockWithCurrencies.values[currency] =  (stockValue.VALUE * currencyList.rates[currency]).toFixed(2)
        }
// return the final computation to the port
        return stockWithCurrencies;
    }
}

ドメインロジックが DynamoDB テーブルとやり取りする方法は次のとおりです。

  1. ドメインロジックでは、データベースとの対話にリポジトリポートを使用します。ドメインとアダプタの間には直接接続がありません。
const getStockData = async (stockID) => {
    try{
//the domain logic pass the request to fetch the stock ID value to this port
        const data = await getStockValue(stockID);
        return data.Item;
    } 
}
  1. セカンダリアダプターは、DynamoDB テーブルから項目を読み取るロジックをカプセル化します。DynamoDB と対話するためのロジックはすべてこのモジュールにカプセル化されています。
const getStockValue = async (stockID) => {
    let params = {
        TableName : DB_TABLE,
        Key:{
            'STOCK_ID': stockID
        }
    }
    try {
        const stockData = await documentClient.get(params).promise()
        return stockData
    }
}

ドメインロジックでは、サードパーティサービスから為替レートを取得するためにアダプタを使用します。その後、データを処理し、クライアントのリクエストに応答します。

  1. ビジネスロジックの 2 番目の操作は、為替レートの取得です。ドメインロジックは、アダプタにリクエストをプロキシするポートを介してオペレーションをリクエストします。
const getCurrenciesData = async (currencies) => {
    try{
        const data = await getCurrencies(currencies);
        return data
    } 
}
  1. 通貨サービスアダプタは、サードパーティエンドポイントからデータを取得し、その結果をドメインロジックに返します。
const getCurrencies = async (currencies) => {
    try{        
        const res = await axios.get(`http://api.mycurrency.io?symbols=${currencies.toString()}`)
        return res.data
    } 
}

これら 8 つのステップは、ヘキサゴナルアーキテクチャを使用して Lambda 関数コードを構造化する方法を示しています。

キャッシュレイヤを追加する

このシナリオでは、本番在庫サービスでは日中にトラフィックが急増します。為替レートの外部エンドポイントはトラフィックレベルをサポートできません。この問題に対処するために、Redis クラスターを使用して Amazon ElastiCache でキャッシュ戦略を実装できます。このアプローチでは、トラフィックを外部サービスにオフロードするためにキャッシュアサイドパターンを使用します。

通常、コードベースで懸念事項を分離することなく、この変更を実装するためにコードを進化させるのは難しい場合があります。ただし、この例では、外部サービスと対話するアダプタがあります。したがって、実装を変更してキャッシュアサイドパターンを追加し、アプリケーションの他の部分と同じ API コントラクトを維持することができます。

const getCurrencies = async (currencies) => {
    try{        
// Check the exchange rates are available in the Redis cluster
        let res = await asyncClient.get("CURRENCIES");
        if(res){
// If present, return the value retrieved from Redis
            return JSON.parse(res);
        }
// Otherwise, fetch the data from the external service
        const getCurr = await axios.get(`http://api.mycurrency.io?symbols=${currencies.toString()}`)
// Store the new values in the Redis cluster with an expired time of 20 seconds
        await asyncClient.set("CURRENCIES", JSON.stringify(getCurr.data), "ex", 20);
// Return the data to the port
        return getCurr.data
    } 
}

これは、アダプタにのみ影響する労力の少ない変更です。アダプターとやり取りするドメインロジックとポートは変更されておらず、同じ API コントラクトを維持します。このアーキテクチャによって提供されるカプセル化は、コードベースを進化させるのに役立ちます。また、アダプタのみが変更されたことを考慮すると、多くのテストが保持されます。

ドメインロジックをコンテナから Lambda 関数に移動

この例では、このワークロードに取り組んでいるチームは、Amazon ECSAWS Fargate を使用して、すべての機能をコンテナ内にラップしています。この場合、開発者は株価を取得するための GET メソッドのルートを定義します。

// This web application uses the Fastify framework 
  fastify.get('/stock/:StockID', async (request, reply) => {
    try{
        const stockID = request.params.StockID;
        const response = await getStocksRequest(stockID);
        return response
    } 
})

この場合、ルートのエントリポイントは Lambda 関数とまったく同じです。ヘキサゴナルアーキテクチャによって提供される特性のおかげで、チームはコードベースで他に何も変更する必要はありません。

このパターンを使用すると、コンテナや仮想マシンから複数の Lambda 関数にコードを簡単にリファクタリングできます。他のソリューションではより困難になる可能性のあるコードの移植性が高まっています。

メリットとデメリット

他のパターンと同様に、ヘキサゴナルアーキテクチャを使用することには利点と欠点があります。

主なメリットは次のとおりです。

  • ドメインロジックは不可知論であり、外界から独立しています。
  • 関心を分離することで、コードのテスト容易性が向上します。
  • ワークロードにおける技術的負債の削減に役立つ可能性があります。

欠点は以下のとおりです。

  • このパターンには、時間の先行投資が必要です。
  • ドメインロジックの実装については深く触れていません。

このアーキテクチャを Lambda 関数の開発に使用すべきかどうかは、アプリケーションのニーズによって異なります。ワークロードが進化する中で、追加の実装作業は価値があるかもしれません。

このパターンは、カプセル化および関心の分離により、コードのテスト容易性の向上に役立ちます。このアプローチは Lambda 以外のコンピューティングソリューションでも使用でき、コード移行プロジェクトで役立つ場合があります。

結論

この記事では、ヘキサゴナルアーキテクチャを使用してワークロードを進化させる方法を説明しました。新しい機能を追加したり、基盤となるインフラストラクチャを変更したり、異なるコンピューティングソリューション間でコードベースを移植したりする方法について説明しました。これを可能にする主な特徴は、疎結合と強力なカプセル化です。

ヘキサゴナルアーキテクチャおよび類似パターンの詳細については、以下をお読みください。

サーバーレスラーニングリソースの詳細については、Serverless Land をご覧ください。

この記事の翻訳はサーバーレススペシャリストSAの福井 厚が担当しました。原文はこちらです。