Skip to content

Commit

Permalink
[Issue #1547] Deploy analytics ECS service (#1574)
Browse files Browse the repository at this point in the history
## Summary

Fixes #1547

### Time to review: __5 mins__

### Overview

We need to deploy the analytics service to ECS. The problem is, is that
the analytics service is entirely task-based. It is not a web server.
Which means, it is not truly a great fit for being deployed by our
current `infra/modules/service` module. Specifically, the task service
doesn't need a load balancer or any of its associated networking
components. This created a decision for me, with 3 options:

1. Make the analytics service deploy itself with a useless load
balancer. This would waste money, and would significantly confuse our
security scans.
2. Add conditionals into the existing `infra/modules/service` module to
only conditionally deploy the load balancer. This would require doing a
lot of `terraform state mv` migration since terraform resources with
`count` based conditionals have slightly different terraform state
paths.
3. Create a new module without the load balancer. This creates a large
git diff to review.

Ultimately I chose **option 3**, so that is what you see here. A new
terraform module that is simply the same ECS service module we already
had, but without a load balancer.

## My asks of reviewers

1. Do you think that option 3 was a reasonable choice here?
2. I named the module `infra/task-service`, can you think of a better
name for it?

## Changes proposed

This PR is, once again, 95% copy paste.

- `infra/analytics/service` changes were copy pasted from
`infra/api/service`
- `infra/modules/task-service` changes were copy pasted from
`infra/modules/service`

## Testing

Deployed service:

<img width="1290" alt="image"
src="https://github.com/HHS/simpler-grants-gov/assets/5768468/a4f85ece-0128-4096-b593-4c92df5acf3d">

Deployed task definition:

<img width="1297" alt="image"
src="https://github.com/HHS/simpler-grants-gov/assets/5768468/0e670642-7377-42ef-a326-102385aad1fc">
  • Loading branch information
coilysiren authored Apr 1, 2024
1 parent 306210f commit 2f9329e
Show file tree
Hide file tree
Showing 17 changed files with 794 additions and 0 deletions.
4 changes: 4 additions & 0 deletions infra/analytics/app-config/env-config/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ output "service_config" {
secrets = toset(local.secrets)
}
}

output "domain" {
value = var.domain
}
6 changes: 6 additions & 0 deletions infra/analytics/app-config/env-config/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ variable "has_database" {
default = true
}

variable "domain" {
type = string
description = "DNS domain of the website managed by HHS"
default = null
}

variable "database_instance_count" {
description = "Number of database instances. Should be 2+ for production environments."
type = number
Expand Down
25 changes: 25 additions & 0 deletions infra/analytics/service/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions infra/analytics/service/dev.s3.tfbackend
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bucket = "simpler-grants-gov-315341936575-us-east-1-tf"
key = "infra/analytics/service/dev.tfstate"
dynamodb_table = "simpler-grants-gov-315341936575-us-east-1-tf-state-locks"
region = "us-east-1"
56 changes: 56 additions & 0 deletions infra/analytics/service/image_tag.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Make the "image_tag" variable optional so that "terraform plan"
# and "terraform apply" work without any required variables.
#
# This works as follows:

# 1. Accept an optional variable during a terraform plan/apply. (see "image_tag" variable in variables.tf)

# 2. Read the output used from the last terraform state using "terraform_remote_state".
# Get the backend config by parsing the backend config file
locals {
backend_config_file_path = "${path.module}/${var.environment_name}.s3.tfbackend"
backend_config_file = file("${path.module}/${var.environment_name}.s3.tfbackend")

# Use regex to parse backend config file to get a map of variables to their
# defined values since there is no built-in terraform function that does that
#
# The backend config file consists of lines that look like
# <variable_name> = "<variable_value"
# so our regex is (\w+)\s+= "(.+)"
# Note that backslashes in the regex need to be escaped in Terraform
# so they will appear as \\ instead of \
# (see https://developer.hashicorp.com/terraform/language/functions/regex)
backend_config_regex = "(\\w+)\\s+= \"(.+)\""
backend_config = { for match in regexall(local.backend_config_regex, local.backend_config_file) : match[0] => match[1] }
tfstate_bucket = local.backend_config["bucket"]
tfstate_key = local.backend_config["key"]
}
data "terraform_remote_state" "current_image_tag" {
# Don't do a lookup if image_tag is provided explicitly.
# This saves some time and also allows us to do a first deploy,
# where the tfstate file does not yet exist.
count = var.image_tag == null ? 1 : 0
backend = "s3"

config = {
bucket = local.tfstate_bucket
key = local.tfstate_key
region = local.service_config.region
}

defaults = {
image_tag = null
}
}

# 3. Prefer the given variable if provided, otherwise default to the value from last time.
locals {
image_tag = (var.image_tag == null
? data.terraform_remote_state.current_image_tag[0].outputs.image_tag
: var.image_tag)
}

# 4. Store the final value used as a terraform output for next time.
output "image_tag" {
value = local.image_tag
}
137 changes: 137 additions & 0 deletions infra/analytics/service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc
data "aws_vpc" "network" {
filter {
name = "tag:Name"
values = [module.project_config.network_configs[var.environment_name].vpc_name]
}
}

# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.network.id]
}
filter {
name = "tag:subnet_type"
values = ["private"]
}
}

# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet
data "aws_subnets" "public" {
filter {
name = "vpc-id"
values = [data.aws_vpc.network.id]
}
filter {
name = "tag:subnet_type"
values = ["public"]
}
}

locals {
# The prefix key/value pair is used for Terraform Workspaces, which is useful for projects with multiple infrastructure developers.
# By default, Terraform creates a workspace named “default.” If a non-default workspace is not created this prefix will equal “default”,
# if you choose not to use workspaces set this value to "dev"
prefix = terraform.workspace == "default" ? "" : "${terraform.workspace}-"

# Add environment specific tags
tags = merge(module.project_config.default_tags, {
environment = var.environment_name
description = "Application resources created in ${var.environment_name} environment"
})

service_name = "${local.prefix}${module.app_config.app_name}-${var.environment_name}"

is_temporary = startswith(terraform.workspace, "t-")

environment_config = module.app_config.environment_configs[var.environment_name]
service_config = local.environment_config.service_config
database_config = local.environment_config.database_config
domain = local.environment_config.domain
}

terraform {
required_version = ">= 1.2.0, < 2.0.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.34.0"
}
}

backend "s3" {
encrypt = "true"
}
}

provider "aws" {
region = local.service_config.region
default_tags {
tags = local.tags
}
}

module "project_config" {
source = "../../project-config"
}

module "app_config" {
source = "../app-config"
}

data "aws_rds_cluster" "db_cluster" {
count = 1
cluster_identifier = local.database_config.cluster_name
}

data "aws_acm_certificate" "cert" {
count = local.domain != null ? 1 : 0
domain = local.domain
}

data "aws_iam_policy" "app_db_access_policy" {
count = 1
name = local.database_config.app_access_policy_name
}

data "aws_iam_policy" "migrator_db_access_policy" {
count = 1
name = local.database_config.migrator_access_policy_name
}

module "service" {
source = "../../modules/task-service"
service_name = local.service_name
is_temporary = false
image_repository_name = module.app_config.image_repository_name
image_tag = local.image_tag
vpc_id = data.aws_vpc.network.id
public_subnet_ids = data.aws_subnets.public.ids
private_subnet_ids = data.aws_subnets.private.ids
cpu = 1024
memory = 2048

# This is a task based service, not a web server, so we don't need to run any instances of the service at rest.
desired_instance_count = 0

cert_arn = local.domain != null ? data.aws_acm_certificate.cert[0].arn : null

db_vars = {
security_group_ids = data.aws_rds_cluster.db_cluster[0].vpc_security_group_ids
app_access_policy_arn = data.aws_iam_policy.app_db_access_policy[0].arn
migrator_access_policy_arn = data.aws_iam_policy.migrator_db_access_policy[0].arn
connection_info = {
host = data.aws_rds_cluster.db_cluster[0].endpoint
port = data.aws_rds_cluster.db_cluster[0].port
user = local.database_config.app_username
db_name = data.aws_rds_cluster.db_cluster[0].database_name
schema_name = local.database_config.schema_name
}
}

extra_environment_variables = local.service_config.extra_environment_variables
secrets = local.service_config.secrets
}
19 changes: 19 additions & 0 deletions infra/analytics/service/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "service_cluster_name" {
value = module.service.cluster_name
}

output "service_name" {
value = local.service_name
}

output "application_log_group" {
value = module.service.application_log_group
}

output "application_log_stream_prefix" {
value = module.service.application_log_stream_prefix
}

output "migrator_role_arn" {
value = module.service.migrator_role_arn
}
10 changes: 10 additions & 0 deletions infra/analytics/service/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
variable "environment_name" {
type = string
description = "name of the application environment"
}

variable "image_tag" {
type = string
description = "image tag to deploy to the environment"
default = null
}
6 changes: 6 additions & 0 deletions infra/modules/task-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# modules/task-service

This module is functionally the same module as `modules/service`, but with the load balancer and associated networking components removed.

This module (eg. `modules/task-service`) is meant for use with services that composed of individually run tasks. The modules it was based off of
(eg. `modules/service`) is meant for use with web servers.
Loading

0 comments on commit 2f9329e

Please sign in to comment.