From 8f2b65d86533b332b9b41beee859c4164f0ac3a9 Mon Sep 17 00:00:00 2001 From: Alex Lokshin Date: Wed, 15 Nov 2023 13:42:34 -0600 Subject: [PATCH] feat: Support helm chart deployments (#2723) * feat: Happy app example with Helm * Bump * Bump * Bump * commit from cli-ci -- updated coverage * Update target_group_only.tf * Update target_group_only.tf * Removed target group support * Bump * commit from ci -- ran terraform-docs and pushed * commit from ci -- ran terraform-docs and pushed * Bump * commit from ci -- ran terraform-docs and pushed * commit from ci -- ran terraform fmt and pushed * commit from ci -- ran terraform-docs and pushed * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Bump * Update charts-release.yml * Per code review --------- Co-authored-by: alexlokshin-czi Co-authored-by: github-actions[bot] Co-authored-by: czi-github-helper[bot] --- .github/workflows/charts-release.yml | 2 +- cli/COVERAGE | 2 +- cli/cmd/COVERAGE | 2 +- cli/cmd/create.go | 16 +++-- cli/cmd/push.go | 2 +- cli/cmd/update.go | 2 +- examples/integration_test_helm/.gitignore | 2 + .../integration_test_helm/.happy/config.json | 54 +++++++++++++++ .../.happy/terraform/envs/rdev/main.tf | 55 +++++++++++++++ .../.happy/terraform/envs/rdev/outputs.tf | 15 ++++ .../.happy/terraform/envs/rdev/providers.tf | 52 ++++++++++++++ .../.happy/terraform/envs/rdev/variables.tf | 35 ++++++++++ .../.happy/terraform/envs/rdev/versions.tf | 22 ++++++ examples/integration_test_helm/Dockerfile | 3 + examples/integration_test_helm/README.md | 13 ++++ .../integration_test_helm/docker-compose.yml | 11 +++ .../integration_test_helm/src/api/.gitignore | 1 + .../integration_test_helm/src/api/Dockerfile | 20 ++++++ examples/integration_test_helm/src/api/go.mod | 19 +++++ examples/integration_test_helm/src/api/go.sum | 28 ++++++++ .../integration_test_helm/src/api/main.go | 37 ++++++++++ shared/config/happy_config.go | 6 +- shared/hclmanager/hcl_manager.go | 9 ++- terraform/modules/happy-stack-eks/README.md | 4 +- terraform/modules/happy-stack-eks/main.tf | 2 +- .../modules/happy-stack-eks/variables.tf | 18 ++--- .../modules/happy-stack-helm-eks/README.md | 5 +- .../modules/happy-stack-helm-eks/locals.tf | 2 +- .../modules/happy-stack-helm-eks/main.tf | 61 ++++++++-------- .../happy-stack-helm-eks/target_group_only.tf | 25 ------- .../target_group_only/README.md | 44 ------------ .../target_group_only/main.tf | 45 ------------ .../target_group_only/output.tf | 3 - .../target_group_only/variables.tf | 69 ------------------- .../target_group_only/versions.tf | 13 ---- .../modules/happy-stack-helm-eks/variables.tf | 6 ++ 36 files changed, 443 insertions(+), 262 deletions(-) create mode 100644 examples/integration_test_helm/.gitignore create mode 100644 examples/integration_test_helm/.happy/config.json create mode 100644 examples/integration_test_helm/.happy/terraform/envs/rdev/main.tf create mode 100644 examples/integration_test_helm/.happy/terraform/envs/rdev/outputs.tf create mode 100644 examples/integration_test_helm/.happy/terraform/envs/rdev/providers.tf create mode 100644 examples/integration_test_helm/.happy/terraform/envs/rdev/variables.tf create mode 100644 examples/integration_test_helm/.happy/terraform/envs/rdev/versions.tf create mode 100644 examples/integration_test_helm/Dockerfile create mode 100644 examples/integration_test_helm/README.md create mode 100644 examples/integration_test_helm/docker-compose.yml create mode 100644 examples/integration_test_helm/src/api/.gitignore create mode 100644 examples/integration_test_helm/src/api/Dockerfile create mode 100644 examples/integration_test_helm/src/api/go.mod create mode 100644 examples/integration_test_helm/src/api/go.sum create mode 100644 examples/integration_test_helm/src/api/main.go delete mode 100644 terraform/modules/happy-stack-helm-eks/target_group_only.tf delete mode 100644 terraform/modules/happy-stack-helm-eks/target_group_only/README.md delete mode 100644 terraform/modules/happy-stack-helm-eks/target_group_only/main.tf delete mode 100644 terraform/modules/happy-stack-helm-eks/target_group_only/output.tf delete mode 100644 terraform/modules/happy-stack-helm-eks/target_group_only/variables.tf delete mode 100644 terraform/modules/happy-stack-helm-eks/target_group_only/versions.tf diff --git a/.github/workflows/charts-release.yml b/.github/workflows/charts-release.yml index 9cb237856a..8be97e76a1 100644 --- a/.github/workflows/charts-release.yml +++ b/.github/workflows/charts-release.yml @@ -94,7 +94,7 @@ jobs: ${HELM_CMD} repo index ${PACKAGE_DIR} --url ${CHART_DOWNLOAD_URL} --merge ${INDEX_DIR}/index.yaml cd ../chart-repo - mv ${PACKAGE_DIR}/index.yaml .${INDEX_DIR}/index.yaml + mv ${INDEX_DIR}/index.yaml ./index.yaml git add -A git commit -m "chore: publish charts from ${{ github.repository }} ${{ needs.check-chart-released.outputs.tag }}" git push diff --git a/cli/COVERAGE b/cli/COVERAGE index e70b3aebd5..676df72173 100644 --- a/cli/COVERAGE +++ b/cli/COVERAGE @@ -1 +1 @@ -22.12 \ No newline at end of file +22.10 \ No newline at end of file diff --git a/cli/cmd/COVERAGE b/cli/cmd/COVERAGE index ae27ab17be..37b64dadd4 100644 --- a/cli/cmd/COVERAGE +++ b/cli/cmd/COVERAGE @@ -1 +1 @@ -11.7 \ No newline at end of file +11.6 \ No newline at end of file diff --git a/cli/cmd/create.go b/cli/cmd/create.go index 3ca5291f5b..eaa40a4e43 100644 --- a/cli/cmd/create.go +++ b/cli/cmd/create.go @@ -69,9 +69,6 @@ var createCmd = &cobra.Command{ RunE: runCreate, } -// keep in sync with happy-stack-eks terraform module -const terraformECRTargetPathTemplate = `module.stack.module.services["%s"].module.ecr` - func runCreate( cmd *cobra.Command, args []string, @@ -90,7 +87,7 @@ func runCreate( validateStackNameGloballyAvailable(ctx, happyClient.StackService, stackName, force), validateTFEBackLog(ctx, happyClient.AWSBackend), validateStackExistsCreate(ctx, stackName, happyClient, message), - validateECRExists(ctx, stackName, terraformECRTargetPathTemplate, happyClient, message), + validateECRExists(ctx, stackName, happyClient, message), validateImageExists(ctx, createTag, skipCheckTag, imageSrcEnv, imageSrcStack, imageSrcRoleArn, happyClient, cmd.Flags().Changed(config.FlagAWSProfile)), ) if err != nil { @@ -115,7 +112,10 @@ func runCreate( return nil } -func validateECRExists(ctx context.Context, stackName string, ecrTargetPathFormat string, happyClient *HappyClient, options ...workspace_repo.TFERunOption) validation { +// keep in sync with happy-stack-eks and happy-stack-helm-eks terraform module +var ecrTargetPathFormats = []string{`module.stack.module.services["%s"].module.ecr`, `module.stack.module.ecr["%s"]`} + +func validateECRExists(ctx context.Context, stackName string, happyClient *HappyClient, options ...workspace_repo.TFERunOption) validation { log.Debug("Scheduling validateECRExists()") return func() error { log.Debug("Running validateECRExists()") @@ -141,7 +141,9 @@ func validateECRExists(ctx context.Context, stackName string, ecrTargetPathForma log.Debugf("missing ECRs for the following services %s. making them now", strings.Join(missingServiceECRs, ",")) targetAddrs := []string{} for _, service := range happyClient.HappyConfig.GetServices() { - targetAddrs = append(targetAddrs, fmt.Sprintf(ecrTargetPathFormat, service)) + for _, ecrTargetPathFormat := range ecrTargetPathFormats { + targetAddrs = append(targetAddrs, fmt.Sprintf(ecrTargetPathFormat, service)) + } } stack, err := happyClient.StackService.GetStack(ctx, stackName) if err != nil { @@ -160,6 +162,8 @@ func validateECRExists(ctx context.Context, stackName string, ecrTargetPathForma tfDirPath := happyClient.HappyConfig.TerraformDirectory() happyProjectRoot := happyClient.HappyConfig.GetProjectRoot() srcDir := filepath.Join(happyProjectRoot, tfDirPath) + + log.Debugf("Adding ECRs to stack %s, terraform targets: %s", stack.Name, strings.Join(targetAddrs, ",")) return stack.Apply(ctx, srcDir, makeWaitOptions(stackName, happyClient.HappyConfig, happyClient.AWSBackend), append(options, workspace_repo.TargetAddrs(targetAddrs))...) } } diff --git a/cli/cmd/push.go b/cli/cmd/push.go index 779f8cb07c..587a65e979 100644 --- a/cli/cmd/push.go +++ b/cli/cmd/push.go @@ -53,7 +53,7 @@ var pushCmd = &cobra.Command{ validateGitTree(happyClient.HappyConfig.GetProjectRoot()), validateStackNameAvailable(ctx, happyClient.StackService, stackName, force), validateStackExistsCreate(ctx, stackName, happyClient), - validateECRExists(ctx, stackName, terraformECRTargetPathTemplate, happyClient), + validateECRExists(ctx, stackName, happyClient), ) if err != nil { return errors.Wrap(err, "failed one of the happy client validations") diff --git a/cli/cmd/update.go b/cli/cmd/update.go index 71e59e6778..078d9395d6 100644 --- a/cli/cmd/update.go +++ b/cli/cmd/update.go @@ -78,7 +78,7 @@ func runUpdate(cmd *cobra.Command, args []string) error { validateStackNameAvailable(ctx, happyClient.StackService, stackName, force), validateTFEBackLog(ctx, happyClient.AWSBackend), validateStackExistsUpdate(ctx, stackName, happyClient), - validateECRExists(ctx, stackName, terraformECRTargetPathTemplate, happyClient), + validateECRExists(ctx, stackName, happyClient), validateImageExists(ctx, createTag, skipCheckTag, imageSrcEnv, imageSrcStack, imageSrcRoleArn, happyClient, cmd.Flags().Changed(config.FlagAWSProfile)), ) if err != nil { diff --git a/examples/integration_test_helm/.gitignore b/examples/integration_test_helm/.gitignore new file mode 100644 index 0000000000..554ddb9f1d --- /dev/null +++ b/examples/integration_test_helm/.gitignore @@ -0,0 +1,2 @@ +.happy/terraform/envs/*/.terraform/ +.happy/terraform/envs/*/.terraform.lock.hcl diff --git a/examples/integration_test_helm/.happy/config.json b/examples/integration_test_helm/.happy/config.json new file mode 100644 index 0000000000..e991385f68 --- /dev/null +++ b/examples/integration_test_helm/.happy/config.json @@ -0,0 +1,54 @@ +{ + "config_version": "v3", + "default_env": "rdev", + "app": "integration-test-helm", + "default_compose_env_file": ".env.ecr", + "environments": { + "rdev": { + "aws_profile": "czi-playground", + "aws_region": "us-west-2", + "k8s": { + "namespace": "si-rdev-happy-eks-rdev-happy-env", + "cluster_id": "si-playground-eks-v2", + "auth_method": "eks", + "context": "si-playground-eks-v2" + }, + "terraform_directory": ".happy/terraform/envs/rdev", + "task_launch_type": "k8s" + } + }, + "slice_default_tag": "branch-main", + "services": [ + "frontend" + ], + "features": { + "enable_dynamo_locking": true, + "enable_ecr_auto_creation": true + }, + "api": {}, + "stack_defaults": { + "routing_method": "CONTEXT", + "services": { + "frontend": { + "build": { + "context": "/Users/alokshin/GitHub/chanzuckerberg/happy/examples/integration_test_helm/src/api", + "dockerfile": "Dockerfile" + }, + "cpu": "100m", + "desired_count": 1, + "health_check_path": "/health", + "max_count": 1, + "memory": "100Mi", + "name": "frontend", + "path": "/*", + "platform_architecture": "arm64", + "port": 3000, + "scaling_cpu_threshold_percentage": 50, + "scan_on_push": true, + "service_type": "INTERNAL", + "tag_mutability": false + } + }, + "source": "git@github.com:chanzuckerberg/happy//terraform/modules/happy-stack-helm-eks?ref=main" + } +} \ No newline at end of file diff --git a/examples/integration_test_helm/.happy/terraform/envs/rdev/main.tf b/examples/integration_test_helm/.happy/terraform/envs/rdev/main.tf new file mode 100644 index 0000000000..569b3ba37b --- /dev/null +++ b/examples/integration_test_helm/.happy/terraform/envs/rdev/main.tf @@ -0,0 +1,55 @@ +module "stack" { + source = "git@github.com:chanzuckerberg/happy//terraform/modules/happy-stack-helm-eks?ref=main" + + image_tag = var.image_tag + image_tags = jsondecode(var.image_tags) + stack_name = var.stack_name + deployment_stage = "rdev" + + stack_prefix = "/${var.stack_name}" + k8s_namespace = var.k8s_namespace + + // this allow these services under the same domain + // each service is reachable via their path configured below + routing_method = "CONTEXT" + + services = { + frontend = { + name = "frontend" + desired_count = 10 + // the maximum number of copies of this service it can autoscale to + max_count = 50 + // the signal used to trigger autoscaling (ie. 50% of CPU means scale up) + scaling_cpu_threshold_percentage = 50 + // the port the service is running on + port = 3000 + memory = "500Mi" + memory_requests = "300Mi" + cpu = "500m" + cpu_requests = "500m" + // an endpoint that returns a 200. Your service will not start if this endpoint is not healthy + health_check_path = "/health" + // oneof: INTERNAL, EXTERNAL, PRIVATE, TARGET_GROUP_ONLY, IMAGE_TEMPLATE + // INTERNAL: OIDC protected endpoints + // EXTERNAL: internet accessible + // PRIVATE: only accessible within the cluster + // TARGET_GROUP_ONLY: attach to an existing ALB rather than making a new one + // IMAGE_TEMPLATE: don't deploy any services, just use to create and push images + service_type = "INTERNAL" + // the path to reach this search + path = "/*" + // the platform architecture of the container. this should match what is in + // the platform attribute of your docker-compose.yml file for your service. + // oneof: amd64, arm64. + // Try to always select arm since it comes with a lot of cost savings and performance + // benefits and has little to no impact on developers. + platform_architecture = "arm64" + scan_on_push = true + tag_mutability = false + } + } + + // tasks can be utilized to run post-deployment tasks such as database migrations or deletions + tasks = { + } +} diff --git a/examples/integration_test_helm/.happy/terraform/envs/rdev/outputs.tf b/examples/integration_test_helm/.happy/terraform/envs/rdev/outputs.tf new file mode 100644 index 0000000000..edf8462fcf --- /dev/null +++ b/examples/integration_test_helm/.happy/terraform/envs/rdev/outputs.tf @@ -0,0 +1,15 @@ +output "service_urls" { + value = module.stack.service_endpoints + description = "The URL endpoint for the frontend website service" + sensitive = false +} + +output "service_ecrs" { + value = module.stack.service_ecrs + description = "The services ECR locations for their docker containers" + sensitive = false +} + +output "k8s_namespace" { + value = data.kubernetes_namespace.happy-namespace.metadata.0.name +} diff --git a/examples/integration_test_helm/.happy/terraform/envs/rdev/providers.tf b/examples/integration_test_helm/.happy/terraform/envs/rdev/providers.tf new file mode 100644 index 0000000000..fc219e50b2 --- /dev/null +++ b/examples/integration_test_helm/.happy/terraform/envs/rdev/providers.tf @@ -0,0 +1,52 @@ +provider "aws" { + region = "us-west-2" + assume_role { + role_arn = "arn:aws:iam::${var.aws_account_id}:role/${var.aws_role}" + } + allowed_account_ids = [var.aws_account_id] +} + +provider "aws" { + alias = "czi-si" + region = "us-west-2" + + assume_role { + role_arn = "arn:aws:iam::626314663667:role/tfe-si" + } + + allowed_account_ids = ["626314663667"] +} + +data "aws_eks_cluster" "cluster" { + name = var.k8s_cluster_id +} + +data "aws_eks_cluster_auth" "cluster" { + name = var.k8s_cluster_id +} + +provider "kubernetes" { + host = data.aws_eks_cluster.cluster.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) + token = data.aws_eks_cluster_auth.cluster.token +} + +data "kubernetes_namespace" "happy-namespace" { + metadata { + name = var.k8s_namespace + } +} + +data "aws_ssm_parameter" "dd_app_key" { + name = "/shared-infra-prod-datadog/app_key" + provider = aws.czi-si +} +data "aws_ssm_parameter" "dd_api_key" { + name = "/shared-infra-prod-datadog/api_key" + provider = aws.czi-si +} + +provider "datadog" { + app_key = data.aws_ssm_parameter.dd_app_key.value + api_key = data.aws_ssm_parameter.dd_api_key.value +} diff --git a/examples/integration_test_helm/.happy/terraform/envs/rdev/variables.tf b/examples/integration_test_helm/.happy/terraform/envs/rdev/variables.tf new file mode 100644 index 0000000000..85627774a4 --- /dev/null +++ b/examples/integration_test_helm/.happy/terraform/envs/rdev/variables.tf @@ -0,0 +1,35 @@ +variable "aws_account_id" { + type = string + description = "AWS account ID to apply changes to" +} + +variable "k8s_cluster_id" { + type = string + description = "EKS K8S Cluster ID" +} + +variable "k8s_namespace" { + type = string + description = "K8S namespace for this stack" +} + +variable "aws_role" { + type = string + description = "Name of the AWS role to assume to apply changes" +} + +variable "image_tag" { + type = string + description = "Please provide an image tag" +} + +variable "image_tags" { + type = string + description = "Override the default image tags (json-encoded map)" + default = "{}" +} + +variable "stack_name" { + type = string + description = "Happy Path stack name" +} diff --git a/examples/integration_test_helm/.happy/terraform/envs/rdev/versions.tf b/examples/integration_test_helm/.happy/terraform/envs/rdev/versions.tf new file mode 100644 index 0000000000..af454fc035 --- /dev/null +++ b/examples/integration_test_helm/.happy/terraform/envs/rdev/versions.tf @@ -0,0 +1,22 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.14" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.16" + } + datadog = { + source = "datadog/datadog" + version = ">= 3.20.0" + } + happy = { + source = "chanzuckerberg/happy" + version = ">= 0.53.5" + } + } + required_version = ">= 1.3" +} + diff --git a/examples/integration_test_helm/Dockerfile b/examples/integration_test_helm/Dockerfile new file mode 100644 index 0000000000..77ab41383d --- /dev/null +++ b/examples/integration_test_helm/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:1.23-alpine +EXPOSE 3000 +CMD ["/bin/sh", "-c", "sed -i 's/listen .*/listen 3000;/g' /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"] diff --git a/examples/integration_test_helm/README.md b/examples/integration_test_helm/README.md new file mode 100644 index 0000000000..590923f889 --- /dev/null +++ b/examples/integration_test_helm/README.md @@ -0,0 +1,13 @@ +# Integration test app with Helm + +An integration test application deployed using happy path. + +## Prerequistes + +* Install the latest version of happy: `brew tap chanzuckerberg/tap && brew install happy` +* Make sure you have access to the czi-playground AWS environment + +## Notes + +* All stacks in this examples folder will be automatically cleaned up within 24 hours of creation; it is not intended for production usage +* All stacks are created in the czi-playground environment; all CZI employees should have access to this environmnet \ No newline at end of file diff --git a/examples/integration_test_helm/docker-compose.yml b/examples/integration_test_helm/docker-compose.yml new file mode 100644 index 0000000000..42f51f2bc9 --- /dev/null +++ b/examples/integration_test_helm/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.8" + +services: + frontend: + image: "frontend" + profiles: [ "*" ] + platform: linux/arm64 + build: + context: src/api + dockerfile: Dockerfile + diff --git a/examples/integration_test_helm/src/api/.gitignore b/examples/integration_test_helm/src/api/.gitignore new file mode 100644 index 0000000000..9e5bfb42d2 --- /dev/null +++ b/examples/integration_test_helm/src/api/.gitignore @@ -0,0 +1 @@ +api \ No newline at end of file diff --git a/examples/integration_test_helm/src/api/Dockerfile b/examples/integration_test_helm/src/api/Dockerfile new file mode 100644 index 0000000000..d957783247 --- /dev/null +++ b/examples/integration_test_helm/src/api/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.20-alpine AS builder +WORKDIR /app +RUN apk update && apk upgrade +RUN apk --update add --no-cache git tzdata +ADD . . +RUN GOPROXY=direct go build -o api + +# This artificially adds high level vulnerabilities for testing purposes +FROM alpine:3.9 +WORKDIR /app +RUN apk update && apk upgrade && apk --no-cache add curl +# # Uncomment the statement below to detect vulnerabilities +# RUN apk add --no-cache git make gcc g++ libc-dev pkgconfig \ +# libxml2-dev libxslt-dev postgresql-dev coreutils curl wget bash \ +# gnupg tar linux-headers bison readline-dev readline zlib-dev \ +# zlib yaml-dev autoconf ncurses-dev curl-dev apache2-dev \ +# libx11-dev libffi-dev tcl-dev tk-dev openjdk8 +COPY --from=builder /app/api /app/ +EXPOSE 3000 +ENTRYPOINT ./api \ No newline at end of file diff --git a/examples/integration_test_helm/src/api/go.mod b/examples/integration_test_helm/src/api/go.mod new file mode 100644 index 0000000000..c68f5b9e3b --- /dev/null +++ b/examples/integration_test_helm/src/api/go.mod @@ -0,0 +1,19 @@ +module api + +go 1.21 + +require github.com/gofiber/fiber/v2 v2.50.0 + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.13.0 // indirect +) diff --git a/examples/integration_test_helm/src/api/go.sum b/examples/integration_test_helm/src/api/go.sum new file mode 100644 index 0000000000..a4aacd7f84 --- /dev/null +++ b/examples/integration_test_helm/src/api/go.sum @@ -0,0 +1,28 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= +github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/examples/integration_test_helm/src/api/main.go b/examples/integration_test_helm/src/api/main.go new file mode 100644 index 0000000000..a0f5276db3 --- /dev/null +++ b/examples/integration_test_helm/src/api/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "net/http" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +type Response struct { + Status string + Service string + Complete bool +} + +func main() { + app := fiber.New(fiber.Config{ + ReadTimeout: 60 * time.Second, + ReadBufferSize: 1024 * 64, + }) + + app.Use(logger.New(logger.Config{ + // For more options, see the Config section + Format: "${pid} ${locals:requestid} ${status} - ${method} ${path} ${reqHeaders}​\n", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Status(http.StatusOK).JSON(Response{Status: "OK", Service: "frontend"}) + }) + + app.Get("/health", func(c *fiber.Ctx) error { + return c.Status(http.StatusOK).JSON(Response{Status: "Health", Service: "frontend"}) + }) + + app.Listen(":3000") +} diff --git a/shared/config/happy_config.go b/shared/config/happy_config.go index 8445c5882d..b1105b1726 100644 --- a/shared/config/happy_config.go +++ b/shared/config/happy_config.go @@ -443,11 +443,11 @@ func (s *HappyConfig) GetModuleSource() string { return moduleSource } -func (s *HappyConfig) GetModuleName() string { +func (s *HappyConfig) GetModuleNames() map[string]bool { if s.TaskLaunchType() == util.LaunchTypeK8S { - return "happy-stack-eks" + return map[string]bool{"happy-stack-eks": true, "happy-stack-helm-eks": true} } else { - return "happy-stack-ecs" + return map[string]bool{"happy-stack-ecs": true} } } diff --git a/shared/hclmanager/hcl_manager.go b/shared/hclmanager/hcl_manager.go index 73c9107dcb..f64206450f 100644 --- a/shared/hclmanager/hcl_manager.go +++ b/shared/hclmanager/hcl_manager.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-config-inspect/tfconfig" errs "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" ) type HclManager struct { @@ -168,9 +169,11 @@ func (h HclManager) Validate(ctx context.Context) error { if err != nil { return errs.Wrapf(err, "unable to parse module source for environment '%s'", moduleSource) } - expectedModuleName := fmt.Sprintf("terraform/modules/%s", h.HappyConfig.GetModuleName()) - if moduleName != expectedModuleName { - return errs.Errorf("module name '%s' does not match, expected '%s'", moduleName, expectedModuleName) + + moduleName = strings.TrimPrefix(moduleName, "terraform/modules/") + expectedModuleNames := h.HappyConfig.GetModuleNames() + if _, ok := expectedModuleNames[moduleName]; !ok { + return errs.Errorf("module name '%s' does not match, expected '%v'", moduleName, strings.Join(maps.Keys(expectedModuleNames), ",")) } } return nil diff --git a/terraform/modules/happy-stack-eks/README.md b/terraform/modules/happy-stack-eks/README.md index 7b6ea4b762..d5df7cf7de 100644 --- a/terraform/modules/happy-stack-eks/README.md +++ b/terraform/modules/happy-stack-eks/README.md @@ -60,11 +60,11 @@ | [image\_tags](#input\_image\_tags) | Override image tag for each docker image | `map(string)` | `{}` | no | | [k8s\_namespace](#input\_k8s\_namespace) | K8S namespace for this stack | `string` | n/a | yes | | [routing\_method](#input\_routing\_method) | Traffic routing method for this stack. Valid options are 'DOMAIN', when every service gets a unique domain name, or a 'CONTEXT' when all services share the same domain name, and routing is done by request path. | `string` | `"DOMAIN"` | no | -| [services](#input\_services) | The services you want to deploy as part of this stack. |
map(object({
name : string,
service_type : optional(string, "INTERNAL"),
allow_mesh_services : optional(list(object({
service : optional(string, null),
stack : optional(string, null),
service_account_name : optional(string, null)
})), null),
ingress_security_groups : optional(list(string), []), // Only used for VPC service_type
alb : optional(object({
name : string,
listener_port : number,
}), null), // Only used for TARGET_GROUP_ONLY
desired_count : optional(number, 2),
max_count : optional(number, 2),
max_unavailable_count : optional(string, "1"),
scaling_cpu_threshold_percentage : optional(number, 80),
port : optional(number, 80),
scheme : optional(string, "HTTP"),
cmd : optional(list(string), []),
args : optional(list(string), []),
image_pull_policy : optional(string, "IfNotPresent"), // Supported values: IfNotPresent, Always, Never
tag_mutability : optional(bool, true),
scan_on_push : optional(bool, false),
service_port : optional(number, null),
service_scheme : optional(string, "HTTP"),
memory : optional(string, "100Mi"),
memory_requests : optional(string, "100Mi"),
cpu : optional(string, "100m"),
cpu_requests : optional(string, "100m"),
gpu : optional(number, null), // Whole number of GPUs to request, 0 will schedule all available GPUs. Requires GPU-enabled nodes in the cluster, `k8s-device-plugin` installed, platform_architecture = "amd64", and additional_node_selectors = { "nvidia.com/gpu.present" = "true" } present.
health_check_path : optional(string, "/"),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
path : optional(string, "/*"), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
priority : optional(number, 0), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
success_codes : optional(string, "200-499"),
synthetics : optional(bool, false),
initial_delay_seconds : optional(number, 30),
alb_idle_timeout : optional(number, 60) // in seconds
period_seconds : optional(number, 3),
liveness_timeout_seconds : optional(number, 30),
readiness_timeout_seconds : optional(number, 30),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64; GPU nodes are amd64 only.
additional_node_selectors : optional(map(string), {}), // For GPU use: { "nvidia.com/gpu.present" = "true" }
bypasses : optional(map(object({ // Only used for INTERNAL service_type
paths = optional(set(string), [])
methods = optional(set(string), [])
})), {})
sidecars : optional(map(object({
image : string
tag : string
port : optional(number, 80),
scheme : optional(string, "HTTP"),
memory : optional(string, "100Mi")
cpu : optional(string, "100m")
image_pull_policy : optional(string, "IfNotPresent") // Supported values: IfNotPresent, Always, Never
health_check_path : optional(string, "/")
initial_delay_seconds : optional(number, 30),
period_seconds : optional(number, 3),
liveness_timeout_seconds : optional(number, 30),
readiness_timeout_seconds : optional(number, 30),
})), {})
additional_env_vars : optional(map(string), {}),
}))
| n/a | yes | +| [services](#input\_services) | The services you want to deploy as part of this stack. |
map(object({
name : string,
service_type : optional(string, "INTERNAL"),
allow_mesh_services : optional(list(object({
service : optional(string, null),
stack : optional(string, null),
service_account_name : optional(string, null)
})), null),
ingress_security_groups : optional(list(string), []), // Only used for VPC service_type
alb : optional(object({
name : string,
listener_port : number,
}), null), // Only used for TARGET_GROUP_ONLY
desired_count : optional(number, 2),
max_count : optional(number, 5),
max_unavailable_count : optional(string, "1"),
scaling_cpu_threshold_percentage : optional(number, 80),
port : optional(number, 80),
scheme : optional(string, "HTTP"),
cmd : optional(list(string), []),
args : optional(list(string), []),
image_pull_policy : optional(string, "IfNotPresent"), // Supported values: IfNotPresent, Always, Never
tag_mutability : optional(bool, true),
scan_on_push : optional(bool, false),
service_port : optional(number, null),
service_scheme : optional(string, "HTTP"),
memory : optional(string, "500Mi"),
memory_requests : optional(string, "200Mi"),
cpu : optional(string, "1"),
cpu_requests : optional(string, "500m"),
gpu : optional(number, null), // Whole number of GPUs to request, 0 will schedule all available GPUs. Requires GPU-enabled nodes in the cluster, `k8s-device-plugin` installed, platform_architecture = "amd64", and additional_node_selectors = { "nvidia.com/gpu.present" = "true" } present.
health_check_path : optional(string, "/"),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
path : optional(string, "/*"), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
priority : optional(number, 0), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
success_codes : optional(string, "200-499"),
synthetics : optional(bool, false),
initial_delay_seconds : optional(number, 30),
alb_idle_timeout : optional(number, 60) // in seconds
period_seconds : optional(number, 3),
liveness_timeout_seconds : optional(number, 30),
readiness_timeout_seconds : optional(number, 30),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64; GPU nodes are amd64 only.
additional_node_selectors : optional(map(string), {}), // For GPU use: { "nvidia.com/gpu.present" = "true" }
bypasses : optional(map(object({ // Only used for INTERNAL service_type
paths = optional(set(string), [])
methods = optional(set(string), [])
})), {})
sidecars : optional(map(object({
image : string
tag : string
port : optional(number, 80),
scheme : optional(string, "HTTP"),
memory : optional(string, "200Mi")
cpu : optional(string, "500m")
image_pull_policy : optional(string, "IfNotPresent") // Supported values: IfNotPresent, Always, Never
health_check_path : optional(string, "/")
initial_delay_seconds : optional(number, 30),
period_seconds : optional(number, 3),
liveness_timeout_seconds : optional(number, 30),
readiness_timeout_seconds : optional(number, 30),
})), {})
additional_env_vars : optional(map(string), {}),
}))
| n/a | yes | | [skip\_config\_injection](#input\_skip\_config\_injection) | Skip injecting app configs into the services / tasks | `bool` | `false` | no | | [stack\_name](#input\_stack\_name) | Happy Path stack name | `string` | n/a | yes | | [stack\_prefix](#input\_stack\_prefix) | Do bucket storage paths and db schemas need to be prefixed with the stack name? (Usually '/{stack\_name}' for dev stacks, and '' for staging/prod stacks) | `string` | `""` | no | -| [tasks](#input\_tasks) | The deletion/migration tasks you want to run when a stack comes up and down. |
map(object({
image : string,
memory : optional(string, "10Mi"),
cpu : optional(string, "10m"),
cmd : optional(list(string), []),
args : optional(list(string), []),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64
is_cron_job : optional(bool, false),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
cron_schedule : optional(string, "0 0 1 1 *"),
additional_env_vars : optional(map(string), {}),
}))
| `{}` | no | +| [tasks](#input\_tasks) | The deletion/migration tasks you want to run when a stack comes up and down. |
map(object({
image : string,
memory : optional(string, "200Mi"),
cpu : optional(string, "500m"),
cmd : optional(list(string), []),
args : optional(list(string), []),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64
is_cron_job : optional(bool, false),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
cron_schedule : optional(string, "0 0 1 1 *"),
additional_env_vars : optional(map(string), {}),
}))
| `{}` | no | ## Outputs diff --git a/terraform/modules/happy-stack-eks/main.tf b/terraform/modules/happy-stack-eks/main.tf index 7c9cb3437a..747e780b5d 100644 --- a/terraform/modules/happy-stack-eks/main.tf +++ b/terraform/modules/happy-stack-eks/main.tf @@ -72,7 +72,7 @@ locals { replace(v.image, "/{(${join("|", keys(local.service_ecrs))})}/", "%s"), [ for repo in flatten(regexall("{(${join("|", keys(local.service_ecrs))})}", v.image)) : - lookup(local.service_ecrs, repo) + lookup(local.service_ecrs, repo, "") ]... ) }) } diff --git a/terraform/modules/happy-stack-eks/variables.tf b/terraform/modules/happy-stack-eks/variables.tf index 27ad09fa37..c35c3a89d9 100644 --- a/terraform/modules/happy-stack-eks/variables.tf +++ b/terraform/modules/happy-stack-eks/variables.tf @@ -57,7 +57,7 @@ variable "services" { listener_port : number, }), null), // Only used for TARGET_GROUP_ONLY desired_count : optional(number, 2), - max_count : optional(number, 2), + max_count : optional(number, 5), max_unavailable_count : optional(string, "1"), scaling_cpu_threshold_percentage : optional(number, 80), port : optional(number, 80), @@ -69,10 +69,10 @@ variable "services" { scan_on_push : optional(bool, false), service_port : optional(number, null), service_scheme : optional(string, "HTTP"), - memory : optional(string, "100Mi"), - memory_requests : optional(string, "100Mi"), - cpu : optional(string, "100m"), - cpu_requests : optional(string, "100m"), + memory : optional(string, "500Mi"), + memory_requests : optional(string, "200Mi"), + cpu : optional(string, "1"), + cpu_requests : optional(string, "500m"), gpu : optional(number, null), // Whole number of GPUs to request, 0 will schedule all available GPUs. Requires GPU-enabled nodes in the cluster, `k8s-device-plugin` installed, platform_architecture = "amd64", and additional_node_selectors = { "nvidia.com/gpu.present" = "true" } present. health_check_path : optional(string, "/"), aws_iam : optional(object({ @@ -99,8 +99,8 @@ variable "services" { tag : string port : optional(number, 80), scheme : optional(string, "HTTP"), - memory : optional(string, "100Mi") - cpu : optional(string, "100m") + memory : optional(string, "200Mi") + cpu : optional(string, "500m") image_pull_policy : optional(string, "IfNotPresent") // Supported values: IfNotPresent, Always, Never health_check_path : optional(string, "/") initial_delay_seconds : optional(number, 30), @@ -182,8 +182,8 @@ variable "services" { variable "tasks" { type = map(object({ image : string, - memory : optional(string, "10Mi"), - cpu : optional(string, "10m"), + memory : optional(string, "200Mi"), + cpu : optional(string, "500m"), cmd : optional(list(string), []), args : optional(list(string), []), platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64 diff --git a/terraform/modules/happy-stack-helm-eks/README.md b/terraform/modules/happy-stack-helm-eks/README.md index beb4c7a18b..1d92deef36 100644 --- a/terraform/modules/happy-stack-helm-eks/README.md +++ b/terraform/modules/happy-stack-helm-eks/README.md @@ -28,7 +28,6 @@ | Name | Source | Version | |------|--------|---------| | [ecr](#module\_ecr) | git@github.com:chanzuckerberg/cztack//aws-ecr-repo | v0.59.0 | -| [target\_group\_only](#module\_target\_group\_only) | ./target_group_only | n/a | ## Resources @@ -60,11 +59,11 @@ | [image\_tags](#input\_image\_tags) | Override image tag for each docker image | `map(string)` | `{}` | no | | [k8s\_namespace](#input\_k8s\_namespace) | K8S namespace for this stack | `string` | n/a | yes | | [routing\_method](#input\_routing\_method) | Traffic routing method for this stack. Valid options are 'DOMAIN', when every service gets a unique domain name, or a 'CONTEXT' when all services share the same domain name, and routing is done by request path. | `string` | `"DOMAIN"` | no | -| [services](#input\_services) | The services you want to deploy as part of this stack. |
map(object({
name : string,
service_type : optional(string, "INTERNAL"),
allow_mesh_services : optional(list(object({
service : optional(string, null),
stack : optional(string, null),
service_account_name : optional(string, null)
})), null),
ingress_security_groups : optional(list(string), []), // Only used for VPC service_type
alb : optional(object({
name : string,
listener_port : number,
}), null), // Only used for TARGET_GROUP_ONLY
desired_count : optional(number, 2),
max_count : optional(number, 5),
max_unavailable_count : optional(string, "1"),
scaling_cpu_threshold_percentage : optional(number, 80),
port : optional(number, 80),
scheme : optional(string, "HTTP"),
cmd : optional(list(string), []),
args : optional(list(string), []),
image_pull_policy : optional(string, "IfNotPresent"), // Supported values: IfNotPresent, Always, Never
tag_mutability : optional(bool, true),
scan_on_push : optional(bool, false),
service_port : optional(number, null),
service_scheme : optional(string, "HTTP"),
memory : optional(string, "500Mi"),
memory_requests : optional(string, "200Mi"),
cpu : optional(string, "1"),
cpu_requests : optional(string, "500m"),
gpu : optional(number, null), // Whole number of GPUs to request, 0 will schedule all available GPUs. Requires GPU-enabled nodes in the cluster, `k8s-device-plugin` installed, platform_architecture = "amd64", and additional_node_selectors = { "nvidia.com/gpu.present" = "true" } present.
health_check_path : optional(string, "/"),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
path : optional(string, "/*"), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
priority : optional(number, 0), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
success_codes : optional(string, "200-499"),
synthetics : optional(bool, false),
initial_delay_seconds : optional(number, 30),
alb_idle_timeout : optional(number, 60) // in seconds
period_seconds : optional(number, 3),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64; GPU nodes are amd64 only.
additional_node_selectors : optional(map(string), {}), // For GPU use: { "nvidia.com/gpu.present" = "true" }
bypasses : optional(map(object({ // Only used for INTERNAL service_type
paths = optional(set(string), [])
methods = optional(set(string), [])
})), {})
sidecars : optional(map(object({
image : string
tag : string
port : optional(number, 80),
scheme : optional(string, "HTTP"),
memory : optional(string, "200Mi")
cpu : optional(string, "500m")
image_pull_policy : optional(string, "IfNotPresent") // Supported values: IfNotPresent, Always, Never
health_check_path : optional(string, "/")
initial_delay_seconds : optional(number, 30),
period_seconds : optional(number, 3),
})), {})
}))
| n/a | yes | +| [services](#input\_services) | The services you want to deploy as part of this stack. |
map(object({
name : string,
service_type : optional(string, "INTERNAL"),
allow_mesh_services : optional(list(object({
service : optional(string, null),
stack : optional(string, null),
service_account_name : optional(string, null)
})), null),
ingress_security_groups : optional(list(string), []), // Only used for VPC service_type
alb : optional(object({
name : string,
listener_port : number,
}), null), // Only used for TARGET_GROUP_ONLY
desired_count : optional(number, 2),
max_count : optional(number, 5),
max_unavailable_count : optional(string, "1"),
scaling_cpu_threshold_percentage : optional(number, 80),
port : optional(number, 80),
scheme : optional(string, "HTTP"),
cmd : optional(list(string), []),
args : optional(list(string), []),
image_pull_policy : optional(string, "IfNotPresent"), // Supported values: IfNotPresent, Always, Never
tag_mutability : optional(bool, true),
scan_on_push : optional(bool, false),
service_port : optional(number, null),
service_scheme : optional(string, "HTTP"),
memory : optional(string, "500Mi"),
memory_requests : optional(string, "200Mi"),
cpu : optional(string, "1"),
cpu_requests : optional(string, "500m"),
gpu : optional(number, null), // Whole number of GPUs to request, 0 will schedule all available GPUs. Requires GPU-enabled nodes in the cluster, `k8s-device-plugin` installed, platform_architecture = "amd64", and additional_node_selectors = { "nvidia.com/gpu.present" = "true" } present.
health_check_path : optional(string, "/"),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
path : optional(string, "/*"), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
priority : optional(number, 0), // Only used for CONTEXT and TARGET_GROUP_ONLY routing
success_codes : optional(string, "200-499"),
synthetics : optional(bool, false),
initial_delay_seconds : optional(number, 30),
alb_idle_timeout : optional(number, 60) // in seconds
period_seconds : optional(number, 3),
liveness_timeout_seconds : optional(number, 30), // TODO
readiness_timeout_seconds : optional(number, 30), // TODO
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64; GPU nodes are amd64 only.
additional_node_selectors : optional(map(string), {}), // For GPU use: { "nvidia.com/gpu.present" = "true" }
bypasses : optional(map(object({ // Only used for INTERNAL service_type
paths = optional(set(string), [])
methods = optional(set(string), [])
})), {})
sidecars : optional(map(object({
image : string
tag : string
port : optional(number, 80),
scheme : optional(string, "HTTP"),
memory : optional(string, "200Mi")
cpu : optional(string, "500m")
image_pull_policy : optional(string, "IfNotPresent") // Supported values: IfNotPresent, Always, Never
health_check_path : optional(string, "/")
initial_delay_seconds : optional(number, 30),
period_seconds : optional(number, 3),
liveness_timeout_seconds : optional(number, 30), // TODO
readiness_timeout_seconds : optional(number, 30), // TODO
})), {})
additional_env_vars : optional(map(string), {}),
}))
| n/a | yes | | [skip\_config\_injection](#input\_skip\_config\_injection) | Skip injecting app configs into the services / tasks | `bool` | `false` | no | | [stack\_name](#input\_stack\_name) | Happy Path stack name | `string` | n/a | yes | | [stack\_prefix](#input\_stack\_prefix) | Do bucket storage paths and db schemas need to be prefixed with the stack name? (Usually '/{stack\_name}' for dev stacks, and '' for staging/prod stacks) | `string` | `""` | no | -| [tasks](#input\_tasks) | The deletion/migration tasks you want to run when a stack comes up and down. |
map(object({
image : string,
memory : optional(string, "200Mi"),
cpu : optional(string, "500m"),
cmd : optional(list(string), []),
args : optional(list(string), []),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64
is_cron_job : optional(bool, false),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
cron_schedule : optional(string, "0 0 1 1 *"),
}))
| `{}` | no | +| [tasks](#input\_tasks) | The deletion/migration tasks you want to run when a stack comes up and down. |
map(object({
image : string,
memory : optional(string, "200Mi"),
cpu : optional(string, "500m"),
cmd : optional(list(string), []),
args : optional(list(string), []),
platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64
is_cron_job : optional(bool, false),
aws_iam : optional(object({
policy_json : optional(string, ""),
service_account_name : optional(string, null),
}), {}),
cron_schedule : optional(string, "0 0 1 1 *"),
additional_env_vars : optional(map(string), {}),
}))
| `{}` | no | ## Outputs diff --git a/terraform/modules/happy-stack-helm-eks/locals.tf b/terraform/modules/happy-stack-helm-eks/locals.tf index 822f0ed6fc..0ec6aa8c0e 100644 --- a/terraform/modules/happy-stack-helm-eks/locals.tf +++ b/terraform/modules/happy-stack-helm-eks/locals.tf @@ -103,7 +103,7 @@ locals { replace(v.image, "/{(${join("|", keys(local.service_ecrs))})}/", "%s"), [ for repo in flatten(regexall("{(${join("|", keys(local.service_ecrs))})}", v.image)) : - lookup(local.service_ecrs, repo) + lookup(local.service_ecrs, repo, "") ]... ) }) diff --git a/terraform/modules/happy-stack-helm-eks/main.tf b/terraform/modules/happy-stack-helm-eks/main.tf index 88150cb3cf..98c1aa61aa 100644 --- a/terraform/modules/happy-stack-helm-eks/main.tf +++ b/terraform/modules/happy-stack-helm-eks/main.tf @@ -32,20 +32,20 @@ locals { "schedule" = v.cron_schedule "suspend" = v.is_cron_job ? false : true "volumes" = { - "additionalVolumesFromConfigMaps" = [for k1, v1 in var.additional_volumes_from_config_maps : { - "mountPath" = v1.base_dir - "name" = k1 + "additionalVolumesFromConfigMaps" = [for v1 in var.additional_volumes_from_config_maps.items : { + "mountPath" = "/var/${v1}" + "name" = v1 "readOnly" = true }] - "additionalVolumesFromSecrets" = [for k1, v1 in var.additional_volumes_from_secrets : { - "mountPath" = v1.base_dir - "name" = k1 + "additionalVolumesFromSecrets" = [for v1 in var.additional_volumes_from_secrets.items : { + "mountPath" = "/var/${v1}" + "name" = v1 "readOnly" = true }] } }] - services = [for k, v in local.patched_service_definitions : { + services = [for k, v in local.service_definitions : { "additionalNodeSelectors" = v.additional_node_selectors "additionalPodLabels" = var.additional_pod_labels "awsIam" = { @@ -90,23 +90,24 @@ locals { "loadBalancerAttributes" = [ "idle_timeout.timeout_seconds=${v.alb_idle_timeout}", ] - "securityGroups" = v.securityGroups "targetGroup" = v.group_name - "targetGroupArn" = v.targetGroupArn + "targetGroupArn" = "" // TODO + "securityGroups" = "" // TODO } "bypasses" = [ - (length(v.bypasses[k].methods) != 0 ? { - field = "http-request-method" - httpRequestMethodConfig = { - Values = v.bypasses[k].methods - } - } : null), - (length(v.bypasses[k].paths) != 0 ? { - field = "path-pattern" - pathPatternConfig = { - Values = v.bypasses[k].paths - } - } : null) + // TODO + # (length(v.bypasses[k].methods) != 0 ? { + # field = "http-request-method" + # httpRequestMethodConfig = { + # Values = v.bypasses[k].methods + # } + # } : null), + # (length(v.bypasses[k].paths) != 0 ? { + # field = "path-pattern" + # pathPatternConfig = { + # Values = v.bypasses[k].paths + # } + # } : null) ] "groupName" = v.group_name "hostMatch" = v.host_match @@ -157,14 +158,14 @@ locals { }] "volumes" = { - "additionalVolumesFromConfigMaps" = [for k1, v1 in var.additional_volumes_from_config_maps : { - "mountPath" = v1.base_dir - "name" = k1 + "additionalVolumesFromConfigMaps" = [for v1 in var.additional_volumes_from_config_maps.items : { + "mountPath" = "/var/${v1}" + "name" = v1 "readOnly" = true }] - "additionalVolumesFromSecrets" = [for k1, v1 in var.additional_volumes_from_secrets : { - "mountPath" = v1.base_dir - "name" = k1 + "additionalVolumesFromSecrets" = [for v1 in var.additional_volumes_from_secrets.items : { + "mountPath" = "/var/${v1}" + "name" = v1 "readOnly" = true }] } @@ -200,9 +201,9 @@ locals { } resource "helm_release" "stack" { - name = var.app_name - repository = "https://chanzuckerberg.github.io/happy-stack-helm/" - chart = "happy-stack" + name = var.stack_name + repository = "https://chanzuckerberg.github.io/happy-helm-charts/" + chart = "stack" namespace = var.k8s_namespace values = [yamlencode(local.values)] wait = true diff --git a/terraform/modules/happy-stack-helm-eks/target_group_only.tf b/terraform/modules/happy-stack-helm-eks/target_group_only.tf deleted file mode 100644 index d20fdee4b7..0000000000 --- a/terraform/modules/happy-stack-helm-eks/target_group_only.tf +++ /dev/null @@ -1,25 +0,0 @@ - -locals { - target_group_only_services = [for sd in local.service_definitions : sd if sd.service_type == "TARGET_GROUP_ONLY"] - other_services = [for sd in local.service_definitions : sd if sd.service_type != "TARGET_GROUP_ONLY"] - - updated_target_service_definitions = [for sd in local.service_definitions : merge(sd, { - "targetGroupArn" = module.target_group_only.aws_lb_target_group_arn - "securityGroups" = module.target_group_only.security_groups - })] - - updated_other_service_definitions = [for sd in local.other_services : merge(sd, { - "targetGroupArn" = "" - "securityGroups" = [] - })] - - patched_service_definitions = concat(local.updated_other_service_definitions, local.updated_target_service_definitions) -} - -module "target_group_only" { - for_each = local.target_group_only_services - source = "./target_group_only" - routing = each.value.routing - cloud_env = local.cloud_env - health_check_path = each.value.health_check_path -} \ No newline at end of file diff --git a/terraform/modules/happy-stack-helm-eks/target_group_only/README.md b/terraform/modules/happy-stack-helm-eks/target_group_only/README.md deleted file mode 100644 index df5670f2ae..0000000000 --- a/terraform/modules/happy-stack-helm-eks/target_group_only/README.md +++ /dev/null @@ -1,44 +0,0 @@ - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.3 | -| [aws](#requirement\_aws) | >= 5.23 | -| [random](#requirement\_random) | >= 3.5 | - -## Providers - -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 5.23 | -| [random](#provider\_random) | >= 3.5 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -|------|------| -| [aws_lb_listener_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener_rule) | resource | -| [aws_lb_target_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group) | resource | -| [random_pet.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | -| [aws_lb.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/lb) | data source | -| [aws_lb_listener.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/lb_listener) | data source | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [cloud\_env](#input\_cloud\_env) | Typically data.terraform\_remote\_state.cloud-env.outputs |
object({
public_subnets : list(string),
private_subnets : list(string),
database_subnets : list(string),
database_subnet_group : string,
vpc_id : string,
vpc_cidr_block : string,
})
| n/a | yes | -| [health\_check\_path](#input\_health\_check\_path) | The path to use for the health check | `string` | `"/health"` | no | -| [routing](#input\_routing) | Routing configuration for the ingress |
object({
method : optional(string, "DOMAIN")
host_match : string
additional_hostnames : optional(set(string), [])
group_name : string
alb : optional(object({
name : string,
listener_port : number,
}), null)
priority : number
path : optional(string, "/*")
service_name : string
port : number
service_port : number
alb_idle_timeout : optional(number, 60) // in seconds
service_scheme : optional(string, "HTTP")
scheme : optional(string, "HTTP")
success_codes : optional(string, "200-499")
service_type : string
service_mesh : bool
allow_mesh_services : optional(list(object({
service : optional(string, null),
stack : optional(string, null),
service_account_name : optional(string, null),
})), null)
oidc_config : optional(object({
issuer : string
authorizationEndpoint : string
tokenEndpoint : string
userInfoEndpoint : string
secretName : string
}), {
issuer = ""
authorizationEndpoint = ""
tokenEndpoint = ""
userInfoEndpoint = ""
secretName = ""
})
bypasses : optional(map(object({
paths = optional(set(string), [])
methods = optional(set(string), [])
})))
})
| n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| [security\_groups](#output\_security\_groups) | n/a | - \ No newline at end of file diff --git a/terraform/modules/happy-stack-helm-eks/target_group_only/main.tf b/terraform/modules/happy-stack-helm-eks/target_group_only/main.tf deleted file mode 100644 index 4f2b65b27b..0000000000 --- a/terraform/modules/happy-stack-helm-eks/target_group_only/main.tf +++ /dev/null @@ -1,45 +0,0 @@ -resource "random_pet" "this" { - keepers = { - target_group_name = var.routing.service_name - } -} - -locals { - # only hyphens and a max of 32 characters - target_group_name = replace(substr(random_pet.this.id, 0, 32), "_", "-") -} - -data "aws_lb" "this" { - name = var.routing.alb.name -} - -data "aws_lb_listener" "this" { - load_balancer_arn = data.aws_lb.this.arn - port = var.routing.alb.listener_port -} - -resource "aws_lb_target_group" "this" { - name = local.target_group_name - port = var.routing.service_port - protocol = "HTTP" - vpc_id = var.cloud_env.vpc_id - health_check { - path = var.health_check_path - } -} - -resource "aws_lb_listener_rule" "this" { - listener_arn = data.aws_lb_listener.this.arn - priority = 100 - - action { - type = "forward" - target_group_arn = aws_lb_target_group.this.arn - } - - condition { - path_pattern { - values = [var.routing.path] - } - } -} \ No newline at end of file diff --git a/terraform/modules/happy-stack-helm-eks/target_group_only/output.tf b/terraform/modules/happy-stack-helm-eks/target_group_only/output.tf deleted file mode 100644 index 52394f89db..0000000000 --- a/terraform/modules/happy-stack-helm-eks/target_group_only/output.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "security_groups" { - value = data.aws_lb.this.security_groups -} \ No newline at end of file diff --git a/terraform/modules/happy-stack-helm-eks/target_group_only/variables.tf b/terraform/modules/happy-stack-helm-eks/target_group_only/variables.tf deleted file mode 100644 index 9b8ce790be..0000000000 --- a/terraform/modules/happy-stack-helm-eks/target_group_only/variables.tf +++ /dev/null @@ -1,69 +0,0 @@ -variable "health_check_path" { - description = "The path to use for the health check" - type = string - default = "/health" -} - -variable "cloud_env" { - type = object({ - public_subnets : list(string), - private_subnets : list(string), - database_subnets : list(string), - database_subnet_group : string, - vpc_id : string, - vpc_cidr_block : string, - }) - description = "Typically data.terraform_remote_state.cloud-env.outputs" -} - -variable "routing" { - type = object({ - method : optional(string, "DOMAIN") - host_match : string - additional_hostnames : optional(set(string), []) - group_name : string - alb : optional(object({ - name : string, - listener_port : number, - }), null) - priority : number - path : optional(string, "/*") - service_name : string - port : number - service_port : number - alb_idle_timeout : optional(number, 60) // in seconds - service_scheme : optional(string, "HTTP") - scheme : optional(string, "HTTP") - success_codes : optional(string, "200-499") - service_type : string - service_mesh : bool - allow_mesh_services : optional(list(object({ - service : optional(string, null), - stack : optional(string, null), - service_account_name : optional(string, null), - })), null) - oidc_config : optional(object({ - issuer : string - authorizationEndpoint : string - tokenEndpoint : string - userInfoEndpoint : string - secretName : string - }), { - issuer = "" - authorizationEndpoint = "" - tokenEndpoint = "" - userInfoEndpoint = "" - secretName = "" - }) - bypasses : optional(map(object({ - paths = optional(set(string), []) - methods = optional(set(string), []) - }))) - }) - description = "Routing configuration for the ingress" - - validation { - condition = var.routing.service_mesh == true || var.routing.allow_mesh_services == null - error_message = "The allow_mesh_services option is only supported if service_mesh is enabled on the stack" - } -} \ No newline at end of file diff --git a/terraform/modules/happy-stack-helm-eks/target_group_only/versions.tf b/terraform/modules/happy-stack-helm-eks/target_group_only/versions.tf deleted file mode 100644 index e08b1ea38f..0000000000 --- a/terraform/modules/happy-stack-helm-eks/target_group_only/versions.tf +++ /dev/null @@ -1,13 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 5.23" - } - random = { - source = "hashicorp/random" - version = ">= 3.5" - } - } - required_version = ">= 1.3" -} diff --git a/terraform/modules/happy-stack-helm-eks/variables.tf b/terraform/modules/happy-stack-helm-eks/variables.tf index f0b0cf74be..49e17a6fb7 100644 --- a/terraform/modules/happy-stack-helm-eks/variables.tf +++ b/terraform/modules/happy-stack-helm-eks/variables.tf @@ -86,6 +86,8 @@ variable "services" { initial_delay_seconds : optional(number, 30), alb_idle_timeout : optional(number, 60) // in seconds period_seconds : optional(number, 3), + liveness_timeout_seconds : optional(number, 30), // TODO + readiness_timeout_seconds : optional(number, 30), // TODO platform_architecture : optional(string, "amd64"), // Supported values: amd64, arm64; GPU nodes are amd64 only. additional_node_selectors : optional(map(string), {}), // For GPU use: { "nvidia.com/gpu.present" = "true" } bypasses : optional(map(object({ // Only used for INTERNAL service_type @@ -103,7 +105,10 @@ variable "services" { health_check_path : optional(string, "/") initial_delay_seconds : optional(number, 30), period_seconds : optional(number, 3), + liveness_timeout_seconds : optional(number, 30), // TODO + readiness_timeout_seconds : optional(number, 30), // TODO })), {}) + additional_env_vars : optional(map(string), {}), })) description = "The services you want to deploy as part of this stack." @@ -188,6 +193,7 @@ variable "tasks" { service_account_name : optional(string, null), }), {}), cron_schedule : optional(string, "0 0 1 1 *"), + additional_env_vars : optional(map(string), {}), })) description = "The deletion/migration tasks you want to run when a stack comes up and down." default = {}