import * as appmesh from '@aws-cdk/aws-appmesh' ; import * as ec2 from '@aws-cdk/aws-ec2' ; import * as ecr from '@aws-cdk/aws-ecr' ; import * as ecs from '@aws-cdk/aws-ecs' ; import * as iam from '@aws-cdk/aws-iam' ; import * as cdk from '@aws-cdk/core' ; import * as regionInfo from '@aws-cdk/region-info' ; import { Service } from '../service' ; import { Container } from './container' ; import { ServiceExtension , ServiceBuild } from './extension-interfaces' ; import { Construct } from '@aws-cdk/core' ; const APP_MESH_ENVOY_SIDECAR_VERSION = 'v1.15.1.0-prod' ; /**
 * The settings for the App Mesh extension.
 */ export interface MeshProps { /**
     * The service mesh into which to register the service.
     */ readonly mesh : appmesh . Mesh ; default readonly protocol ? : appmesh . Protocol ; } /**
 * This extension adds an Envoy sidecar to the task definition and
 * creates the App Mesh resources required to route network traffic
 * to the container in a service mesh.
 *
 * The service will then be available to other App Mesh services at the
 * address `<service name>.<environment name>`. For example, a service called
 * `orders` deploying in an environment called `production` would be accessible
 * to other App Mesh enabled services at the address `http://orders.production`.
 */ export class AppMeshExtension extends ServiceExtension { protected virtualNode ! : appmesh . VirtualNode ; protected virtualService ! : appmesh . VirtualService ; protected virtualRouter ! : appmesh . VirtualRouter ; protected route ! : appmesh . Route ; private mesh : appmesh . Mesh ; /**
     * The protocol used for AppMesh routing.
     * default - Protocol.HTTP
     */ public readonly protocol : appmesh . Protocol ; constructor ( props ) { super ( 'appmesh' ) ; this . mesh = props . mesh ; if ( props . protocol ) { this . protocol = props . protocol ; } else { this . protocol = appmesh . Protocol . HTTP ; } } public prehook ( service , scope ) { this . parentService = service ; this . scope = scope ; if ( ! this . parentService . cluster . defaultCloudMapNamespace ) { this . parentService . environment . addDefaultCloudMapNamespace ( { name : this . parentService . environment . id , } ) ; } } public modifyTaskDefinitionProps ( props ) { const containerextension = this . parentService . serviceDescription . get ( 'service-container' ) as Container ; if ( ! containerextension ) { throw new Error ( 'Appmesh extension requires an application extension' ) ; } return { ... props , networkMode : ecs . NetworkMode . AWS_VPC , proxyConfiguration : new ecs . AppMeshProxyConfiguration ( { containerName : 'envoy' , properties : { appPorts : [ containerextension . trafficPort ] , proxyEgressPort : 15001 , proxyIngressPort : 15000 , ignoredUID : 1337 , ignoredGID : 1338 , egressIgnoredIPs : [ '169.254.170.2' , '169.254.169.254' , ] , egressIgnoredPorts : [ ] , } , } ) , } as ecs . TaskDefinitionProps ; } private accountIdForRegion ( region ) { return { ecrRepo : regionInfo . RegionInfo . get ( region ) . appMeshRepositoryAccount } ; } public useTaskDefinition ( taskDefinition ) { var region = cdk . Stack . of ( this . scope ) . region ; var partition = cdk . Stack . of ( this . scope ) . partition ; var appMeshRepo ; const mapping = new cdk . CfnMapping ( this . scope , `${ this . parentService . id }-envoy-image-account-mapping` , { mapping : { 'ap-northeast-1' : this . accountIdForRegion ( 'ap-northeast-1' ) , 'ap-northeast-2' : this . accountIdForRegion ( 'ap-northeast-2' ) , 'ap-south-1' : this . accountIdForRegion ( 'ap-south-1' ) , 'ap-southeast-1' : this . accountIdForRegion ( 'ap-southeast-1' ) , 'ap-southeast-2' : this . accountIdForRegion ( 'ap-southeast-1' ) , 'ca-central-1' : this . accountIdForRegion ( 'ca-central-1' ) , 'cn-north-1' : this . accountIdForRegion ( 'cn-north-1' ) , 'cn-northwest-1' : this . accountIdForRegion ( 'cn-northwest-1' ) , 'eu-central-1' : this . accountIdForRegion ( 'eu-central-1' ) , 'eu-north-1' : this . accountIdForRegion ( 'eu-north-1' ) , 'eu-south-1' : this . accountIdForRegion ( 'eu-south-1' ) , 'eu-west-1' : this . accountIdForRegion ( 'eu-west-1' ) , 'eu-west-2' : this . accountIdForRegion ( 'eu-west-2' ) , 'eu-west-3' : this . accountIdForRegion ( 'eu-west-3' ) , 'sa-east-1' : this . accountIdForRegion ( 'sa-east-1' ) , 'us-east-1' : this . accountIdForRegion ( 'us-east-1' ) , 'us-east-2' : this . accountIdForRegion ( 'us-east-2' ) , 'us-west-1' : this . accountIdForRegion ( 'us-west-1' ) , 'us-west-2' : this . accountIdForRegion ( 'us-west-2' ) , 'me-south-1' : this . accountIdForRegion ( 'me-south-1' ) , 'ap-east-1' : this . accountIdForRegion ( 'ap-east-1' ) , 'af-south-1' : this . accountIdForRegion ( 'af-south-1' ) , } , } ) ; const ownerAccount = mapping . findInMap ( region , 'ecrRepo' ) ; appMeshRepo = ecr . Repository . fromRepositoryAttributes ( this . scope , `${ this . parentService . id }-envoy-repo` , { repositoryName : 'aws-appmesh-envoy' , repositoryArn : `arn:${ partition }:ecr:${ region }:${ ownerAccount }:repository/aws-appmesh-envoy` , } ) ; this . container = taskDefinition . addContainer ( 'envoy' , { image : ecs . ContainerImage . fromEcrRepository ( appMeshRepo , APP_MESH_ENVOY_SIDECAR_VERSION ) , essential : true , environment : { APPMESH_VIRTUAL_NODE_NAME : `mesh/${ this . mesh . meshName }/virtualNode/${ this . parentService . id }` , AWS_REGION : cdk . Stack . of ( this . parentService ) . region , ENABLE_ENVOY_STATS_TAGS : '1' , ENABLE_ENVOY_DOG_STATSD : '1' , } , healthCheck : { command : [ 'CMD-SHELL' , 'curl -s http://localhost:9901/server_info | grep state | grep -q LIVE' , ] , startPeriod : cdk . Duration . seconds ( 10 ) , interval : cdk . Duration . seconds ( 5 ) , timeout : cdk . Duration . seconds ( 2 ) , } , memoryReservationMiB : 128 , user : '1337' , logging : new ecs . AwsLogDriver ( { streamPrefix : 'envoy' } ) , } ) ; new iam . Policy ( this . scope , `${ this . parentService . id }-envoy-to-appmesh` , { roles : [ taskDefinition . taskRole ] , statements : [ new iam . PolicyStatement ( { resources : [ this . mesh . meshArn ] , actions : [ 'appmesh:StreamAggregatedResources' ] , } ) , ] , } ) ; this . container . addUlimits ( { softLimit : 1024000 , hardLimit : 1024000 , name : ecs . UlimitName . NOFILE , } ) ; } public modifyServiceProps ( props ) { return { ... props , cloudMapOptions : { dnsRecordType : 'A' , dnsTtl : cdk . Duration . seconds ( 10 ) , failureThreshold : 2 , name : this . parentService . id , } , minHealthyPercent : 100 , maxHealthyPercent : 125 , } as ServiceBuild ; } public useService ( service ) { const containerextension = this . parentService . serviceDescription . get ( 'service-container' ) as Container ; if ( ! containerextension ) { throw new Error ( 'Firelens extension requires an application extension' ) ; } const cloudmapNamespace = this . parentService . cluster . defaultCloudMapNamespace ; if ( ! cloudmapNamespace ) { throw new Error ( 'You must add a CloudMap namespace to the ECS cluster in order to use the AppMesh extension' ) ; } function addListener ( protocol , port ) { switch ( protocol ) { case appmesh . Protocol . HTTP : return appmesh . VirtualNodeListener . http ( { port } ) ; case appmesh . Protocol . HTTP2 : return appmesh . VirtualNodeListener . http2 ( { port } ) ; case appmesh . Protocol . GRPC : return appmesh . VirtualNodeListener . grpc ( { port } ) ; case appmesh . Protocol . TCP : return appmesh . VirtualNodeListener . tcp ( { port } ) ; } } this . virtualNode = new appmesh . VirtualNode ( this . scope , `${ this . parentService . id }-virtual-node` , { mesh : this . mesh , virtualNodeName : this . parentService . id , serviceDiscovery : service . cloudMapService ? appmesh . ServiceDiscovery . cloudMap ( service . cloudMapService ) : undefined , listeners : [ addListener ( this . protocol , containerextension . trafficPort ) ] , } ) ; this . virtualRouter = new appmesh . VirtualRouter ( this . scope , `${ this . parentService . id }-virtual-router` , { mesh : this . mesh , listeners : [ this . virtualRouterListener ( containerextension . trafficPort ) , ] , virtualRouterName : `${ this . parentService . id }` , } ) ; const serviceName = `${ this . parentService . id }.${ cloudmapNamespace . namespaceName }` ; const weightedTargets = [ { virtualNode : this . virtualNode , weight : 1 , } ] ; this . route = this . virtualRouter . addRoute ( `${ this . parentService . id }-route` , { routeSpec : this . routeSpec ( weightedTargets , serviceName ) , } ) ; this . virtualService = new appmesh . VirtualService ( this . scope , `${ this . parentService . id }-virtual-service` , { virtualServiceProvider : appmesh . VirtualServiceProvider . virtualRouter ( this . virtualRouter ) , virtualServiceName : serviceName , } ) ; } public connectToService ( otherService ) { const otherAppMesh = otherService . serviceDescription . get ( 'appmesh' ) as AppMeshExtension ; const otherContainer = otherService . serviceDescription . get ( 'service-container' ) as Container ; if ( otherAppMesh . parentService . environment . id !== this . parentService . environment . id ) { throw new Error ( `Unable to connect service '${ this . parentService . id }' in environment '${ this . parentService . environment . id }' to service '${ otherService . id }' in environment '${ otherAppMesh . parentService . environment . id }' because services can not be connected across environment boundaries` ) ; } this . parentService . ecsService . connections . allowTo ( otherService . ecsService , ec2 . Port . tcp ( otherContainer . trafficPort ) , `Accept inbound traffic from ${ this . parentService . id }` ) ; this . virtualNode . addBackend ( appmesh . Backend . virtualService ( otherAppMesh . virtualService ) ) ; } private routeSpec ( weightedTargets , serviceName ) { switch ( this . protocol ) { case appmesh . Protocol . HTTP : return appmesh . RouteSpec . http ( { weightedTargets : weightedTargets , } ) ; case appmesh . Protocol . HTTP2 : return appmesh . RouteSpec . http2 ( { weightedTargets : weightedTargets , } ) ; case appmesh . Protocol . GRPC : return appmesh . RouteSpec . grpc ( { weightedTargets : weightedTargets , match : { serviceName : serviceName , } , } ) ; case appmesh . Protocol . TCP : return appmesh . RouteSpec . tcp ( { weightedTargets : weightedTargets , } ) ; } } private virtualRouterListener ( port ) { switch ( this . protocol ) { case appmesh . Protocol . HTTP : return appmesh . VirtualRouterListener . http ( port ) ; case appmesh . Protocol . HTTP2 : return appmesh . VirtualRouterListener . http2 ( port ) ; case appmesh . Protocol . GRPC : return appmesh . VirtualRouterListener . grpc ( port ) ; case appmesh . Protocol . TCP : return appmesh . VirtualRouterListener . tcp ( port ) ; } } }