search
LoginSignup
0

posted at

updated at

SAMを使ってAPI-Gateway経由でAWS::Serverless::Functionから画像をダウンロードする

はじめに

AWSのSAMでASP.NET Core WebApiプロジェクトを作りPDFなどをダウンロードする場合、APIGatewayに設定を行わないとBase64でエンコードされたファイルがダウンロードされてしまいます。
これ自体はAPI Gateway REST API を使用したバイナリサポートの有効化の記載に従い、マネジメントコンソールで操作をすれば解決はできるのですが、SAMのテンプレートでこれを指定すにはどうすればよいでしょうか?

前準備

AWS関連のプロジェクトテンプレートと、Lambda関連のデプロイサブコマンドをインストールしていない場合はインストールしておいてください。

❯ dotnet new --install Amazon.Lambda.Templates
❯ dotnet tool install -g Amazon.Lambda.Tools

Amazon.Lambda.AspNetCoreServer

SAMのプロジェクトテンプレートにはいくつかの種類がありますが、まずは以前からあったserverless.AspNetCoreWebAPIで確認していきましょう。

プロジェクトの作成とデプロイ

プロジェクトを作成します。

> dotnet new serverless.AspNetCoreWebAPI -o webapi

コントローラーを追加します。とりあえず今回は一般的なPNGと比較的新しいWebP、あと確認のために通常のテキストファイルをダウンロードできるようにしておきましょう。

AssetsController.cs
using Microsoft.AspNetCore.Mvc;

namespace ImageDownloadWebApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AssetsController : ControllerBase
{
    [HttpGet("png")]
    public IActionResult Png() => File(System.IO.File.ReadAllBytes("image.png"), "image/png", "image.png");
    [HttpGet("webp")]
    public IActionResult WebP() => File(System.IO.File.ReadAllBytes("image.webp"), "image/webp", "image.webp");
    [HttpGet("text")]
    public IActionResult text() => File(System.Text.Encoding.UTF8.GetBytes("1234"), "text/plain", "data.txt");
}

AWSにデプロイします。

❯ dotnet lambda deploy-serverless --s3-bucket [アプリをアップロードするS3のバケット名] --stack-name image-download-api

バイナリーデータのダウンロード

デプロイしたURLからそれぞれのアセットをダウンロードします。

curl -X GET https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/api/assets/text \
  -H 'accept: text/plain' -o text.txt
curl -X GET https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/api/assets/png \
  -H 'accept: image/png' -o image.png
curl -X GET https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/api/assets/webp \
  -H 'accept: image/webp' -o image.webp

テキストは普通に表示されますが、
image.png
PNGは次のように認識されないファイルとしてダウンロードされました。
image.png
テキストエディターでimage.pngを開くとBase64っぽい形式でダウンロードされているようです。
image.png
WebPはよくわからないバイナリーデータですね。

ApiGatewayの設定と再デプロイ

下記に記載がある通りAPIGateway RESTAPIではUTF8のテキストデータを想定しているため、バイナリーデータは個別の許可が必要です。

ということでimage/pngimage/webpを追加します。
image.png
APIGatewayの設定を変更したら忘れずにデプロイしましょう。
image.png

バイナリーデータ → Base64 → バイナリーデータの変換

image.pngは表示できるようになりましたが、image.webpはいまだに表示できないままです。これは前述のとおりAPIGatewayのバックエンドではバイナリーデータはBase64で返却しなければならないというAPIGatewayの制限から来るものです。
プログラム側でBase64形式で画像を返してもよいのですが、LambdaEntryPointでRegisterResponseContentEncodingForContentTypeを使ってコンテンツタイプを登録することで自動的にバイナリデータ→Base64の変換を受け持ってくれるのでこれを利用しましょう。

public class LambdaEntryPoint: Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
{
    protected override void Init(IWebHostBuilder builder)
    {
        RegisterResponseContentEncodingForContentType("image/webp", ResponseContentEncoding.Base64);
        builder
            .UseStartup<Startup>();
    }
    protected override void Init(IHostBuilder builder)
    {
    }
}

application/octet-streamimage/pngなど代表的なファイルはあらかじめ登録されているようです。

WebPもダウンロードできるようになりましたね。

確認が終わったらスタックも削除しておきましょう

❯ dotnet lambda delete-serverless --stack-name image-download-api

Amazon.Lambda.AspNetCoreServer.Hosting

.NET 6のMinimalAPIに対応する場合も基本的にはASP.NET側とAPIGateway側に未知のコンテンツタイプを登録していけばよいです。
ただし、MinimalAPIの場合はLambdaEntryPointが無いので、LambdaRuntimeSupportServerを拡張してカスタムのホスティング環境を作成する必要があります。

プロジェクトを作成してカスタムホスティング環境を適用する。

プロジェクトを作り、AspNetCoreWebAPIの時と同じコントローラーを追加します。

❯ dotnet new serverless.AspNetCoreMinimalAPI -o MinimalApi
AssetsController.cs
using Microsoft.AspNetCore.Mvc;

namespace ImageDownloadWebApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AssetsController : ControllerBase
{
    [HttpGet("png")]
    public IActionResult Png() => File(System.IO.File.ReadAllBytes("image.png"), "image/png", "image.png");
    [HttpGet("webp")]
    public IActionResult WebP() => File(System.IO.File.ReadAllBytes("image.webp"), "image/webp", "image.webp");
    [HttpGet("text")]
    public IActionResult text() => File(System.Text.Encoding.UTF8.GetBytes("1234"), "text/plain", "data.txt");
}

image/webpを登録したカスタムのランタイムサーバーと、ホスティング用の拡張メソッドを定義します。

using Amazon.Lambda.AspNetCoreServer;
using Amazon.Lambda.AspNetCoreServer.Hosting.Internal;
using Amazon.Lambda.AspNetCoreServer.Internal;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;

public class ImageSupportAPIGatewayRestApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer
{
    public ImageSupportAPIGatewayRestApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider)
        : base(serviceProvider)
    {
    }

    protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider)
    {
        var handler = new APIGatewayRestApiMinimalApi(serviceProvider).FunctionHandlerAsync;
        return HandlerWrapper.GetHandlerWrapper(handler, new DefaultLambdaJsonSerializer());

    }

    public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction
    {
        public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider)
        {
            RegisterResponseContentEncodingForContentType("image/webp", ResponseContentEncoding.Base64);
        }
    }
}

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddAWSLambdaHostingCustomized(this IServiceCollection services, LambdaEventSource eventSource)
    {
        if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME")))
            return services;
        var type = eventSource switch
        {
            LambdaEventSource.RestApi => typeof(ImageSupportAPIGatewayRestApiLambdaRuntimeSupportServer),
            _ => throw new NotSupportedException()
        };
        Utilities.EnsureLambdaServerRegistered(services, type);
    }
}

デフォルトのホスティング設定をコメントにして、builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);上で作成したランタイムでホストします。

Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSwaggerDocument();

// Add services to the container.
builder.Services.AddControllers();

// Add AWS Lambda support. When application is run in Lambda Kestrel is swapped out as the web server with Amazon.Lambda.AspNetCoreServer. This
// package will act as the webserver translating request and responses between the Lambda event source and ASP.NET Core.
//builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
builder.Services.AddAWSLambdaHostingCustomized(LambdaEventSource.RestApi);

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.UseOpenApi();
app.UseSwaggerUi3();

app.MapGet("/", () => "Welcome to running ASP.NET Core Minimal API on AWS Lambda");

app.Run();

SAMのデプロイ後に、image/pngimage/webpを許可するApiGatewayの設定をすれば、同様に利用できるようになります。

Amazon.Lambda.AspNetCoreServer.Hosting + HTTP API

ここまではWebGateway のREST APIを利用してきましたが、HTTP APIを利用するとAPI Gateway側のコンテンツタイプ設定を行わなずに済みます。MinimalApiを利用する場合は次のようにHttpApi用のカスタムランタイムを作成し、

public class ImageSupportAPIGatewayHttpApiV2LambdaRuntimeSupportServer : LambdaRuntimeSupportServer
{
    public ImageSupportAPIGatewayHttpApiV2LambdaRuntimeSupportServer(IServiceProvider serviceProvider)
        : base(serviceProvider)
    {
    }

    protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider)
    {
        var handler = new APIGatewayHttpApiMinimalApi(serviceProvider).FunctionHandlerAsync;
        return HandlerWrapper.GetHandlerWrapper(handler, new DefaultLambdaJsonSerializer());

    }

    public class APIGatewayHttpApiMinimalApi : APIGatewayHttpApiV2ProxyFunction
    {
        public APIGatewayHttpApiMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider)
        {
            RegisterResponseContentEncodingForContentType("image/webp", ResponseContentEncoding.Base64);
        }
    }
}

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddAWSLambdaHostingCustomized(this IServiceCollection services, LambdaEventSource eventSource)
    {
        // Not running in Lambda so exit and let Kestrel be the web server
        if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME")))
            return services;

        var type = eventSource switch
        {
            LambdaEventSource.RestApi => typeof(ImageSupportAPIGatewayRestApiLambdaRuntimeSupportServer),
            LambdaEventSource.HttpApi => typeof(ImageSupportAPIGatewayHttpApiV2LambdaRuntimeSupportServer),
            _ => throw new NotSupportedException()
        };

        Utilities.EnsureLambdaServerRegistered(services, type);
        return services;
    }
}

ホスティング設定時にLambdaEventSource.RestApiではなくLambdaEventSource.HttpApiを利用するように修正します。

builder.Services.AddAWSLambdaHostingCustomized(LambdaEventSource.RestApi);

合わせて、serverless.templateを次のように書き換えてデプロイします。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "An AWS Serverless Application that uses the ASP.NET Core framework running in Amazon Lambda.",
  "Parameters": {},
  "Conditions": {},
  "Resources": {
    "AspNetCoreFunction": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "MinimalApi",
        "Runtime": "dotnet6",
        "CodeUri": "",
        "MemorySize": 256,
        "Timeout": 30,
        "Role": null,
        "Policies": [
          "AWSLambda_FullAccess"
        ],
        "Events": {
          "ProxyResource": {
-            "Type": "Api",
+            "Type": "HttpApi",
            "Properties": {
              "Path": "/{proxy+}",
              "Method": "ANY"
            }
          },
          "RootResource": {
-            "Type": "Api",
+            "Type": "HttpApi",
            "Properties": {
              "Path": "/",
              "Method": "ANY"
            }
          }
        }
      }
    }
  },
  "Outputs": {
    "ApiURL": {
      "Description": "API endpoint URL for Prod environment",
      "Value": {
-        "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
+        "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
      }
    }
  }
}

まとめ

APIGateway経由でバイナリーデータをダウンロードする場合、アプリ側とAPIGateway側の両方の設定が必要です。
ただし、APIGateway側の設定はHTTP APIを利用するのであればserverless.templateでの設定だけで完結することができます。

おわりに

ASP.NETの進化とAWS側のサービスの増加に伴い、ホスティング方法が多岐にわたってきてマニュアルをざっと見ただけだとこの場合どうやってやるの?を理解するのが大変になってきましたね。。。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0