# terraform-fargate

## Overview

> [!WARNING]
> Breaking changes! Starting with version 6.0.0, the module has been refactored to use a second provider for DNS records creation. It can be the same provider as the default one, but it must be explicitly defined in the module call. See [Providers](#providers) for more information.
> The variable `networking_route53_zone_id` has been removed. And the variable `route53_zone_id` has been renamed to `override_route53_zone_id` to ensure the correct zone id is used with the correct provider. 
> The value for `route53_zone_id` is set based on the environment name specified by the `var.environment` variable. For the development environment, it defaults to `Z21XEY26C989RH`, which is the zone ID for `dev.theorchard.io` in the Orchard development account. 
> For all other environments, the default is `Z0183645HDT0XCWHLW7S`, corresponding to the `theorchard.io` zone in the networking account. If you need to create records in a different account or zone, please specify the appropriate zone ID using the `override_route53_zone_id` parameter.
> In the development and shared account, you may need to use `moved` blocks to migrate existing resources. See the [migration guide](#migration-guide) for more information.

> [!WARNING]
> Breaking changes! Starting with version 5.0.0, the module has been refactored to use a second provider for DNS record creation. This provider should be defined in the same directory as the module and should use the `networking` profile. See [Providers](#providers) for more information.

This module provides functionality to create Fargate tasks and services. It can be used with web services or worker environments.

This module follows a few conventions:

- Services will be named `var.environment-var.service_name` (e.g. qa-ows-search)
- IAM roles will be named `var.environment-var.service_name-task-role`
- User-provided IAM policies will be named `var.environment-var.service_name-task-policy`
- `var.datadog_enabled` controls whether or not the SecretsManager policy allowing access to Datadog API keys is attached, and whether or not Datadog log subscriptions are automatically added. Default is true.
- Security groups will be named as follows:
  - Task security group: `var.environment-var.service_name-task-security-group`

## Resources

#### The module manages the following:

- IAM roles for task execution, tasks, and service autoscaling
- Fargate ECS clusters
- Fargate task definitions
  - Use the default or override (see [Task Definitions](#task_definitions))
- Fargate service
  - Service security groups
- Load balancers
- Logging (via Firelens/Fluentbit sidecar container)
- Autoscaling
  - Cloudwatch metrics
  - Scaling alarm thresholds
- Datadog Integration
- Route53 records

#### The module does not manage the following:

- ECR repositories (handled by Jenkins job builder)
- Deployments (handled by Jenkins job builder)
  - In normal scenario, run the service's deployment pipeline.
  - In an outage, rollback the task definition (See [Jenkins job builder](https://pipeline.theorchard.io/job/fargate-fastrack-rollback/))
- Subnets
  - This module assumes that subnets already exist and can be specified by `var.fargate_service_subnets`

## Workflow

### Providers

The module requires an explicitly defined second AWS provider to create DNS records in the shared networking account or in the same account, depending on your setup.
This provider must be defined in the same directory as the module and should use the `networking` profile if you are creating records in the networking account. If you are creating records in the same account, you can use the default provider.
The networking provider should be defined as follows:

```hcl
provider "aws" {
  region  = var.aws_region
}

provider "aws" {
  region  = var.aws_region
  alias   = "networking"
  profile = "networking"
}

data "aws_route53_zone" "networking_route53_zone" {
  provider = aws.networking
  name     = "dev.theorchard.io"
}

module "fargate" {
  source = "git@github.com:theorchard/terraform-fargate.git//?ref=4.11.0"

  providers = {
    aws.dns = aws.networking
  }
  
  override_route53_zone_id = data.aws_route53_zone.networking_route53_zone.id
  
  // other settings
}
```

alternatively, if you are creating records in the same account, you can use the default provider as follows:

```hcl
provider "aws" {
  region  = var.aws_region
}

data "aws_route53_zone" "route53_zone" {
  name     = "dev.theorchard.io"
}

module "fargate" {
  source = "git@github.com:theorchard/terraform-fargate.git//?ref=4.11.0"

  providers = {
    aws.dns = aws
  }
  
  override_route53_zone_id = data.aws_route53_zone.route53_zone.id
  
  // other settings
}
```

if you are not sure which account to create records in, please contact the DevOps team. As of now, only records in `theorchard.io` and `pdestorage.com` zones are created in the networking account.

### migration guide

#### Upgrading from version 5.x to 6.x

in the development environment and in the shared environment when you upgrade from 5.0.0 to 6.0.0, you may need to use the following `moved` block to migrate existing resources:

```hcl
moved {
    from = module.ows_podcast_fargate_environment.aws_route53_record.fargate_service_route53_record[0]
    to   = module.ows_podcast_fargate_environment.aws_route53_record.service_route53_record[0]
}
```

in order to determine if you need to use this `moved` block, check terraform plan output, if you see something like this:

```
  # module.fargate_sonarqube.aws_route53_record.fargate_service_route53_record[0] will be destroyed
  # (because aws_route53_record.fargate_service_route53_record is not in configuration)
  - resource "aws_route53_record" "fargate_service_route53_record" {
      - fqdn                             = "shared-sonar.shared.theorchard.io" -> null
      - id                               = "Z065043930H6KTNETHD5U_shared-sonar_CNAME" -> null
      - multivalue_answer_routing_policy = false -> null
      - name                             = "shared-sonar" -> null
      - records                          = [
          - "internal-shared-sonar-1671611520.us-east-1.elb.amazonaws.com",
        ] -> null
      - ttl                              = 60 -> null
      - type                             = "CNAME" -> null
      - zone_id                          = "Z065043930H6KTNETHD5U" -> null
        # (2 unchanged attributes hidden)
    }

  # module.fargate_sonarqube.aws_route53_record.service_route53_record[0] will be created
  + resource "aws_route53_record" "service_route53_record" {
      + allow_overwrite = (known after apply)
      + fqdn            = (known after apply)
      + id              = (known after apply)
      + name            = "shared-sonar"
      + records         = [
          + "internal-shared-sonar-1671611520.us-east-1.elb.amazonaws.com",
        ]
      + ttl             = 60
      + type            = "CNAME"
      + zone_id         = "Z065043930H6KTNETHD5U"
    }
````

then you need to use the `moved` block above. If you don't see this in the plan output, then you don't need to use the `moved` block.

You need to look for the resource name `aws_route53_record.fargate_service_route53_record` in the plan output being destroyed and `aws_route53_record.service_route53_record` being created.

### Variables

There are several variables that must be provided for the module to function properly:

```
* environment
* environment_variables
* service_name (must match the ECR repository name, e.g. ows-product)
```

Similarly, there are a number of variables for which you should likely provide values specific to the AWS account and region (i.e set in qa/prod):

```
* fargate_service_subnets
* vpc_id
* desired_task_count
* task_cpu
* task_memory
* maximum_capacity
* minimum_capacity
* override_route53_zone_id
* load_balancer_access_logs_s3_bucket_name
* https_listener_certificate_id
```

The default values for these variables are configured to work with the development account and don’t need to be explicitly specified. However, they should be set for QA, production, or other AWS accounts. 

By default, the module will look for a docker image from an ECR repository matching the service name. However, it is possible to override this functionality and provide a custom docker image name

```
non_ecr_image
```

It is best practice to include a version on this name, i.e. `centos:centos7`

This module will create security groups for the both the service LB and the service itself, the latter of which simply limits external access to the service to the loadbalancer. If you wish to attach custom security groups to the service, include the

```
custom_security_group_ids
```

variable and set `var.task_type` to "worker". Similarly, if you have created your own load balancer(s), reference the arn(s) in

```
additional_lb_target_group_arns
```

again, with `var.task_type = worker`.

Finally, there are variables that may vary between types of environments (e.g. web_service vs worker):

```
* container_port
* container_protocol
* datadog_enabled
* target_group_stickiness_enabled
* task_type (must be either web_service or worker)
```

See variables.tf for a full list of variables and settings.

#### WAF

For `task_type = "web_service"`, the module attaches a WAF Web ACL to the load
balancer unless `custom_waf_arn` is set. The default it resolves to depends on
the account:

* If the shared account metadata (published via `terraform-aws-accounts-map`)
  has a non-empty `default_waf_name` for the current account, that Web ACL is
  used, regardless of `blocking_waf_enabled`.
* Otherwise, it falls back to the legacy
  `${environment}-orcd-waf-${block|count-only}` naming convention, selected by
  `blocking_waf_enabled` as before.

#### <a name="Environment Variables">Environment Variables</a>

There are a few environment variables that are preset in all containers:

- DD_ENV = var.environment (cannot be changed)
- DD_SERVICE = var.service_name (cannot be changed)
- AUTH_ISSUERS
  - This has defaults that may be changed if needed. To do so, set the value of `var.auth_issuers` in your main.tf to the following structure:
  ```
  var.auth_issuers = {
    qa = "https://auth0.domain.com, https://otherauth0.domain.com"
  }
  ```
  Replace `qa` in the above example with your environment and supply the desired values. Note that the value should be a comma-separated string and not a list.

### <a name="iam">IAM</a>

In addition to the programmatically generated policies and roles used for task execution, you may also provide a JSON file containing additional IAM policies your application requires, which will be attached to the Fargate task role. In order to use this functionality, set the `${var.iam_policy_file_enabled}` variable to `true` and provide a policy file.

These policy files, which **must** be named `policies/${service_name}.json`, should be located in the same directory as the `main.tf` that defines your service. That directory structure might look like this:

```
* ows-service/
    * qa/
        * policies/
            * ows-service.json <---- this is the additional policy file for qa
        * main.tf
    * prod/
        * policies/
            * ows-service.json <---- this is the additional policy file for prod
        * main.tf
```

Additionally, you may attach existing IAM policies (i.e pre-existing policies defined outside of your code) by setting the `${var.iam_managed_policy_attachments}` variable to a list of ARNs of IAM policies you wish to attach to the Fargate task role being created.

### <a name="task_definitions">Task Definitions</a>

The provided task definition template creates a task with a single _application_ container and several sidecar containers, which should be sufficient for most applications. Alternatively, you may provide a custom task definition file by following these steps:

- Set `var.use_custom_task_definition_file` to `true`
- Set `var.task_definition_file_location` to the location of your task definition
- Make sure to create and set log groups

### Scaling

Autoscaling functionality can be tuned or modified by setting the following variables:

- `var.minimum_capacity`
- `var.maximum_capacity`
- `var.scale_in_cooldown_period`
- `var.scale_out_cooldown_period`

There are four different metrics that can be used for scaling by default, which are:
- CPU utilization
- Memory utilization
- Load balancer requests per target
- Active connection count per target

See the [autoscaling variables](https://github.com/theorchard/terraform-fargate/blob/master/variables.tf#L449) for more specific tuning options

# EFS

You may use this module in conjunction with EFS volumes in order to mount persistent storage in your task container(s). The task definition template supports use of a single EFS mountpoint inside the container. If you need multiple EFS container mount points, please supply a custom task definition file as indicated in [Task Definitions](#task_definitions).

To use this functionality, first ensure you have created an EFS volume with IAM support and a corresponding Access Point. Then set a variable called `var.docker_volumes`, which is a list of maps containing Docker volumes. That might look like this:

```
docker_volumes = [
    {
      name            = "${var.environment}-${var.service_name}"
      file_system_id  = "fs-012345"
      access_point_id = "fsap-012345"
    }
  ]
```

Then attach an IAM policy (if using terraform-efs, this is created for you) allowing IAM access to the EFS access point using the `var.iam_managed_policy_attachments` variable as detailed in [IAM](#iam)

If using the terraform-efs module, you can reference these values programmatically as shown in [this example](https://github.com/theorchard/terraform-fargate/tree/master/test/dev/with_efs/main.tf)

Additionally, the following variables have defaults that can be overridden if needed:

- `var.efs_volume_container_path`
- `var.efs_volume_in_container_is_read_only`

## Abnormal termination monitors and startup issues

You can enable reporting of abnormal terminations and startup issues to Datadog. Every time a container in a task finishes with an exit code other than 0, an event is generated and forwarded to Datadog via EventBridge.  

- `var.task_abnormal_termination_monitoring_enabled` (Optional) A boolean variable that determines if the abnormal termination monitoring functionality should be enabled. Default is false. Set to true to activate this feature.
- `var.aws_cloudwatch_event_api_destination_arn` (Optional) A string variable to specify the ARN (Amazon Resource Name) of the AWS CloudWatch Event API Destination. It's used to forward the monitoring events for further processing or integration with external systems. If not provided, the predefined value is used.

## Examples

See the `examples/` directory in terraform-infra to see examples.

Link to examples directory: https://github.com/theorchard/terraform-infra/tree/master/examples

## Caveats

Due to load balancer naming limitations, service names **cannot contain underscores or special characters**. Always use hyphens.

From https://docs.aws.amazon.com/elasticloadbalancing/2012-06-01/APIReference/API_CreateLoadBalancer.html:
`https://docs.aws.amazon.com/elasticloadbalancing/2012-06-01/APIReference/API_CreateLoadBalancer.html`

Task definition updates and service deployments should not be handled by this module; as such, the services provisioned by the module will ignore task definition updates, in order to avoid conflicts with deployment systems.
