private ecs()

in packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts [232:453]


  private ecs() {
    const {
      categoryName,
      apiName,
      policies,
      containers,
      secretsArns,
      taskEnvironmentVariables,
      exposedContainer,
      taskPorts,
      isInitialDeploy,
      desiredCount,
      currentStackName,
      createCloudMapService,
    } = this.props;

    let cloudMapService: cloudmap.CfnService = undefined;

    if (createCloudMapService) {
      cloudMapService = new cloudmap.CfnService(this, 'CloudmapService', {
        name: apiName,
        dnsConfig: {
          dnsRecords: [
            {
              ttl: 60,
              type: cloudmap.DnsRecordType.SRV,
            },
          ],
          namespaceId: this.cloudMapNamespaceId,
          routingPolicy: cloudmap.RoutingPolicy.MULTIVALUE,
        },
      });
    }

    const task = new ecs.TaskDefinition(this, 'TaskDefinition', {
      compatibility: ecs.Compatibility.FARGATE,
      memoryMiB: '1024',
      cpu: '512',
      family: `${this.envName}-${apiName}`,
    });
    (task.node.defaultChild as ecs.CfnTaskDefinition).overrideLogicalId('TaskDefinition');
    policies.forEach(policy => {
      const statement = isPolicyStatement(policy) ? policy : wrapJsonPoliciesInCdkPolicies(policy);

      task.addToTaskRolePolicy(statement);
    });

    const containersInfo: {
      container: ecs.ContainerDefinition;
      repository: ecr.IRepository;
    }[] = [];

    containers.forEach(
      ({
        name,
        image,
        build,
        portMappings,
        logConfiguration,
        environment,
        entrypoint: entryPoint,
        command,
        working_dir: workingDirectory,
        healthcheck: healthCheck,
        secrets: containerSecrets,
      }) => {
        const logGroup = new logs.LogGroup(this, `${name}ContainerLogGroup`, {
          logGroupName: `/ecs/${this.envName}-${apiName}-${name}`,
          retention: logs.RetentionDays.ONE_MONTH,
          removalPolicy: cdk.RemovalPolicy.DESTROY,
        });

        const { logDriver, options: { 'awslogs-stream-prefix': streamPrefix } = {} } = logConfiguration;

        const logging: ecs.LogDriver =
          logDriver === 'awslogs'
            ? ecs.LogDriver.awsLogs({
                streamPrefix,
                logGroup: logs.LogGroup.fromLogGroupName(this, `${name}logGroup`, logGroup.logGroupName),
              })
            : undefined;

        let repository: ecr.IRepository;
        if (build) {
          const logicalId = `${name}Repository`;

          const repositoryName = `${currentStackName}-${categoryName}-${apiName}-${name}`;

          if (this.props.existingEcrRepositories.has(repositoryName)) {
            repository = ecr.Repository.fromRepositoryName(this, logicalId, repositoryName);
          } else {
            repository = new ecr.Repository(this, logicalId, {
              repositoryName: `${this.envName}-${categoryName}-${apiName}-${name}`,
              removalPolicy: cdk.RemovalPolicy.RETAIN,
              lifecycleRules: [
                {
                  rulePriority: 10,
                  maxImageCount: 1,
                  tagPrefixList: ['latest'],
                  tagStatus: ecr.TagStatus.TAGGED,
                },
                {
                  rulePriority: 100,
                  maxImageAge: cdk.Duration.days(7),
                  tagStatus: ecr.TagStatus.ANY,
                },
              ],
            });
            (repository.node.defaultChild as ecr.CfnRepository).overrideLogicalId(logicalId);
          }

          // Needed because the image will be pulled from ecr repository later
          repository.grantPull(task.obtainExecutionRole());
        }

        const secrets: ecs.ContainerDefinitionOptions['secrets'] = {};
        const environmentWithoutSecrets = environment || {};

        containerSecrets.forEach((s, i) => {
          if (secretsArns.has(s)) {
            secrets[s] = ecs.Secret.fromSecretsManager(ssm.Secret.fromSecretPartialArn(this, `${name}secret${i + 1}`, secretsArns.get(s)));
          }

          delete environmentWithoutSecrets[s];
        });

        const container = task.addContainer(name, {
          image: repository ? ecs.ContainerImage.fromEcrRepository(repository) : ecs.ContainerImage.fromRegistry(image),
          logging,
          environment: {
            ...taskEnvironmentVariables,
            ...environmentWithoutSecrets,
          },
          entryPoint,
          command,
          workingDirectory,
          healthCheck: healthCheck && {
            command: healthCheck.command,
            interval: cdk.Duration.seconds(healthCheck.interval ?? 30),
            retries: healthCheck.retries,
            timeout: cdk.Duration.seconds(healthCheck.timeout ?? 5),
            startPeriod: cdk.Duration.seconds(healthCheck.start_period ?? 0),
          },
          secrets,
        });

        containersInfo.push({
          container,
          repository,
        });

        // TODO: should we use hostPort too? check network mode
        portMappings?.forEach(({ containerPort, protocol, hostPort }) => {
          container.addPortMappings({
            containerPort,
            protocol: ecs.Protocol.TCP,
          });
        });
      },
    );

    const serviceSecurityGroup = new ec2.CfnSecurityGroup(this, 'ServiceSG', {
      vpcId: this.vpcId,
      groupDescription: 'Service SecurityGroup',
      securityGroupEgress: [
        {
          description: 'Allow all outbound traffic by default',
          cidrIp: '0.0.0.0/0',
          ipProtocol: '-1',
        },
      ],
      securityGroupIngress: taskPorts.map(servicePort => ({
        ipProtocol: 'tcp',
        fromPort: servicePort,
        toPort: servicePort,
        cidrIp: this.vpcCidrBlock,
      })),
    });

    let serviceRegistries: ecs.CfnService.ServiceRegistryProperty[] = undefined;

    if (cloudMapService) {
      serviceRegistries = [
        {
          containerName: exposedContainer.name,
          containerPort: exposedContainer.port,
          registryArn: cloudMapService.attrArn,
        },
      ];
    }

    const service = new ecs.CfnService(this, 'Service', {
      serviceName: `${apiName}-service-${exposedContainer.name}-${exposedContainer.port}`,
      cluster: this.clusterName,
      launchType: 'FARGATE',
      desiredCount: isInitialDeploy ? 0 : desiredCount, // This is later adjusted by the Predeploy action in the codepipeline
      networkConfiguration: {
        awsvpcConfiguration: {
          assignPublicIp: 'ENABLED',
          securityGroups: [serviceSecurityGroup.attrGroupId],
          subnets: <string[]>this.subnets,
        },
      },
      taskDefinition: task.taskDefinitionArn,
      serviceRegistries,
    });

    new cdk.CfnOutput(this, 'ServiceName', {
      value: service.serviceName,
    });

    new cdk.CfnOutput(this, 'ClusterName', {
      value: this.clusterName,
    });

    return {
      service,
      serviceSecurityGroup,
      containersInfo,
      cloudMapService,
    };
  }