Dotnet 6 Web API в Yandex Cloud Serverless Container
Для начала создадим проект на основе примера Web API.
Базовое приложение
Отлично. Теперь пройдемся по изменениям, которые нужно внести в проект, чтобы он «взлетел» в Serverless Containers.
Как я писал в посте про запуск контейнера по триггеру
на необходимо реализовать интерфейс HTTP сервера. К счастью, dotnet core Web API именно его и реализует
и нам остается только добавить несколько настроек, чтобы выполнить главное условие — запускать сервер
на порту, переданном приложение через переменную окружения PORT
. Для этого в Program.cs
добавим следующее:
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
builder.WebHost.UseKestrel(
serverOptions =>
{
serverOptions.ListenAnyIP(int.Parse(port));
});
Теперь, нам нужно удалить еще одну настройку:
app.UseHttpsRedirection();
Она нам не понадобится так как вызов контейнера идет по HTTP, а в логах мы будем видеть следующие предупреждения, ведь порт для HTTPS мы не задаем:
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
Failed to determine the https port for redirect.
Еще я бы убрал из Dockerfile
строки EXPOSE, чтобы не вводить в заблуждение, ведь наше приложение больше не использует эти порты.
EXPOSE 80
EXPOSE 443
Также можно почистить %project%/Properties/launchSettings.json
от лишних параметров.
Теперь можно развернуть приложение и убедиться, что все работает. Я репозитории с примером буду делать это при помощи
Terraform CDK. Работа с ним выходит за рамки этого туториала, но у меня есть отдельная
статья про TF CDK. И так я перейду в папку cdk
и выполню следующую команду.
TF_VAR_folderId=b1g*** TF_VAR_token=`yc iam create-token` \
cdktf deploy --output ../cdktf.out
В результате будет собран docker image, запушен в облачный репозиторий и из него будет развернут serverless контейнер.
Авторизация
Если вы попробуете добавить авторизацию в ваше приложение развернутое в YC Serverless Containers вас ждет неприятный сюрприз: как сказано в документации загол овки Authorization
и Cookie
из запроса вырезаются, как и некоторые другие.
Что же делать? Ставить контейнер за API Gateway. Кроме возможности использовать авторизацию нормально, он также, например, позволит использовать свой собственный домен.
API Gateway
Для того чтобы создать API GW, нам понадобится OpenAPI спецификация описывающая наше API. Её можно написать руками, а можно сгенерировать на основе кода. Тем более в базовом примере и так уже используется пакет Swashbuckle для генерации спеки для Swagger. Установим CLI которую, будем вызывать при билде.
dotnet tool install SwashBuckle.AspNetCore.Cli --global
Теперь мы можем добавить в csproj команду для вызова этой утилиты.
<Target Name="OpenAPI" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Exec Command="swagger tofile --output ./swagger.yaml --yaml $(OutputPath)$(AssemblyName).dll v1" WorkingDirectory="$(ProjectDir)" />
</Target>
В итоге мы получим swagger.yaml
где будет OpenAPI 3.0 документ, описывающий наше API. Чтобы использовать его в API Gateway нам понадобится лишь одна доработка: нам нужно разметить объекты операций расширением спецификации x-yc-apigateway-integration
, которе скажет гейтвею, что для этого запроса нужно вызвать контейнер.
Для этого нам понадобится написать класс имплементирующий интерфейс IOperationFilter
. Его метод Apply
будет вызываться для каждого объекта операций в OpenAPI спецификации и в нем мы сможем модифицировать операцию как захотим.
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ServerlessContainerDemo.OpenAPI.Filters;
public class YcExtensionsFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Extensions.Add("x-yc-apigateway-integration", new OpenApiObject()
{
["type"] = new OpenApiString("serverless_containers"),
["container_id"] = new OpenApiString("${container_id}"),
["service_account_id"] = new OpenApiString("${container_service_account_id}"),
});
}
}
Значения ${container_id}
и ${container_service_account_id}
мы подставим в шаблонизаторе в Terraform.
Теперь зарегистрируем фильтр в Program.cs
.
builder.Services.AddSwaggerGen(c =>
{
c.EnableAnnotations();
c.SwaggerDoc("v1", new OpenApiInfo() {Title = "Serverless Container Demo", Version = "v1"});
c.OperationFilter<YcExtensionsFilter>();
});
Добавляем авторизацию
Этот хэндлер приведен только для демонстрационных целей.
namespace ServerlessContainerDemo.Auth;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock
) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeader = Request.Headers["Authorization"].ToString();
if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Basic ".Length).Trim();
System.Console.WriteLine(token);
var credentialString = Encoding.UTF8.GetString(Convert.FromBase64String(token));
var credentials = credentialString.Split(':');
if (credentials[0] == "admin" && credentials[1] == "admin")
{
var claims = new[] {new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin")};
var identity = new ClaimsIdentity(claims, "Basic");
var claimsPrincipal = new ClaimsPrincipal(identity);
return Task.FromResult(
AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name)));
}
Response.StatusCode = 401;
Response.Headers.Add("WWW-Authenticate", "Basic");
return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
}
else
{
Response.StatusCode = 401;
Response.Headers.Add("WWW-Authenticate", "Basic");
return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
}
}
}
Теперь добавим опции в конфигурацию приложения.
builder.Services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>
("BasicAuthentication", null);
builder.Services.AddAuthorization(options =>
{
var basicAuthenticationSchemePolicyBuilder = new AuthorizationPolicyBuilder("BasicAuthentication");
options.AddPolicy("BasicAuthentication", basicAuthenticationSchemePolicyBuilder
.RequireClaim(ClaimTypes.Role, "Admin")
.Build());
});
И после этого мы можем использовать аннотацию Authorize на хэндлерах нашего контроллера:
[Authorize(Policy = "BasicAuthentication")]
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()