Skip to content

Instantly share code, notes, and snippets.

@guitarrapc
Last active November 9, 2020 07:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guitarrapc/0626a43971274ea5b49d2ee9c7529252 to your computer and use it in GitHub Desktop.
Save guitarrapc/0626a43971274ea5b49d2ee9c7529252 to your computer and use it in GitHub Desktop.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://0.0.0.0:80"
}
}
},
"AllowedHosts": "*"
}
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\aws.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.33.1" />
<PackageReference Include="Grpc.HealthCheck" Version="2.33.1" />
<PackageReference Include="MagicOnion.Abstractions" Version="4.0.0-preview.1" />
<PackageReference Include="MagicOnion.Server" Version="4.0.0-preview.1" />
<PackageReference Include="ZLogger" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EchoGrpcMagicOnion.Shared\EchoGrpcMagicOnion.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
using EchoGrpcMagicOnion.Shared;
using Grpc.Core;
using MagicOnion;
using MagicOnion.Server;
using MagicOnion.Server.Hubs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
using ZLogger;
namespace EchoGrpcMagicOnion
{
class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((hostContext, logging) =>
{
logging.ClearProviders();
logging.AddZLoggerConsole();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseKestrel(options =>
{
// WORKAROUND: Accept HTTP/2 only to allow insecure HTTP/2 connections during development.
options.ConfigureEndpointDefaults(endpointOptions =>
{
endpointOptions.Protocols = HttpProtocols.Http2;
});
})
.UseStartup<Startup>();
});
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(); // MagicOnion depends on ASP.NET Core gRPC service.
services.AddGrpcReflection();
services.AddMagicOnion(options =>
{
options.GlobalStreamingHubFilters.Add<StreamingHeaderFilterAttribute>();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapMagicOnionService();
endpoints.MapGrpcService<HealtchCheckService>();
//endpoints.MapGrpcService<AlbService>();
if (env.IsDevelopment())
{
endpoints.MapGrpcReflectionService();
}
});
}
}
/// <summary>
/// You don't need implement this service if status code 12 is enough.
/// `grpcurl -v -plaintext localhost:80 AWS.ALB/healthcheck`
/// </summary>
public class AlbService : ALB.ALBBase
{
private readonly ILogger<AlbService> _logger;
public AlbService(ILogger<AlbService> logger)
{
_logger = logger;
}
public override Task<Google.Protobuf.WellKnownTypes.Empty> healthcheck(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
_logger.LogInformation("healthy");
return Task.FromResult(new Google.Protobuf.WellKnownTypes.Empty());
}
}
/// <summary>
/// Implement https://github.com/grpc/grpc/tree/master/src/csharp/Grpc.HealthCheck
/// `grpcurl -plaintext 127.0.0.1:80 grpc.health.v1.Health.Check`
/// `grpc-health-probe -addr 127.0.0.1:80`
/// </summary>
public class HealtchCheckService : Grpc.Health.V1.Health.HealthBase
{
public override Task<Grpc.Health.V1.HealthCheckResponse> Check(Grpc.Health.V1.HealthCheckRequest request, ServerCallContext context)
{
return Task.FromResult(new Grpc.Health.V1.HealthCheckResponse
{
Status = Grpc.Health.V1.HealthCheckResponse.Types.ServingStatus.Serving,
});
}
}
public class HealthzService : ServiceBase<IHealthzService>, IHealthzService
{
private readonly ILogger<HealthzService> _logger;
public HealthzService(ILogger<HealthzService> logger)
{
_logger = logger;
}
public UnaryResult<int> Readiness()
{
return UnaryResult<int>(0);
}
}
[FromTypeFilter(typeof(UnaryHeaderFilterAttribute))]
public class EchoService : ServiceBase<IEchoService>, IEchoService
{
private readonly ILogger<EchoService> _logger;
public EchoService(ILogger<EchoService> logger)
{
_logger = logger;
}
public async UnaryResult<string> EchoAsync(string request)
{
_logger.LogDebug($"Handling Echo request '{request}' with context {Context}");
var hostName = Environment.MachineName;
var metadata = new Metadata();
metadata.Add("hostname", hostName);
await Context.CallContext.WriteResponseHeadersAsync(metadata);
return hostName;
}
}
public class MyHub : StreamingHubBase<IMyHub, IMyHubReceiver>, IMyHub
{
private Player player;
private IGroup room;
private IInMemoryStorage<Player> storage;
private readonly ILogger<MyHub> _logger;
public MyHub(ILogger<MyHub> logger)
{
_logger = logger;
}
public async Task<Player[]> JoinAsync(string roomName, string username)
{
_logger.LogDebug($"Handling Stream Join request '{roomName}/{username}' with context {Context}");
player = new Player
{
Name = username
};
(room, storage) = await Group.AddAsync(roomName, player);
Broadcast(room).OnJoin(player);
return storage.AllValues.ToArray();
}
public async Task LeaveAsync()
{
_logger.LogDebug($"Handling Stream Leave request '{room.GroupName}/{player.Name}' with context {Context}");
await room.RemoveAsync(this.Context);
Broadcast(room).OnLeave(player);
}
public Task<string> EchoAsync(string message)
{
_logger.LogDebug($"Handling Stream Echo request '{room.GroupName}/{player.Name}' with context {Context}");
var hostName = Environment.MachineName;
Broadcast(room).OnEcho(hostName);
return Task.FromResult(hostName);
}
protected override async ValueTask OnConnecting()
{
_logger.LogDebug($"OnConnecting {Context.ContextId}");
var metadata = new Metadata();
var headers = Context.CallContext.RequestHeaders;
if (!headers.Any(x => x.Key == "hostname"))
{
var hostName = Environment.MachineName;
headers.Add("hostname", hostName);
await Context.CallContext.WriteResponseHeadersAsync(metadata);
}
}
protected override ValueTask OnDisconnected()
{
_logger.LogDebug($"OnDisconnected {Context.ContextId}");
return CompletedTask;
}
}
public class StreamingHeaderFilterAttribute : StreamingHubFilterAttribute
{
private readonly ILogger<StreamingHeaderFilterAttribute> _logger;
public StreamingHeaderFilterAttribute(ILogger<StreamingHeaderFilterAttribute> logger)
{
_logger = logger;
}
public override async ValueTask Invoke(StreamingHubContext context, Func<StreamingHubContext, ValueTask> next)
{
_logger.LogInformation($"Request header contents of {context.Path}.");
_logger.LogInformation("{");
foreach (var header in context.ServiceContext.CallContext.RequestHeaders)
{
_logger.LogInformation($" {header.Key}: {header.Value}");
}
_logger.LogInformation("}");
await next(context);
}
}
public class UnaryHeaderFilterAttribute : MagicOnionFilterAttribute
{
private readonly ILogger<UnaryHeaderFilterAttribute> _logger;
public UnaryHeaderFilterAttribute(ILogger<UnaryHeaderFilterAttribute> logger)
{
_logger = logger;
}
public override async ValueTask Invoke(ServiceContext context, Func<ServiceContext, ValueTask> next)
{
_logger.LogInformation($"Request header contents of {context.CallContext.Method}.");
_logger.LogInformation("{");
foreach (var header in context.CallContext.RequestHeaders)
{
_logger.LogInformation($" {header.Key}: {header.Value}");
}
_logger.LogInformation("}");
await next(context);
}
}
}
MY_DOMAIN=dummy.example.com
NAMESPACE=envoy-grpc
ACM=arn:aws:acm:us-west-2:xxxxx:certificate/xxxxxxx
TARGET_GROUP=arn:aws:elasticloadbalancing:us-west-2:xxxxx:targetgroup/xxxxxxx/xxxxxxxxxx
kubectl kustomize ./k8s_grpc_magiconion |
sed -e "s|gcr.io/GOOGLE_CLOUD_PROJECT|guitarrapc|g" |
sed -e "s|<namespace>|$NAMESPACE|g" |
sed -e "s|\.default|.$NAMESPACE|g" |
sed -e "s|<acm>|$ACM|g" |
sed -e "s|<domain>|$MY_DOMAIN|g" |
sed -e "s|<targetgroup>|$TARGET_GROUP|g"
# check
grpc-health-probe.exe -tls -addr $MY_DOMAIN:443
apiVersion: v1
kind: Namespace
metadata:
name: <namespace>
---
apiVersion: v1
data:
envoy.yaml: |
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address:
address: 127.0.0.1
protocol: TCP
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
protocol: TCP
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.file_access_log
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/dev/stdout"
route_config:
name: local_route
virtual_hosts:
- name: http
domains:
- "*"
routes:
- match:
prefix: /
grpc: {}
route:
cluster: dynamic_forward_proxy_cluster
typed_per_filter_config:
envoy.filters.http.lua:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute
name: forwardproxy.lua
envoy.filters.http.dynamic_forward_proxy:
"@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.PerRouteConfig
host_rewrite_header: x-host-port
metadata:
filter_metadata:
# expected header format 'x-host-port: 10-0-0-10'. otherwise forward to default_route.
envoy.filters.http.lua:
svc: ".echo-grpc"
port: 80
header: x-host-port
default_route: "echo-grpc:80"
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
# dummy
function envoy_on_request(request_handle)
return
end
source_codes:
forwardproxy.lua:
inline_string: |
function envoy_on_request(request_handle)
local conf_svc = request_handle:metadata():get("svc")
local conf_port = request_handle:metadata():get("port")
local conf_header_key = request_handle:metadata():get("header")
local conf_default_route = request_handle:metadata():get("default_route")
-- handle header
local xHostPort = conf_default_route
local original = ""
value = request_handle:headers():get(conf_header_key)
if value ~= nil then
request_handle:logDebug(string.format("Header %s: %s", conf_header_key, value))
original = value
xHostPort = string.format("%s%s:%s", value, conf_svc, conf_port)
end
request_handle:headers():replace(conf_header_key, xHostPort)
-- check protocol
protocol = request_handle:streamInfo():protocol()
request_handle:logInfo(string.format("%s", protocol))
-- debug log
request_handle:logInfo(string.format("%s, original=%s, after=%s", conf_header_key, original, xHostPort, changed))
end
- name: envoy.filters.http.dynamic_forward_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig
dns_cache_config:
name: dynamic_forward_proxy_cache_config
dns_lookup_family: V4_ONLY
- name: envoy.filters.http.router
clusters:
- name: dynamic_forward_proxy_cluster
connect_timeout: 1s
lb_policy: CLUSTER_PROVIDED
http2_protocol_options: {}
ignore_health_on_host_removal: true
cluster_type:
name: envoy.clusters.dynamic_forward_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig
dns_cache_config:
name: dynamic_forward_proxy_cache_config
dns_lookup_family: V4_ONLY
kind: ConfigMap
metadata:
name: envoy-conf-579bdt5554
namespace: <namespace>
---
apiVersion: v1
kind: Service
metadata:
name: echo-grpc
namespace: <namespace>
spec:
clusterIP: None
ports:
- name: http2-service
port: 80
protocol: TCP
selector:
app: echo-grpc
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: envoy-svc
namespace: <namespace>
spec:
ports:
- name: http
port: 10000
protocol: TCP
targetPort: http
selector:
app: envoy
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-grpc
namespace: <namespace>
spec:
replicas: 3
selector:
matchLabels:
app: echo-grpc
template:
metadata:
labels:
app: echo-grpc
spec:
containers:
- image: guitarrapc/echo-magiconion:4.0.0-preview-1
imagePullPolicy: Always
name: echo-grpc
ports:
- containerPort: 80
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=127.0.0.1:80
initialDelaySeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: envoy
namespace: <namespace>
spec:
replicas: 2
selector:
matchLabels:
app: envoy
template:
metadata:
labels:
app: envoy
spec:
containers:
- image: envoyproxy/envoy-alpine:v1.16.0
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- wget -qO- --post-data='' http://127.0.0.1:8001/healthcheck/fail &&
sleep 10
livenessProbe:
initialDelaySeconds: 15
periodSeconds: 20
tcpSocket:
port: 10000
name: envoy
ports:
- containerPort: 10000
name: http
readinessProbe:
initialDelaySeconds: 3
periodSeconds: 10
tcpSocket:
port: 10000
volumeMounts:
- mountPath: /etc/envoy
name: config
volumes:
- configMap:
name: envoy-conf-579bdt5554
name: config
---
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
name: envoy-grpc
namespace: <namespace>
spec:
networking:
ingress:
- from:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- port: 10000
protocol: TCP
serviceRef:
name: envoy-svc
port: 10000
targetGroupARN: <targetgroup>
targetType: ip
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/actions.forward-single-tg: |
{"type":"forward","targetGroupARN": "<targetgroup>","serviceName":"envoy-svc","servicePort":10000}
alb.ingress.kubernetes.io/certificate-arn: <acm>
alb.ingress.kubernetes.io/load-balancer-attributes: routing.http2.enabled=true,
idle_timeout.timeout_seconds=60
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30
alb.ingress.kubernetes.io/target-type: ip
external-dns.alpha.kubernetes.io/hostname: <domain>
kubernetes.io/ingress.class: alb
name: envoy-ingress
namespace: <namespace>
spec:
rules:
- http:
paths:
- backend:
serviceName: forward-single-tg
servicePort: use-annotation
path: /*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment