internal SubmitOrderStack()

in src/ModernTacoShop/SubmitOrder/cdk/SubmitOrderStack.cs [19:363]


        internal SubmitOrderStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            // Look up the VPC from another stack.
            var vpc = Vpc.FromLookup(this,
                "ModernTacoShop-VPC",
                new VpcLookupOptions
                {
                    VpcName = "ModernTacoShop-CommonStack/vpc"
                });

            // Get the certificate from Certificate Manager.
            var certificateArnParameter = StringParameter.FromStringParameterAttributes(this,
                "ModernTacoShop-CertificateARNParameter",
                new StringParameterAttributes
                {
                    ParameterName = "/ModernTacoShop/CertificateARN"
                });
            var certificate = Certificate.FromCertificateArn(this,
                "ModernTacoShop-Certificate",
                certificateArnParameter.StringValue);

            // Get the name of the code bucket from the parameter store.
            var codeBucketNameParameter = StringParameter.FromStringParameterAttributes(this,
                "ModernTacoShop-CodeBucketNameParameter",
                new StringParameterAttributes
                {
                    ParameterName = "/ModernTacoShop/CodeBucketName"
                });
            var codeBucket = Bucket.FromBucketName(this,
                "CodeBucket",
                codeBucketNameParameter.StringValue);

            // Create a role for the microservice.
            var submitOrderMicroserviceRole = new Role(this,
                "ModernTacoShop-SubmitOrder-MicroserviceRole",
                new RoleProps
                {
                    AssumedBy = new ServicePrincipal("ec2.amazonaws.com")
                });

            // Grant the microservice role access to read from the code bucket.
            // Instances need this access to download the microservice code.
            codeBucket.GrantRead(submitOrderMicroserviceRole);

            // Grant the microservice role full access to CloudWatch logs.
            submitOrderMicroserviceRole.AddManagedPolicy(ManagedPolicy.FromAwsManagedPolicyName("CloudWatchLogsFullAccess"));

            // Grant the microservice role access to read Systems Manager parameters that are microservice domain names.
            var readParameterPolicyArnParameter = StringParameter.FromStringParameterAttributes(this,
                "ModernTacoShop-ReadParameterPolicyARNParameter",
                new StringParameterAttributes
                {
                    ParameterName = "/ModernTacoShop/ReadMicroserviceDomainNameParametersPolicyARN"
                });
            var readParameterPolicy = ManagedPolicy.FromManagedPolicyArn(this,
                "ModernTacoShop-SubmitOrder-ReadParameterPolicy",
                readParameterPolicyArnParameter.StringValue);

            submitOrderMicroserviceRole.AddManagedPolicy(readParameterPolicy);

            // Create an instance profile for the role so it can be assigned to EC2 instances.
            var submitOrderMicroserviceInstanceProfile = new CfnInstanceProfile(this,
                "ModernTacoShop-SubmitOrder-MicroserviceRole-InstanceProfile",
                new CfnInstanceProfileProps
                {
                    Roles = new string[] { submitOrderMicroserviceRole.RoleName }
                });

            // Create a DynamoDB table for submitted orders.
            var table = new Table(this,
                "ModernTacoShop-SubmitOrder-Table",
                new TableProps
                {
                    PartitionKey = new Attribute
                    {
                        Name = "Id",
                        Type = AttributeType.NUMBER
                    },
                    BillingMode = BillingMode.PAY_PER_REQUEST,
                    Encryption = TableEncryption.AWS_MANAGED,
                    PointInTimeRecovery = true,
                    RemovalPolicy = RemovalPolicy.DESTROY
                });
            table.GrantReadWriteData(submitOrderMicroserviceRole);
            table.Grant(submitOrderMicroserviceRole, "dynamodb:DescribeTable");

            // The name of the DynamoDB table is (partially) generated by the script, so store the final table name in Parameter Store.
            var tableNameParameter = new StringParameter(this,
                "ModernTacoShop-SubmitOrder-TableNameParameter",
                new StringParameterProps
                {
                    ParameterName = "/ModernTacoShop/SubmitOrder/OrderTableName",
                    StringValue = table.TableName,
                    Tier = ParameterTier.STANDARD
                });
            // Allow the microservice role (assumed by the instances) to read the parameter value.
            tableNameParameter.GrantRead(submitOrderMicroserviceRole);

            // Create an ASG to run the service.
            var microserviceSecurityGroup = new SecurityGroup(this,
                "ModernTacoShop-SubmitOrder-MicroserviceSecurityGroup",
                new SecurityGroupProps
                {
                    Vpc = vpc,
                    AllowAllOutbound = false
                });

            // Allow outgoing HTTP/S traffic, so the instance can download updates and communicate with AWS regional services
            // (including CodeDeploy and S3).
            microserviceSecurityGroup.AddEgressRule(Peer.AnyIpv4(), Port.Tcp(80));
            microserviceSecurityGroup.AddEgressRule(Peer.AnyIpv4(), Port.Tcp(443));

            // Security: Suppress the cfn_nag warning for the microservice SG allowing outbound HTTP/S.
            ((CfnSecurityGroup)microserviceSecurityGroup.Node.DefaultChild).AddMetadata("cfn_nag",
                    new Dictionary<string, object>
                    {
                        ["rules_to_suppress"] = new Dictionary<string, object>[]
                        {
                            new Dictionary<string, object>
                            {
                                ["id"] = "W5",
                                ["reason"] = "This instance needs outbound Internet access to download updates and communicate with AWS regional services."
                            }
                        }
                    });

            // Use Ubuntu as it is officially supported for .NET 5.
            var ubuntuImage = new LookupMachineImage(new LookupMachineImageProps
            {
                Name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-20201026"
            });

            var autoScalingGroup = new AutoScalingGroup(this,
                "ModernTacoShop-SubmitOrder-ASG",
                new AutoScalingGroupProps
                {
                    Vpc = vpc,
                    InstanceType = InstanceType.Of(InstanceClass.BURSTABLE4_GRAVITON, InstanceSize.MICRO),
                    MachineImage = ubuntuImage,
                    SecurityGroup = microserviceSecurityGroup,
                    MinCapacity = 1,
                    MaxCapacity = 1,
                    UserData = UserData.ForLinux(),
                    Role = submitOrderMicroserviceRole,
                    VpcSubnets = new SubnetSelection
                    {
                        Subnets = vpc.PrivateSubnets
                    },
                    UpdatePolicy = UpdatePolicy.RollingUpdate(new RollingUpdateOptions
                    {
                        MinInstancesInService = 0
                    }),
                    HealthCheck = Amazon.CDK.AWS.AutoScaling.HealthCheck.Elb(new ElbHealthCheckOptions
                    {
                        Grace = Duration.Minutes(5)
                    }),
                });

            // Create a security group for the load balancer.
            // Set `allowAllOutbound` to false, because this SG should only allow traffic to the microservice SG (which contains the ASG instances).
            // Egress rules will be generated automatically from the configuration below.
            var microserviceLoadBalancerSecurityGroup = new SecurityGroup(this,
                "ModernTacoShop-SubmitOrder-MicroserviceLoadBalancerSecurityGroup",
                new SecurityGroupProps
                {
                    Vpc = vpc,
                    AllowAllOutbound = false
                });

            // Security: Suppress the cfn_nag warning for the ALB SG being open to the world.
            // This is a public-facing ELB and Internet ingress should be permitted.
            ((CfnSecurityGroup)microserviceLoadBalancerSecurityGroup.Node.DefaultChild).AddMetadata("cfn_nag",
                    new Dictionary<string, object>
                    {
                        ["rules_to_suppress"] = new Dictionary<string, object>[]
                        {
                            new Dictionary<string, object>
                            {
                                ["id"] = "W2",
                                ["reason"] = "This is a public facing ELB and ingress from the Internet should be permitted."
                            },
                            new Dictionary<string, object>
                            {
                                ["id"] = "W9",
                                ["reason"] = "This is a public facing ELB and ingress from the Internet should be permitted."
                            }
                        }
                    });

            var microserviceLoadBalancer = new ApplicationLoadBalancer(this,
                "ModernTacoShop-SubmitOrder-ALB",
                new ApplicationLoadBalancerProps
                {
                    Vpc = vpc,
                    Http2Enabled = true,
                    InternetFacing = true,
                    SecurityGroup = microserviceLoadBalancerSecurityGroup,
                    VpcSubnets = new SubnetSelection
                    {
                        Subnets = vpc.PublicSubnets
                    }
                });

            var loadBalancerAccessLogPrefix = "access-logs/modern-taco-shop/submit-order-alb";
            microserviceLoadBalancer.LogAccessLogs(codeBucket, loadBalancerAccessLogPrefix);

            // Create a load balancer listener using HTTPS.
            // HTTPS will terminate at the load balancer, so install the certificate onto the ALB.
            var microserviceListener = microserviceLoadBalancer.AddListener(
                "ModernTacoShop-SubmitOrder-ALB-Microservice-Listener",
                new BaseApplicationListenerProps
                {
                    Port = 443,
                    Protocol = ApplicationProtocol.HTTPS,
                    SslPolicy = SslPolicy.TLS12
                });
            microserviceListener.AddCertificates(
                "ModernTacoShop-Certificate",
                new ListenerCertificate[]
            {
                ListenerCertificate.FromCertificateManager(certificate)
            });

            // Create a target group pointing to the ASG. This will be over HTTP, as SSL is terminated at the load balancer.
            var microserviceTargetGroup = new ApplicationTargetGroup(this,
                "ModernTacoShop-SubmitOrder-ASG-Targets",
                new ApplicationTargetGroupProps
                {
                    Protocol = ApplicationProtocol.HTTP,
                    ProtocolVersion = ApplicationProtocolVersion.GRPC,
                    Vpc = vpc,
                    TargetType = TargetType.INSTANCE,
                    Port = 5000,
                    Targets = new IApplicationLoadBalancerTarget[] { autoScalingGroup },
                    DeregistrationDelay = Duration.Seconds(30)
                });

            // Enable a health check on the gRPC service.
            microserviceTargetGroup.ConfigureHealthCheck(new Amazon.CDK.AWS.ElasticLoadBalancingV2.HealthCheck()
            {
                Enabled = true,
                Timeout = Duration.Seconds(5),
                HealthyThresholdCount = 2,
                UnhealthyThresholdCount = 2,
                Interval = Duration.Seconds(10),
                Path = "/modern_taco_shop.SubmitOrder/HealthCheck",
                HealthyGrpcCodes = "0" // Code `0` in gRPC is success.
            });

            microserviceListener.AddTargetGroups(
                "ModernTacoShop-SubmitOrder-ALB-Microservice-Listener-Targets",
                new AddApplicationTargetGroupsProps
                {
                    TargetGroups = new ApplicationTargetGroup[] { microserviceTargetGroup }
                });

            // Create a sub-domain for this microservice, then target that subdomain to the load balancer.
            // First, get the 'parent' hosted zone from parameters that were stored in Parameter Store by the `common` stack.
            var hostedZoneIdParameter = StringParameter.FromStringParameterAttributes(this,
                "ModernTacoShop-ParentHostedZoneID",
                new StringParameterAttributes
                {
                    ParameterName = "/ModernTacoShop/HostedZoneID"
                });
            var hostedZoneNameParameter = StringParameter.FromStringParameterAttributes(this,
                "ModernTacoShop-ParentHostedZoneName",
                new StringParameterAttributes
                {
                    ParameterName = "/ModernTacoShop/HostedZoneName"
                });

            var hostedZone = HostedZone.FromHostedZoneAttributes(this,
                "ModernTacoShop-ParentHostedZone",
                new HostedZoneAttributes
                {
                    HostedZoneId = hostedZoneIdParameter.StringValue,
                    ZoneName = hostedZoneNameParameter.StringValue
                });

            // Create a sub-domain record for this microservice.
            // Target the load balancer.
            var record = new ARecord(this,
                "ModernTacoShop-SubmitOrder-ARecord",
                new ARecordProps
                {
                    Zone = hostedZone,
                    Target = RecordTarget.FromAlias(new LoadBalancerTarget(microserviceLoadBalancer)),
                    RecordName = "submit-order"
                });

            // Store the service domain name in Systems Manager Parameter Store.
            var recordDomainNameParameter = new StringParameter(this,
                "ModernTacoShop-SubmitOrder-DomainNameParameter",
                new StringParameterProps
                {
                    ParameterName = "/ModernTacoShop/SubmitOrder/DomainName",
                    StringValue = record.DomainName,
                    Tier = ParameterTier.STANDARD
                });

            // Tag the service domain name parameter so it can be accessed by other microservices.
            Amazon.CDK.Tags.Of(recordDomainNameParameter).Add("parameter-type", "modern-taco-shop-microservice-domain-name");

            // CodeDeploy configuration

            var codeDeployServiceRole = new Role(this,
                "ModernTacoShop-SubmitOrder-CodeDeploy-ServiceRole",
                new RoleProps
                {
                    AssumedBy = new ServicePrincipal("codedeploy.amazonaws.com")
                });
            codeDeployServiceRole.AddManagedPolicy(ManagedPolicy.FromAwsManagedPolicyName("service-role/AWSCodeDeployRole"));

            var application = new ServerApplication(this,
                "ModernTacoShop-SubmitOrder-CodeDeploy-Application",
                new ServerApplicationProps
                {
                    ApplicationName = "ModernTacoShop-SubmitOrder"
                });
            var deploymentGroup = new ServerDeploymentGroup(this,
                "ModernTacoShop-SubmitOrder-CodeDeploy-DeploymentGroup",
                new ServerDeploymentGroupProps
                {
                    Application = application,
                    LoadBalancer = LoadBalancer.Application(microserviceTargetGroup),
                    AutoScalingGroups = new AutoScalingGroup[] { autoScalingGroup },
                    InstallAgent = true,
                    DeploymentConfig = ServerDeploymentConfig.ALL_AT_ONCE,
                    Role = codeDeployServiceRole
                });

            new CfnOutput(this,
                "ModernTacoShop-SubmitOrder-LoadBalancerName",
                new CfnOutputProps
                {
                    Value = microserviceLoadBalancer.LoadBalancerName
                });

            new CfnOutput(this,
                "ModernTacoShop-SubmitOrder-DeploymentGroupNameOutput",
                new CfnOutputProps
                {
                    Value = deploymentGroup.DeploymentGroupName
                });
        }