Exemplo n.º 1
0
        public FrontendStack(Construct scope, string name, string url, StackProps props = null) : base(scope, $"frontend-{name}", props)
        {
            // pricing - hosted zone
            // 0,50 USD por zona hospedada/mês para as primeiras 25 zonas hospedadas
            // 0,10 USD por zona hospedada/mês para zonas hospedadas adicionais
            // Consultas padrão
            // 0,40 USD por milhão de consultas – primeiro 1 bilhão de consultas/mês
            // 0,20 USD por milhão de consultas – mais de 1 bilhão de consultas/mês
            // Consultas de roteamento baseado em latência
            // 0,60 USD por milhão de consultas – primeiro 1 bilhão de consultas/mês
            // 0,30 USD por milhão de consultas – mais de 1 bilhão de consultas/mês
            // Consultas de Geo DNS e geoproximidade
            // 0,70 USD por milhão de consultas -- primeiro 1 bilhão de consultas/mês
            // 0,35 USD por milhão de consultas -- mais de 1 bilhão de consultas/mês

            _bucket = new Bucket(this, $"frontend-{name}-bucket", new BucketProps()
            {
                BucketName           = name + "72b302bf297a228a75730123efef7c41",
                WebsiteIndexDocument = "index.html",
                PublicReadAccess     = true,
                RemovalPolicy        = RemovalPolicy.DESTROY
            });

            _bucketDeployment = new BucketDeployment(this, $"frontend-{name}-deployment", new BucketDeploymentProps()
            {
                Sources           = new[] { Source.Asset("../tools/frontend/") },
                DestinationBucket = _bucket,
                RetainOnDelete    = false
            });

            _hostedZone = new HostedZone(this, $"frontend-{name}-hostedzone", new HostedZoneProps
            {
                ZoneName = url
            });

            _certificate = new Certificate(this, $"frontend-{name}-certificate", new CertificateProps
            {
                DomainName = url,
                Validation = CertificateValidation.FromDns(_hostedZone)
            });

            _distribution = new Distribution(this, $"frontend-{name}-distribution", new DistributionProps
            {
                DefaultBehavior = new BehaviorOptions
                {
                    Origin = new S3Origin(_bucket),
                    ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS
                },
                DomainNames       = new[] { url },
                Certificate       = _certificate,
                DefaultRootObject = "index.html"
            });

            _aRecord = new ARecord(this, $"frontend-{name}-arecord", new ARecordProps
            {
                Zone       = _hostedZone,
                RecordName = url,
                Target     = RecordTarget.FromAlias(new CloudFrontTarget(_distribution))
            });
        }
Exemplo n.º 2
0
        public void Construct(WebsiteConfiguration app)
        {
            var bucket = new Bucket(this.stack, "WebsiteBucket", new BucketProps {
                BucketName           = app.DomainName,
                PublicReadAccess     = true,
                RemovalPolicy        = RemovalPolicy.DESTROY,
                WebsiteIndexDocument = "index.html"
            });

            var deploy = new BucketDeployment(this.stack, "BucketDeployment", new BucketDeploymentProps {
                DestinationBucket = bucket,
                Sources           = new [] { Source.Asset(app.Resource) }
            });
        }
Exemplo n.º 3
0
        private void ConfigureS3Deployment(IRecipeProps <Configuration> props)
        {
            if (ContentS3Bucket == null)
            {
                throw new InvalidOperationException($"{nameof(ContentS3Bucket)} has not been set. The {nameof(ConfigureS3ContentBucket)} method should be called before {nameof(ContentS3Bucket)}");
            }

            if (string.IsNullOrEmpty(props.DotnetPublishOutputDirectory))
            {
                throw new InvalidOrMissingConfigurationException("The provided path containing the dotnet publish output is null or empty.");
            }

            var bucketDeploymentProps = new BucketDeploymentProps
            {
                Sources           = new ISource[] { Source.Asset(Path.Combine(props.DotnetPublishOutputDirectory, "wwwroot")) },
                DestinationBucket = ContentS3Bucket,
                MemoryLimit       = 4096,

                Distribution      = CloudFrontDistribution,
                DistributionPaths = new string[] { "/*" }
            };

            ContentS3Deployment = new BucketDeployment(this, nameof(ContentS3Deployment), InvokeCustomizeCDKPropsEvent(nameof(ContentS3Deployment), this, bucketDeploymentProps));
        }
Exemplo n.º 4
0
        internal AuthlambdaStack(Construct scope, string id, AuthlambdaStackProps props = null) : base(scope, id, props)
        {
            functionsStack = props.functionsStack;

            Bucket websiteBucket = new Bucket(this, "websiteBucket", new BucketProps()
            {
                BlockPublicAccess = BlockPublicAccess.BLOCK_ALL,
                PublicReadAccess  = false,
                //WebsiteIndexDocument = "index.html",
                RemovalPolicy = RemovalPolicy.DESTROY,
                Cors          = new ICorsRule[] {
                    new CorsRule()
                    {
                        AllowedHeaders = new string[] { "Authorization", "Content-Type", "Origin" },
                        AllowedMethods = new HttpMethods[] { HttpMethods.GET, HttpMethods.HEAD },
                        AllowedOrigins = new string[] { "*" }
                    }
                }
            });

            Bucket privateBucket = new Bucket(this, "privateBucket", new BucketProps()
            {
                BlockPublicAccess = BlockPublicAccess.BLOCK_ALL,
                PublicReadAccess  = false,
                RemovalPolicy     = RemovalPolicy.DESTROY,
                Cors = new ICorsRule[] {
                    new CorsRule()
                    {
                        AllowedHeaders = new string[] { "Authorization", "Content-Type", "Origin" },
                        AllowedMethods = new HttpMethods[] { HttpMethods.GET, HttpMethods.HEAD },
                        AllowedOrigins = new string[] { "*" }
                    }
                }
            });

            // The S3 bucket deployment for the website
            var websiteDeployment = new BucketDeployment(this, "TestStaticWebsiteDeployment", new BucketDeploymentProps()
            {
                Sources           = new [] { Source.Asset("./src/website") },
                DestinationBucket = websiteBucket,
                RetainOnDelete    = false
            });

            var privateDeployment = new BucketDeployment(this, "TestPrivateDeployment", new BucketDeploymentProps()
            {
                Sources           = new [] { Source.Asset("./src/private") },
                DestinationBucket = privateBucket,
                RetainOnDelete    = false
            });

            var cloudfrontOAI = OriginAccessIdentity.FromOriginAccessIdentityName(this, "CloudfrontOAIName", cloudfrontOAIName);

            websiteBucket.GrantRead(cloudfrontOAI.GrantPrincipal);
            privateBucket.GrantRead(cloudfrontOAI.GrantPrincipal);

            var cachePolicy = new CachePolicy(this, "TestCachePolicy", new CachePolicyProps()
            {
                CachePolicyName = "TestCachePolicy",
                Comment         = "Cache policy for Testing",
                DefaultTtl      = Duration.Seconds(0),
                CookieBehavior  = CacheCookieBehavior.All(),
                HeaderBehavior  = CacheHeaderBehavior.AllowList(
                    "Authorization",
                    "Content-Type",
                    "Origin"
                    ),
                QueryStringBehavior        = CacheQueryStringBehavior.All(),
                EnableAcceptEncodingBrotli = false,
                EnableAcceptEncodingGzip   = false
            });

            var websiteOrigin = new S3Origin(websiteBucket, new S3OriginProps()
            {
                OriginAccessIdentity = cloudfrontOAI
            });
            var privateOrigin = new S3Origin(privateBucket, new S3OriginProps()
            {
                OriginAccessIdentity = cloudfrontOAI
            });

            var dummyOrigin = new HttpOrigin("example.com", new HttpOriginProps()
            {
                ProtocolPolicy = OriginProtocolPolicy.HTTPS_ONLY
            });

            // default behavior is for the privateOrigin
            var defaultPrivateBehavior = new BehaviorOptions {
                AllowedMethods       = AllowedMethods.ALLOW_ALL,
                CachePolicy          = cachePolicy,
                OriginRequestPolicy  = OriginRequestPolicy.CORS_S3_ORIGIN,
                ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                Origin      = privateOrigin,
                EdgeLambdas = new IEdgeLambda[] {
                    new EdgeLambda()
                    {
                        EventType       = LambdaEdgeEventType.VIEWER_REQUEST,
                        FunctionVersion = functionsStack.checkAuthHandler.CurrentVersion,
                    },
                    new EdgeLambda()
                    {
                        EventType       = LambdaEdgeEventType.ORIGIN_RESPONSE,
                        FunctionVersion = functionsStack.httpHeadersHandler.CurrentVersion
                    }
                }
            };

            // this behavior is for dummy origin
            var parseAuthBehavior = new BehaviorOptions {
                AllowedMethods       = AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
                CachePolicy          = cachePolicy,
                OriginRequestPolicy  = OriginRequestPolicy.CORS_S3_ORIGIN,
                ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                Origin      = dummyOrigin,
                EdgeLambdas = new IEdgeLambda[] {
                    new EdgeLambda()
                    {
                        EventType       = LambdaEdgeEventType.VIEWER_REQUEST,
                        FunctionVersion = functionsStack.parseAuthHandler.CurrentVersion,
                    }
                }
            };

            var refreshAuthBehavior = new BehaviorOptions {
                AllowedMethods       = AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
                CachePolicy          = cachePolicy,
                OriginRequestPolicy  = OriginRequestPolicy.CORS_S3_ORIGIN,
                ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                Origin      = dummyOrigin,
                EdgeLambdas = new IEdgeLambda[] {
                    new EdgeLambda()
                    {
                        EventType       = LambdaEdgeEventType.VIEWER_REQUEST,
                        FunctionVersion = functionsStack.refreshAuthHandler.CurrentVersion,
                    }
                }
            };

            var signOutBehavior = new BehaviorOptions {
                AllowedMethods       = AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
                CachePolicy          = cachePolicy,
                OriginRequestPolicy  = OriginRequestPolicy.CORS_S3_ORIGIN,
                ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                Origin      = dummyOrigin,
                EdgeLambdas = new IEdgeLambda[] {
                    new EdgeLambda()
                    {
                        EventType       = LambdaEdgeEventType.VIEWER_REQUEST,
                        FunctionVersion = functionsStack.signOutHandler.CurrentVersion,
                    }
                }
            };

            Distribution distribution = new Distribution(this, "TestCloudfrontDistribution", new DistributionProps()
            {
                Comment           = "Test Website Distribution",
                DefaultRootObject = "index.html",
                PriceClass        = PriceClass.PRICE_CLASS_ALL,
                GeoRestriction    = GeoRestriction.Whitelist(new [] {
                    "IN"
                }),
                DefaultBehavior = defaultPrivateBehavior,
            });

            distribution.AddBehavior("/parseauth", dummyOrigin, parseAuthBehavior);
            distribution.AddBehavior("/refreshauth", dummyOrigin, refreshAuthBehavior);
            distribution.AddBehavior("/signout", dummyOrigin, signOutBehavior);

            var domainNameOutput = new CfnOutput(this, "TestWebsiteDistributionDomainName", new CfnOutputProps()
            {
                Value = distribution.DistributionDomainName
            });
        }
Exemplo n.º 5
0
        internal CdkStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            var stackProps                = ReportStackProps.ParseOrDefault(props);
            var dframeWorkerLogGroup      = "MagicOnionBenchWorkerLogGroup";
            var dframeMasterLogGroup      = "MagicOnionBenchMasterLogGroup";
            var benchNetwork              = stackProps.GetBenchNetwork();
            var recreateMagicOnionTrigger = stackProps.GetBenchmarkServerBinariesHash();

            // s3
            var s3 = new Bucket(this, "Bucket", new BucketProps
            {
                AutoDeleteObjects = true,
                RemovalPolicy     = RemovalPolicy.DESTROY,
                AccessControl     = BucketAccessControl.PRIVATE,
            });
            var lifecycleRule = new LifecycleRule
            {
                Enabled    = true,
                Prefix     = "reports/",
                Expiration = Duration.Days(stackProps.DaysKeepReports),
                AbortIncompleteMultipartUploadAfter = Duration.Days(1),
            };

            s3.AddLifecycleRule(lifecycleRule);
            s3.AddToResourcePolicy(new PolicyStatement(new PolicyStatementProps
            {
                Sid        = "AllowPublicRead",
                Effect     = Effect.ALLOW,
                Principals = new[] { new AnyPrincipal() },
                Actions    = new[] { "s3:GetObject*" },
                Resources  = new[] { $"{s3.BucketArn}/html/*" },
            }));
            s3.AddToResourcePolicy(new PolicyStatement(new PolicyStatementProps
            {
                Sid        = "AllowAwsAccountAccess",
                Effect     = Effect.ALLOW,
                Principals = new[] { new AccountRootPrincipal() },
                Actions    = new[] { "s3:*" },
                Resources  = new[] { $"{s3.BucketArn}/*" },
            }));

            // s3 deploy
            var masterDllDeployment = new BucketDeployment(this, "DeployMasterDll", new BucketDeploymentProps
            {
                DestinationBucket    = s3,
                Sources              = new[] { Source.Asset(Path.Combine(Directory.GetCurrentDirectory(), $"out/linux/server")) },
                DestinationKeyPrefix = $"assembly/linux/server"
            });
            var userdataDeployment = new BucketDeployment(this, "UserData", new BucketDeploymentProps
            {
                DestinationBucket    = s3,
                Sources              = new[] { Source.Asset(Path.Combine(Directory.GetCurrentDirectory(), "userdata/")) },
                DestinationKeyPrefix = "userdata/"
            });

            // docker deploy
            var dockerImage = new DockerImageAsset(this, "dframeWorkerImage", new DockerImageAssetProps
            {
                Directory = Path.Combine(Directory.GetCurrentDirectory(), "app"),
                File      = "ConsoleAppEcs/Dockerfile.Ecs",
            });
            var dframeImage = ContainerImage.FromDockerImageAsset(dockerImage);

            // network
            var vpc = new Vpc(this, "Vpc", new VpcProps
            {
                MaxAzs              = 2,
                NatGateways         = 0,
                SubnetConfiguration = new[] { new SubnetConfiguration {
                                                  Name = "public", SubnetType = SubnetType.PUBLIC
                                              } },
            });
            var allsubnets = new SubnetSelection {
                Subnets = vpc.PublicSubnets
            };
            var singleSubnets = new SubnetSelection {
                Subnets = new[] { vpc.PublicSubnets.First() }
            };
            var sg = new SecurityGroup(this, "MasterSg", new SecurityGroupProps
            {
                AllowAllOutbound = true,
                Vpc = vpc,
            });

            foreach (var subnet in vpc.PublicSubnets)
            {
                sg.AddIngressRule(Peer.Ipv4(vpc.VpcCidrBlock), Port.AllTcp(), "VPC", true);
            }

            // service discovery
            var serviceDiscoveryDomain = "local";
            var serverMapName          = "server";
            var dframeMapName          = "dframe-master";
            var ns = new PrivateDnsNamespace(this, "Namespace", new PrivateDnsNamespaceProps
            {
                Vpc  = vpc,
                Name = serviceDiscoveryDomain,
            });
            var serviceDiscoveryServer = ns.CreateService("server", new DnsServiceProps
            {
                Name          = serverMapName,
                DnsRecordType = DnsRecordType.A,
                RoutingPolicy = RoutingPolicy.MULTIVALUE,
            });

            // alb
            var albDnsName = "benchmark-alb";
            var benchToMagicOnionDnsName = benchNetwork.RequireAlb
                ? $"{benchNetwork.EndpointScheme}://{albDnsName}.{stackProps.AlbDomain.domain}"
                : $"{benchNetwork.EndpointScheme}://{serverMapName}.{serviceDiscoveryDomain}";
            IApplicationTargetGroup grpcTargetGroup  = null;
            IApplicationTargetGroup httpsTargetGroup = null;

            if (benchNetwork.RequireAlb)
            {
                // route53
                var hostedZone = HostedZone.FromHostedZoneAttributes(this, "HostedZone", new HostedZoneAttributes
                {
                    HostedZoneId = stackProps.AlbDomain.zoneId,
                    ZoneName     = stackProps.AlbDomain.domain,
                });

                // acm
                var certificate = new DnsValidatedCertificate(this, "certificate", new DnsValidatedCertificateProps
                {
                    DomainName = $"{albDnsName}.{hostedZone.ZoneName}",
                    HostedZone = hostedZone,
                });
                // alb
                var lb = new ApplicationLoadBalancer(this, "LB", new ApplicationLoadBalancerProps
                {
                    Vpc           = vpc,
                    VpcSubnets    = allsubnets,
                    SecurityGroup = new SecurityGroup(this, "AlbSg", new SecurityGroupProps
                    {
                        AllowAllOutbound = true,
                        Vpc = vpc,
                    }),
                    InternetFacing = false,
                    Http2Enabled   = true,
                });
                grpcTargetGroup  = AddGrpcTargetGroup(benchNetwork, vpc, certificate, lb);
                httpsTargetGroup = AddHttpsTargetGroup(benchNetwork, vpc, certificate, lb);

                // Dns Record
                _ = new CnameRecord(this, "alb-alias-record", new CnameRecordProps
                {
                    RecordName = $"{albDnsName}.{stackProps.AlbDomain.domain}",
                    Ttl        = Duration.Seconds(60),
                    Zone       = hostedZone,
                    DomainName = lb.LoadBalancerDnsName,
                });
            }

            // iam
            var iamEc2MagicOnionRole  = GetIamEc2MagicOnionRole(s3, serviceDiscoveryServer);
            var iamEcsTaskExecuteRole = GetIamEcsTaskExecuteRole(new[] { dframeWorkerLogGroup, dframeMasterLogGroup });
            var iamDFrameTaskDefRole  = GetIamEcsDframeTaskDefRole(s3);
            var iamWorkerTaskDefRole  = GetIamEcsWorkerTaskDefRole(s3);

            // secrets
            var ddToken = stackProps.UseEc2DatadogAgentProfiler || stackProps.UseFargateDatadogAgentProfiler
                ? Amazon.CDK.AWS.SecretsManager.Secret.FromSecretNameV2(this, "dd-token", "magiconion-benchmark-datadog-token")
                : null;

            // MagicOnion
            var asg = new AutoScalingGroup(this, "MagicOnionAsg", new AutoScalingGroupProps
            {
                // Monitoring is default DETAILED.
                SpotPrice                = "1.0", // 0.0096 for spot price average for m3.medium
                Vpc                      = vpc,
                SecurityGroup            = sg,
                VpcSubnets               = singleSubnets,
                InstanceType             = stackProps.MagicOnionInstanceType,
                DesiredCapacity          = 1,
                MaxCapacity              = 1,
                MinCapacity              = 0,
                AssociatePublicIpAddress = true,
                MachineImage             = new AmazonLinuxImage(new AmazonLinuxImageProps
                {
                    CpuType        = AmazonLinuxCpuType.X86_64,
                    Generation     = AmazonLinuxGeneration.AMAZON_LINUX_2,
                    Storage        = AmazonLinuxStorage.GENERAL_PURPOSE,
                    Virtualization = AmazonLinuxVirt.HVM,
                }),
                AllowAllOutbound = true,
                GroupMetrics     = new[] { GroupMetrics.All() },
                Role             = iamEc2MagicOnionRole,
                UpdatePolicy     = UpdatePolicy.ReplacingUpdate(),
                Signals          = Signals.WaitForCount(1, new SignalsOptions
                {
                    Timeout = Duration.Minutes(10),
                }),
            });

            asg.AddSecretsReadGrant(ddToken, () => stackProps.UseEc2DatadogAgentProfiler);
            var userdata = GetUserData(recreateMagicOnionTrigger, s3.BucketName, stackProps.BenchmarkBinaryNames, serviceDiscoveryServer.ServiceId, stackProps.UseEc2CloudWatchAgentProfiler, stackProps.UseEc2DatadogAgentProfiler);

            asg.AddUserData(userdata);
            asg.UserData.AddSignalOnExitCommand(asg);
            asg.Node.AddDependency(masterDllDeployment);
            asg.Node.AddDependency(userdataDeployment);
            if (stackProps.EnableMagicOnionScaleInCron)
            {
                asg.ScaleOnSchedule("ScheduleOut", new BasicScheduledActionProps
                {
                    DesiredCapacity = 1,
                    MaxCapacity     = 1,
                    // AM9:00 (JST+9) on Monday to Wednesday
                    Schedule = Schedule.Expression("0 0 * 1-3 *"),
                });
                asg.ScaleOnSchedule("ScheduleIn", new BasicScheduledActionProps
                {
                    DesiredCapacity = 0,
                    MaxCapacity     = 0,
                    // PM9:00 (JST+9) on Everyday
                    Schedule = Schedule.Expression("0 12 * 1-7 *"),
                });
            }
            if (benchNetwork.RequireAlb)
            {
                asg.AttachToApplicationTargetGroup(grpcTargetGroup);
                asg.AttachToApplicationTargetGroup(httpsTargetGroup);
            }

            // ECS
            var cluster = new Cluster(this, "WorkerCluster", new ClusterProps
            {
                Vpc = vpc,
            });

            cluster.Node.AddDependency(asg); // wait until asg is up

            // dframe-worker
            var dframeWorkerContainerName = "worker";
            var dframeWorkerTaskDef       = new FargateTaskDefinition(this, "DFrameWorkerTaskDef", new FargateTaskDefinitionProps
            {
                ExecutionRole  = iamEcsTaskExecuteRole,
                TaskRole       = iamWorkerTaskDefRole,
                Cpu            = stackProps.WorkerFargate.CpuSize,
                MemoryLimitMiB = stackProps.WorkerFargate.MemorySize,
            });

            dframeWorkerTaskDef.AddContainer(dframeWorkerContainerName, new ContainerDefinitionOptions
            {
                Image       = dframeImage,
                Command     = new[] { "--worker-flag" },
                Environment = new Dictionary <string, string>
                {
                    { "DFRAME_MASTER_CONNECT_TO_HOST", $"{dframeMapName}.{serviceDiscoveryDomain}" },
                    { "DFRAME_MASTER_CONNECT_TO_PORT", "12345" },
                    { "BENCH_SERVER_HOST", benchToMagicOnionDnsName },
                    { "BENCH_REPORTID", stackProps.ReportId },
                    { "BENCH_S3BUCKET", s3.BucketName },
                },
                Logging = LogDriver.AwsLogs(new AwsLogDriverProps
                {
                    LogGroup = new LogGroup(this, "WorkerLogGroup", new LogGroupProps
                    {
                        LogGroupName  = dframeWorkerLogGroup,
                        RemovalPolicy = RemovalPolicy.DESTROY,
                        Retention     = RetentionDays.TWO_WEEKS,
                    }),
                    StreamPrefix = dframeWorkerLogGroup,
                }),
            });
            dframeWorkerTaskDef.AddDatadogContainer($"{dframeWorkerContainerName}-datadog", ddToken, () => stackProps.UseFargateDatadogAgentProfiler);
            var dframeWorkerService = new FargateService(this, "DFrameWorkerService", new FargateServiceProps
            {
                ServiceName       = "DFrameWorkerService",
                DesiredCount      = 0,
                Cluster           = cluster,
                TaskDefinition    = dframeWorkerTaskDef,
                VpcSubnets        = singleSubnets,
                SecurityGroups    = new[] { sg },
                PlatformVersion   = FargatePlatformVersion.VERSION1_4,
                MinHealthyPercent = 0,
                AssignPublicIp    = true,
            });

            // dframe-master
            var dframeMasterTaskDef = new FargateTaskDefinition(this, "DFrameMasterTaskDef", new FargateTaskDefinitionProps
            {
                ExecutionRole  = iamEcsTaskExecuteRole,
                TaskRole       = iamDFrameTaskDefRole,
                Cpu            = stackProps.MasterFargate.CpuSize,
                MemoryLimitMiB = stackProps.MasterFargate.MemorySize,
            });

            dframeMasterTaskDef.AddContainer("dframe", new ContainerDefinitionOptions
            {
                Image       = dframeImage,
                Environment = new Dictionary <string, string>
                {
                    { "DFRAME_CLUSTER_NAME", cluster.ClusterName },
                    { "DFRAME_MASTER_SERVICE_NAME", "DFrameMasterService" },
                    { "DFRAME_WORKER_CONTAINER_NAME", dframeWorkerContainerName },
                    { "DFRAME_WORKER_SERVICE_NAME", dframeWorkerService.ServiceName },
                    { "DFRAME_WORKER_TASK_NAME", Fn.Select(1, Fn.Split("/", dframeWorkerTaskDef.TaskDefinitionArn)) },
                    { "DFRAME_WORKER_IMAGE", dockerImage.ImageUri },
                    { "BENCH_REPORTID", stackProps.ReportId },
                    { "BENCH_S3BUCKET", s3.BucketName },
                },
                Logging = LogDriver.AwsLogs(new AwsLogDriverProps
                {
                    LogGroup = new LogGroup(this, "MasterLogGroup", new LogGroupProps
                    {
                        LogGroupName  = dframeMasterLogGroup,
                        RemovalPolicy = RemovalPolicy.DESTROY,
                        Retention     = RetentionDays.TWO_WEEKS,
                    }),
                    StreamPrefix = dframeMasterLogGroup,
                }),
            });
            dframeMasterTaskDef.AddDatadogContainer($"dframe-datadog", ddToken, () => stackProps.UseFargateDatadogAgentProfiler);
            var dframeMasterService = new FargateService(this, "DFrameMasterService", new FargateServiceProps
            {
                ServiceName       = "DFrameMasterService",
                DesiredCount      = 1,
                Cluster           = cluster,
                TaskDefinition    = dframeMasterTaskDef,
                VpcSubnets        = singleSubnets,
                SecurityGroups    = new[] { sg },
                PlatformVersion   = FargatePlatformVersion.VERSION1_4,
                MinHealthyPercent = 0,
                AssignPublicIp    = true,
            });

            dframeMasterService.EnableCloudMap(new CloudMapOptions
            {
                CloudMapNamespace = ns,
                Name          = dframeMapName,
                DnsRecordType = DnsRecordType.A,
                DnsTtl        = Duration.Seconds(300),
            });

            // output
            new CfnOutput(this, "ReportUrl", new CfnOutputProps {
                Value = $"https://{s3.BucketRegionalDomainName}/html/{stackProps.ReportId}/index.html"
            });
            new CfnOutput(this, "EndPointStyle", new CfnOutputProps {
                Value = stackProps.BenchmarkEndpoint.ToString()
            });
            new CfnOutput(this, "AsgName", new CfnOutputProps {
                Value = asg.AutoScalingGroupName
            });
            new CfnOutput(this, "EcsClusterName", new CfnOutputProps {
                Value = cluster.ClusterName
            });
            new CfnOutput(this, "DFrameWorkerEcsTaskdefImage", new CfnOutputProps {
                Value = dockerImage.ImageUri
            });
        }