constructor()

in usecases/guest-webapp-sample/lib/blea-ecsapp-stack.ts [34:342]


  constructor(scope: cdk.Construct, id: string, props: BLEAECSAppStackProps) {
    super(scope, id, props);

    // --------------------- Fargate Cluster ----------------------------

    // ---- PreRequesties

    // Role for ECS Agent
    // The task execution role grants the Amazon ECS container and Fargate agents permission to make AWS API calls on your behalf.
    // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html
    const executionRole = new iam.Role(this, 'EcsTaskExecutionRole', {
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')],
    });

    // Role for Container
    // With IAM roles for Amazon ECS tasks, you can specify an IAM role that can be used by the containers in a task.
    // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
    const serviceTaskRole = new iam.Role(this, 'EcsServiceTaskRole', {
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
    });

    // SecurityGroup for Fargate service
    // - Inbound access will be added automatically on associating ALB
    // - Outbound access will be used for DB and AWS APIs
    const securityGroupForFargate = new ec2.SecurityGroup(this, 'SgFargate', {
      vpc: props.myVpc,
      allowAllOutbound: true, // for AWS APIs
    });
    this.appServerSecurityGroup = securityGroupForFargate;

    // CloudWatch Logs Group for Container
    const fargateLogGroup = new cwl.LogGroup(this, 'FargateLogGroup', {
      retention: cwl.RetentionDays.THREE_MONTHS,
      encryptionKey: props.appKey,
    });

    // Permission to access KMS Key from CloudWatch Logs
    props.appKey.addToResourcePolicy(
      new iam.PolicyStatement({
        actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'],
        principals: [new iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)],
        resources: ['*'],
        conditions: {
          ArnLike: {
            'kms:EncryptionContext:aws:logs:arn': `arn:aws:logs:${cdk.Stack.of(this).region}:${
              cdk.Stack.of(this).account
            }:*`,
          },
        },
      }),
    );

    // ---- Cluster definition

    // Fargate Cluster
    // -  Enabling CloudWatch ContainerInsights
    const ecsCluster = new ecs.Cluster(this, 'Cluster', {
      vpc: props.myVpc,
      containerInsights: true,
      enableFargateCapacityProviders: true,
    });
    this.ecsClusterName = ecsCluster.clusterName;

    // Task definition
    // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html
    const ecsTask = new ecs.FargateTaskDefinition(this, 'EcsTask', {
      executionRole: executionRole,
      taskRole: serviceTaskRole,
      cpu: 256,
      memoryLimitMiB: 512,
    });

    // Container
    const ecsContainer = ecsTask.addContainer('EcsApp', {
      // -- SAMPLE: if you want to use your ECR repository, you can use like this.
      image: ecs.ContainerImage.fromEcrRepository(props.repository, props.imageTag),

      // -- SAMPLE: if you want to use DockerHub, you can use like this.
      // image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),

      environment: {
        ENVIRONMENT_VARIABLE_SAMPLE_KEY: 'Environment Variable Sample Value',
      },
      logging: ecs.LogDriver.awsLogs({
        streamPrefix: 'BLEA-ECSApp-',
        logGroup: fargateLogGroup,
      }),
      // -- SAMPLE: Get value from SecretsManager
      // secrets: {
      //   SECRET_VARIABLE_SAMPLE_KEY: ecs.Secret.fromSecretsManager(secretsManagerConstruct, 'secret_key'),
      // },
    });

    ecsContainer.addPortMappings({
      containerPort: 80,
    });

    // Service
    const ecsService = new ecs.FargateService(this, 'FargateService', {
      cluster: ecsCluster,
      taskDefinition: ecsTask,
      desiredCount: 2,

      // The LATEST is recommended platform version.
      // But if you need another version replace this.
      // See also:
      // - https://docs.aws.amazon.com/AmazonECS/latest/userguide/platform_versions.html
      // - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ecs.FargatePlatformVersion.html
      platformVersion: ecs.FargatePlatformVersion.LATEST,

      // https://docs.aws.amazon.com/cdk/api/latest/docs/aws-ecs-readme.html#fargate-capacity-providers
      capacityProviderStrategies: [
        {
          capacityProvider: 'FARGATE',
          weight: 1,
        },
        // -- SAMPLE: Fargate Spot
        //{
        //  capacityProvider: 'FARGATE_SPOT',
        //  weight: 2,
        //},
      ],
      vpcSubnets: props.myVpc.selectSubnets({
        subnetGroupName: 'Private', // For public DockerHub
        //subnetGroupName: 'Protected'   // For your ECR. Need to use PrivateLinke for ECR
      }),
      securityGroups: [securityGroupForFargate],
    });
    this.ecsServiceName = ecsService.serviceName;

    // Define ALB Target Group
    // https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-elasticloadbalancingv2.ApplicationTargetGroup.html
    const lbForAppTargetGroup = props.webFront.appAlbListerner.addTargets('EcsApp', {
      protocol: elbv2.ApplicationProtocol.HTTP,
      targets: [ecsService],
      deregistrationDelay: cdk.Duration.seconds(30),
    });
    this.appTargetGroupName = lbForAppTargetGroup.targetGroupFullName;

    // SAMPLE: Another way, how to set attibute to TargetGroup - example) Modify algorithm type
    // lbForAppTargetGroup.setAttribute('load_balancing.algorithm.type', 'least_outstanding_requests');

    // SAMPLE: Setup HealthCheck for app
    // lbForAppTargetGroup.configureHealthCheck({
    //   path: '/health',
    //   enabled: true,
    // });

    // ECS Task AutoScaling
    // https://docs.aws.amazon.com/cdk/api/latest/docs/aws-ecs-readme.html#task-auto-scaling
    const ecsScaling = ecsService.autoScaleTaskCount({
      minCapacity: 2,
      maxCapacity: 10,
    });

    // Scaling with CPU Utilization avarage on all tasks
    this.ecsTargetUtilizationPercent = 50; // Used in Dashboard Stack
    ecsScaling.scaleOnCpuUtilization('CpuScaling', {
      targetUtilizationPercent: this.ecsTargetUtilizationPercent,
    });

    // Scaling with Requests per tasks
    this.ecsScaleOnRequestCount = 10000; // Used in Dashboard Stack
    ecsScaling.scaleOnRequestCount('RequestScaling', {
      requestsPerTarget: this.ecsScaleOnRequestCount,
      targetGroup: lbForAppTargetGroup,
    });

    // ----------------------- Alarms for ECS -----------------------------
    ecsService
      .metricCpuUtilization({
        period: cdk.Duration.minutes(1),
        statistic: cw.Statistic.AVERAGE,
      })
      .createAlarm(this, 'FargateCpuUtil', {
        evaluationPeriods: 3,
        datapointsToAlarm: 3,
        threshold: 80,
        comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        actionsEnabled: true,
      })
      .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // RunningTaskCount - CloudWatch Container Insights metric (Custom metric)
    // This is a sample of full set configuration for Metric and Alarm
    // See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarm-evaluation
    //
    // new cw.Metric({
    //   metricName: 'RunningTaskCount',
    //   namespace: 'ECS/ContainerInsights',
    //   dimensions: {
    //     ClusterName: ecsCluster.clusterName,
    //     ServiceName: ecsService.serviceName,
    //   },
    //   period: cdk.Duration.minutes(1),
    //   statistic: cw.Statistic.AVERAGE,
    // })
    //   .createAlarm(this, 'RunningTaskCount', {
    //     evaluationPeriods: 3,
    //     datapointsToAlarm: 2,
    //     threshold: 1,
    //     comparisonOperator: cw.ComparisonOperator.LESS_THAN_THRESHOLD,
    //     actionsEnabled: true,
    //   })
    //   .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // ----------------------- Alarms for ALB -----------------------------

    // Alarm for ALB - ResponseTime
    props.webFront.appAlb
      .metricTargetResponseTime({
        period: cdk.Duration.minutes(1),
        statistic: cw.Statistic.AVERAGE,
      })
      .createAlarm(this, 'AlbResponseTime', {
        evaluationPeriods: 3,
        threshold: 100,
        comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        actionsEnabled: true,
      })
      .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // Alarm for ALB - HTTP 4XX Count
    props.webFront.appAlb
      .metricHttpCodeElb(elbv2.HttpCodeElb.ELB_4XX_COUNT, {
        period: cdk.Duration.minutes(1),
        statistic: cw.Statistic.SUM,
      })
      .createAlarm(this, 'AlbHttp4xx', {
        evaluationPeriods: 3,
        threshold: 10,
        comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        actionsEnabled: true,
      })
      .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // Alarm for ALB - HTTP 5XX Count
    props.webFront.appAlb
      .metricHttpCodeElb(elbv2.HttpCodeElb.ELB_5XX_COUNT, {
        period: cdk.Duration.minutes(1),
        statistic: cw.Statistic.SUM,
      })
      .createAlarm(this, 'AlbHttp5xx', {
        evaluationPeriods: 3,
        threshold: 10,
        comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        actionsEnabled: true,
      })
      .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // Alarm for ALB TargetGroup - HealthyHostCount
    lbForAppTargetGroup
      .metricHealthyHostCount({
        period: cdk.Duration.minutes(1),
        statistic: cw.Statistic.AVERAGE,
      })
      .createAlarm(this, 'AlbTgHealthyHostCount', {
        evaluationPeriods: 3,
        threshold: 1,
        comparisonOperator: cw.ComparisonOperator.LESS_THAN_THRESHOLD,
        actionsEnabled: true,
      })
      .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // Alarm for ALB TargetGroup - UnHealthyHostCount
    // This alarm will be used on Dashbaord
    this.albTgUnHealthyHostCountAlarm = lbForAppTargetGroup
      .metricUnhealthyHostCount({
        period: cdk.Duration.minutes(1),
        statistic: cw.Statistic.AVERAGE,
      })
      .createAlarm(this, 'AlbTgUnHealthyHostCount', {
        evaluationPeriods: 3,
        threshold: 1,
        comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
        actionsEnabled: true,
      });
    this.albTgUnHealthyHostCountAlarm.addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

    // ----------------------- Event notification for ECS -----------------------------
    // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_cwe_events.html#ecs_service_events
    new cwe.Rule(this, 'ECSServiceActionEventRule', {
      description: 'CloudWatch Event Rule to send notification on ECS Service action events.',
      enabled: true,
      eventPattern: {
        source: ['aws.ecs'],
        detailType: ['ECS Service Action'],
        detail: {
          eventType: ['WARN', 'ERROR'],
        },
      },
      targets: [new cwet.SnsTopic(props.alarmTopic)],
    });

    // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_cwe_events.html#ecs_service_deployment_events
    new cwe.Rule(this, 'ECSServiceDeploymentEventRule', {
      description: 'CloudWatch Event Rule to send notification on ECS Service deployment events.',
      enabled: true,
      eventPattern: {
        source: ['aws.ecs'],
        detailType: ['ECS Deployment State Change'],
        detail: {
          eventType: ['WARN', 'ERROR'],
        },
      },
      targets: [new cwet.SnsTopic(props.alarmTopic)],
    });
  }