constructor()

in src/keycloak.ts [499:620]


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

    const vpc = props.vpc;
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });
    const taskRole = new iam.Role(this, 'TaskRole', {
      assumedBy: new iam.CompositePrincipal(
        new iam.ServicePrincipal('ecs.amazonaws.com'),
        new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
      ),
    });
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
      cpu: 4096,
      memoryLimitMiB: 30720,
      executionRole: taskRole,
    });

    const logGroup = new logs.LogGroup(this, 'LogGroup', {
      retention: logs.RetentionDays.ONE_MONTH,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    const kc = taskDefinition.addContainer('keycloak', {
      image: ecs.ContainerImage.fromRegistry(this.getKeyCloakDockerImageUri(props.keycloakVersion.version)),
      environment: Object.assign({
        DB_ADDR: props.database.clusterEndpointHostname,
        DB_DATABASE: 'keycloak',
        DB_PORT: '3306',
        DB_USER: 'admin',
        DB_VENDOR: 'mysql',
        // KEYCLOAK_LOGLEVEL: 'DEBUG',
        JDBC_PARAMS: 'useSSL=false',
        JGROUPS_DISCOVERY_PROTOCOL: 'JDBC_PING',
        // We don't need to specify `initialize_sql` string into `JGROUPS_DISCOVERY_PROPERTIES` property,
        // because the default `initialize_sql` is compatible with MySQL. (See: https://github.com/belaban/JGroups/blob/master/src/org/jgroups/protocols/JDBC_PING.java#L55-L60)
        // But you need to specify `initialize_sql` for PostgreSQL, because `varbinary` schema is not supported. (See: https://github.com/keycloak/keycloak-containers/blob/d4ce446dde3026f89f66fa86b58c2d0d6132ce4d/docker-compose-examples/keycloak-postgres-jdbc-ping.yml#L49)
        // JGROUPS_DISCOVERY_PROPERTIES: '',
      }, props.env),
      secrets: {
        DB_PASSWORD: ecs.Secret.fromSecretsManager(props.database.secret, 'password'),
        KEYCLOAK_USER: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'username'),
        KEYCLOAK_PASSWORD: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'password'),
      },
      logging: ecs.LogDrivers.awsLogs({
        streamPrefix: 'keycloak',
        logGroup,
      }),
    });
    kc.addPortMappings(
      { containerPort: 8443 }, // HTTPS web port
      { containerPort: 7600 }, // jgroups-tcp
      { containerPort: 57600 }, // jgroups-tcp-fd
      { containerPort: 55200, protocol: ecs.Protocol.UDP }, // jgroups-udp
      { containerPort: 54200, protocol: ecs.Protocol.UDP }, // jgroups-udp-fd
    );

    // we need extra privileges to fetch keycloak docker images from China mirror site
    taskDefinition.executionRole?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'));

    this.service = new ecs.FargateService(this, 'Service', {
      cluster,
      taskDefinition,
      circuitBreaker: props.circuitBreaker ? { rollback: true } : undefined,
      desiredCount: props.nodeCount ?? 2,
      healthCheckGracePeriod: cdk.Duration.seconds(120),
    });
    // we need to allow traffic from the same secret group for keycloak cluster with jdbc_ping
    this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(7600), 'kc jgroups-tcp');
    this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(57600), 'kc jgroups-tcp-fd');
    this.service.connections.allowFrom(this.service.connections, ec2.Port.udp(55200), 'kc jgroups-udp');
    this.service.connections.allowFrom(this.service.connections, ec2.Port.udp(54200), 'kc jgroups-udp-fd');

    if (props.autoScaleTask) {
      const minCapacity = props.autoScaleTask.min ?? props.nodeCount ?? 2;
      const scaling = this.service.autoScaleTaskCount({
        minCapacity,
        maxCapacity: props.autoScaleTask.max ?? minCapacity+5,
      });
      scaling.scaleOnCpuUtilization('CpuScaling', {
        targetUtilizationPercent: props.autoScaleTask.targetCpuUtilization ?? 75,
      });
    };

    const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      vpcSubnets: props.publicSubnets,
      internetFacing: true,
    });
    printOutput(this, 'EndpointURL', `https://${alb.loadBalancerDnsName}`);

    const listener = alb.addListener('HttpsListener', {
      protocol: elbv2.ApplicationProtocol.HTTPS,
      certificates: [{ certificateArn: props.certificate.certificateArn }],
    });

    listener.addTargets('ECSTarget', {
      targets: [this.service],
      // set slow_start.duration_seconds to 60
      // see https://docs.aws.amazon.com/cli/latest/reference/elbv2/modify-target-group-attributes.html
      slowStart: cdk.Duration.seconds(60),
      stickinessCookieDuration: props.stickinessCookieDuration ?? cdk.Duration.days(1),
      port: 8443,
      protocol: elbv2.ApplicationProtocol.HTTPS,
    });

    // allow task execution role to read the secrets
    props.database.secret.grantRead(taskDefinition.executionRole!);
    props.keycloakSecret.grantRead(taskDefinition.executionRole!);

    // allow ecs task connect to database
    props.database.connections.allowDefaultPortFrom(this.service);


    // create a bastion host
    if (props.bastion === true) {
      const bast = new ec2.BastionHostLinux(this, 'Bast', {
        vpc,
        instanceType: new ec2.InstanceType('m5.large'),
      });
      props.database.connections.allowDefaultPortFrom(bast);
    }
  }