├── .devcontainer ├── Dockerfile ├── README.md └── devcontainer.json ├── .github └── workflows │ ├── format.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── ec2.tf ├── examples ├── acm │ ├── README.md │ └── main.tf ├── bucket_env │ ├── README.md │ └── main.tf ├── complete │ ├── README.md │ └── main.tf ├── containers │ ├── README.md │ └── main.tf ├── cpu │ └── main.tf ├── docker │ ├── README.md │ ├── content │ │ ├── Dockerfile │ │ └── index.html │ └── main.tf ├── ec2 │ ├── README.md │ └── main.tf ├── ecs │ ├── README.md │ └── main.tf ├── external_docker │ ├── Dockerfile │ ├── README.md │ ├── docker.tf │ ├── ecr.tf │ └── main.tf ├── external_ecr │ ├── README.md │ └── main.tf ├── external_resources │ ├── README.md │ └── main.tf ├── fargate │ ├── README.md │ └── main.tf ├── gRPC │ ├── README.md │ └── main.tf ├── gpu │ ├── README.md │ └── main.tf ├── inferentia │ ├── README.md │ └── main.tf ├── rest │ ├── README.md │ └── main.tf ├── route53 │ ├── README.md │ └── main.tf ├── traffics │ ├── README.md │ └── main.tf └── vpc │ ├── README.md │ └── main.tf ├── images └── architecture.png ├── main.tf ├── modules ├── asg │ ├── data.tf │ ├── main.tf │ ├── outputs.tf │ └── variable.tf ├── bucket │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── docker │ ├── docker.tf │ ├── outputs.tf │ ├── variables.tf │ └── version.tf ├── ecr │ ├── ecr.tf │ ├── outputs.tf │ └── variables.tf ├── ecs │ ├── README.md │ ├── acm.tf │ ├── ami.tf │ ├── asg.tf │ ├── data.tf │ ├── docker.tf │ ├── ecr.tf │ ├── ecs.tf │ ├── elb.tf │ ├── outputs.tf │ ├── route53.tf │ ├── traffic.tf │ └── variables.tf ├── eks │ ├── README.md │ ├── ami.tf │ ├── cluster-role.tf │ ├── data.tf │ ├── ec2-role.tf │ ├── eks.tf │ ├── elb.tf │ ├── microservice-role.tf │ ├── outputs.tf │ └── variables.tf ├── elb │ ├── data.tf │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── record │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── outputs.tf ├── tests ├── README.md ├── acm │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── aws │ ├── data.tf │ ├── outputs.tf │ └── providers.tf ├── check_acm │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── check_ecr │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── check_ecs_asg │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── check_ecs_cluster │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── check_ecs_service │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── check_ecs_task_execution │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── check_grpc │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── check_rest │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── cuda_ecs_ec2_complete.tftest.hcl ├── docker_content │ ├── Dockerfile │ └── index.html ├── ecr │ ├── ecr.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── env_file │ ├── main.tf │ └── variables.tf ├── get_env │ ├── outputs.tf │ └── variables.tf ├── grpc_ecs_ec2_complete.tftest.hcl ├── inferentia_ecs_ec2_complete.tftest.hcl ├── random_id │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── rest_ecr.hcl ├── rest_ecs_ec2_acm.tftest.hcl ├── rest_ecs_ec2_acm_external.tftest.hcl ├── rest_ecs_ec2_complete.tftest.hcl ├── rest_ecs_ec2_docker.tftest.hcl ├── rest_ecs_ec2_docker_external_ecr.tftest.hcl ├── rest_ecs_ec2_instance_type_amd.tftest.hcl ├── rest_ecs_ec2_instance_type_graviton.tftest.hcl ├── rest_ecs_ec2_instance_type_intel.tftest.hcl ├── rest_ecs_ec2_instance_type_none.tftest.hcl ├── rest_ecs_ec2_instance_types.tftest.hcl ├── rest_ecs_ec2_reapply.tftest.hcl ├── rest_ecs_ec2_schedules.tftest.hcl ├── rest_ecs_ec2_traffic.tftest.hcl ├── rest_ecs_fargate_complete.tftest.hcl ├── rest_vpc.hcl ├── variables.tftest.hcl └── wait │ ├── main.tf │ └── variables.tf ├── traffic.tf ├── variables.tf ├── version.tf └── vpc.tf /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=alpine:3.16 2 | 3 | #--------------------------------- 4 | # BUILDER ALPINE 5 | #--------------------------------- 6 | FROM ${VARIANT} as builder-alpine 7 | 8 | ARG TARGETOS TARGETARCH 9 | 10 | RUN apk update 11 | RUN apk add -q --no-cache zip wget 12 | 13 | # terraform 14 | ARG TERRAFORM_VERSION=1.6.2 15 | RUN wget -q https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_${TARGETOS}_${TARGETARCH}.zip \ 16 | && unzip terraform_${TERRAFORM_VERSION}_${TARGETOS}_${TARGETARCH}.zip && mv terraform /usr/local/bin/terraform \ 17 | && chmod +rx /usr/local/bin/terraform && rm terraform_${TERRAFORM_VERSION}_${TARGETOS}_${TARGETARCH}.zip 18 | 19 | # cloud-nuke 20 | ARG CLOUD_NUKE_VERSION=0.31.1 21 | RUN wget -q https://github.com/gruntwork-io/cloud-nuke/releases/download/v${CLOUD_NUKE_VERSION}/cloud-nuke_${TARGETOS}_${TARGETARCH} \ 22 | && mv cloud-nuke_${TARGETOS}_${TARGETARCH} /usr/local/bin/cloud-nuke \ 23 | && chmod +rx /usr/local/bin/cloud-nuke 24 | 25 | #------------------------- 26 | # RUNNER 27 | #------------------------- 28 | FROM ${VARIANT} as runner 29 | 30 | RUN apk update 31 | RUN apk add -q --no-cache make gcc libc-dev bash docker coreutils yq jq github-cli aws-cli curl 32 | 33 | # cloud-nuke 34 | COPY --from=builder-alpine /usr/local/bin/cloud-nuke /usr/local/bin/cloud-nuke 35 | RUN cloud-nuke --version 36 | 37 | # terraform 38 | COPY --from=builder-alpine /usr/local/bin/terraform /usr/local/bin/terraform 39 | RUN terraform --version 40 | 41 | # aws cli 42 | RUN aws --version 43 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # devcontainer 2 | 3 | Run locally a container to run the tests locally 4 | 5 | ## Prerequisites 6 | 7 | Create a file named `devcontainer.env` in this directory with the following contents: 8 | ``` 9 | # the AWS configuration to use 10 | AWS_REGION_NAME=us-east-1 11 | AWS_PROFILE_NAME=Test 12 | AWS_ACCOUNT_ID=471112927676 13 | AWS_ACCESS_KEY_ID=xxx 14 | AWS_SECRET_ACCESS_KEY=xxx 15 | 16 | # the name of the VPC to use 17 | TF_VAR_vpc_id=vpc-043f555cd21835697 18 | 19 | # if you want to use route53 20 | TF_VAR_domain_name=vistimi.com 21 | ``` -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Alpine", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "VARIANT": "mcr.microsoft.com/devcontainers/base:alpine-3.16", 7 | "DOCKER_BUILDKIT": "0" 8 | }, 9 | "target": "runner" 10 | }, 11 | "runArgs": [ 12 | "--env-file=.devcontainer/devcontainer.env" 13 | ], 14 | "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], 15 | // "postStartCommand": "export $(grep -v '^#' .devcontainer/devcontainer.env | xargs)", 16 | "customizations": { 17 | "vscode": { 18 | "extensions": [ 19 | "yzhang.markdown-all-in-one", 20 | "golang.go", 21 | "shakram02.bash-beautify", 22 | "shd101wyy.markdown-preview-enhanced", 23 | "premparihar.gotestexplorer", 24 | "hashicorp.terraform", 25 | "bierner.markdown-emoji", 26 | "ms-vscode.makefile-tools", 27 | "ms-azuretools.vscode-docker", 28 | "IronGeek.vscode-env", 29 | "github.vscode-github-actions", 30 | "hashicorp.hcl" 31 | ] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: "Terraform Format" 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | terraform: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@main 15 | 16 | - name: Setup Terraform 17 | uses: hashicorp/setup-terraform@main 18 | with: 19 | terraform_version: 1.6.2 20 | 21 | - name: Format diff 22 | run: | 23 | terraform fmt --check --diff -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | # workflow_run: 5 | # workflows: ["Docker registry"] 6 | # types: 7 | # - completed 8 | # push: 9 | # branches: [trunk] 10 | workflow_dispatch: 11 | 12 | env: 13 | ECR_PRIVACY: ${{ vars.CONTAINER_REGISTRY_PRIVACY }} 14 | IMAGE_TAG: latest 15 | ORGANIZATION_NAME: ${{ github.repository_owner }} 16 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 17 | ECR_REPOSITORY_EXTENSION: "" 18 | TESTS_TYPES: "rest variables" # grpc cuda fpga ... 19 | 20 | jobs: 21 | setup: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | TEST_NAMES: ${{ steps.get-tests.outputs.TEST_NAMES }} 25 | # environment: test 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@main 30 | 31 | - name: Get tests 32 | id: get-tests 33 | run: | 34 | files="" 35 | for test_type in $TESTS_TYPES; do 36 | files+="$(find tests -type f -name "*$test_type*.tftest.hcl" -exec basename {} *.tftest.hcl \;) " 37 | done 38 | 39 | transformed_files="" 40 | for file in $files; do 41 | transformed_files+="{\"test_name\": \"$file\"}," 42 | done 43 | transformed_files=${transformed_files%,} # Remove the trailing comma and close the square brackets 44 | echo TEST_NAMES={\"include\":[$transformed_files]} >> $GITHUB_OUTPUT 45 | 46 | test: 47 | needs: [setup] 48 | runs-on: ubuntu-latest 49 | permissions: 50 | contents: read 51 | strategy: 52 | matrix: ${{ fromJson(needs.setup.outputs.TEST_NAMES) }} 53 | # environment: test 54 | 55 | env: 56 | AWS_REGION_NAME: ${{ vars.AWS_REGION_NAME }} 57 | AWS_PROFILE_NAME: ${{ vars.AWS_PROFILE_NAME }} 58 | AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }} 59 | AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }} 60 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 61 | GITHUB_OWNER: ${{ github.repository_owner }} 62 | GITHUB_TOKEN: ${{ secrets.GH_TERRA_TOKEN }} 63 | TF_VAR_vpc_id: ${{ vars.VPC_ID }} 64 | TF_VAR_domain_name: ${{ vars.DOMAIN_NAME }} 65 | 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@main 69 | 70 | - name: Setup Terraform 71 | uses: hashicorp/setup-terraform@main 72 | with: 73 | terraform_version: 1.6.2 74 | 75 | - name: Prepare 76 | run: | 77 | make prepare 78 | 79 | - name: Terraform init 80 | run: | 81 | terraform init 82 | 83 | - name: Run Test 84 | run: | 85 | echo Running ${{ matrix.test_name }} 86 | terraform test -filter=tests/${{ matrix.test_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | 3 | # terraform 4 | .terraform* 5 | *.tfstate* 6 | *tfvars 7 | *override* 8 | **/provider.tf 9 | 10 | # ssh 11 | *.pem 12 | 13 | # grpcurl 14 | grpcurl* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "files.associations": { 4 | "*.hcl": "terraform", 5 | }, 6 | "terminal.integrated.scrollback": 20000 7 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | .SILENT: 3 | MAKEFLAGS += --no-print-directory 4 | MAKEFLAGS += --warn-undefined-variables 5 | .ONESHELL: 6 | 7 | PATH_ABS_ROOT=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 8 | 9 | prepare: 10 | FILE_RELATIVE_PATH=provider.tf make provider 11 | 12 | provider: ## create temporary provider files 13 | cat <<-EOF > ${PATH_ABS_ROOT}/${FILE_RELATIVE_PATH} 14 | #------------------------------------------- 15 | # AWS 16 | #------------------------------------------- 17 | 18 | provider "aws" { 19 | region = "us-east-1" 20 | } 21 | 22 | #------------------------------------------- 23 | # Docker 24 | #------------------------------------------- 25 | 26 | locals { 27 | ecr_address = format("%v.dkr.ecr.%v.amazonaws.com", data.aws_caller_identity.current.account_id, "us-east-1") 28 | # ecr_adress = "public.ecr.aws" 29 | } 30 | 31 | data "aws_caller_identity" "current" {} 32 | data "aws_ecr_authorization_token" "token" {} 33 | 34 | provider "docker" { 35 | registry_auth { 36 | address = local.ecr_address 37 | username = data.aws_ecr_authorization_token.token.user_name 38 | password = data.aws_ecr_authorization_token.token.password 39 | } 40 | } 41 | 42 | #------------------------------------------- 43 | # Kubernetes 44 | #------------------------------------------- 45 | 46 | # provider "kubernetes" { 47 | # host = one(values(module.eks)).cluster_endpoint 48 | # cluster_ca_certificate = base64decode(one(values(module.eks)).cluster.certificate_authority_data) 49 | 50 | # exec { 51 | # api_version = "client.authentication.k8s.io/v1beta1" 52 | # command = "aws" 53 | # # This requires the awscli to be installed locally where Terraform is executed 54 | # args = ["eks", "get-token", "--cluster-name", one(values(module.eks)).cluster.name] 55 | # } 56 | # } 57 | # provider "kubectl" { 58 | # host = one(values(module.eks)).cluster_endpoint 59 | # cluster_ca_certificate = base64decode(one(values(module.eks)).cluster.certificate_authority_data) 60 | 61 | # exec { 62 | # api_version = "client.authentication.k8s.io/v1beta1" 63 | # command = "aws" 64 | # # This requires the awscli to be installed locally where Terraform is executed 65 | # args = ["eks", "get-token", "--cluster-name", one(values(module.eks)).cluster.name] 66 | # } 67 | # } 68 | EOF -------------------------------------------------------------------------------- /ec2.tf: -------------------------------------------------------------------------------- 1 | data "aws_ec2_instance_type" "current" { 2 | for_each = { for instance_type in try(var.orchestrator.group.ec2.instance_types, []) : instance_type => {} } 3 | 4 | instance_type = each.key 5 | } 6 | 7 | 8 | locals { 9 | # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/memory-management.html 10 | # https://docs.aws.amazon.com/cli/latest/reference/ecs/describe-container-instances.html 11 | 12 | instance_datas = { 13 | for instance_type in try(var.orchestrator.group.ec2.instance_types, []) : 14 | instance_type => regex("^(?P\\w+)(?P\\d)(?P\\w?)(?P\\w?)\\.(?P\\w+)$", instance_type) 15 | } 16 | 17 | instance_chip_types = { 18 | for instance_type, instance_data in local.instance_datas : 19 | instance_type => contains(["p", "g"], instance_data.instance_family) ? "gpu" : ( 20 | contains(["inf", "trn"], instance_data.instance_family) ? "inf" : "cpu" 21 | ) 22 | } 23 | 24 | instance_specs = { 25 | for instance in data.aws_ec2_instance_type.current : instance.id => { 26 | cpu = instance.default_vcpus * 1024 27 | memory = instance.memory_size 28 | memory_available = floor(instance.memory_size * 0.9) # leaves some overhead for ECS 29 | device_count = coalesce( 30 | try(one(instance.inference_accelerators).count, null), 31 | try(one(instance.gpus).count, null), 32 | 0, 33 | ) 34 | architecture = [for arch in instance.supported_architectures : arch if contains(["x86_64", "arm64"], arch)][0] 35 | } 36 | } 37 | 38 | instances = { 39 | for instance_type, instance_data in local.instance_datas : instance_type => { 40 | instance_family = instance_data.instance_family 41 | instance_generation = instance_data.instance_generation 42 | processor_family = instance_data.processor_family 43 | additional_capability = instance_data.additional_capability 44 | instance_size = instance_data.instance_size 45 | architecture = local.instance_specs[instance_type].architecture 46 | chip_type = local.instance_chip_types[instance_type] 47 | cpu = local.instance_specs[instance_type].cpu 48 | memory = local.instance_specs[instance_type].memory 49 | memory_available = local.instance_specs[instance_type].memory_available 50 | device_count = local.instance_specs[instance_type].device_count 51 | } 52 | } 53 | } 54 | 55 | resource "null_resource" "ec2" { 56 | for_each = var.orchestrator.group.ec2 != null ? { 0 = {} } : {} 57 | 58 | lifecycle { 59 | precondition { 60 | condition = length(distinct([for _, instance_specs in local.instances : instance_specs.architecture])) == 1 61 | error_message = < v.architecture })} 63 | EOF 64 | } 65 | 66 | precondition { 67 | condition = alltrue([for instance_type in var.orchestrator.group.ec2.instance_types : contains(keys(local.instance_specs), instance_type)]) 68 | error_message = < v.cpu })} 78 | EOF 79 | } 80 | 81 | precondition { 82 | condition = length(distinct([for _, instance_specs in local.instances : instance_specs.memory])) == 1 83 | error_message = < v.memory })} 85 | EOF 86 | } 87 | 88 | } 89 | } 90 | 91 | # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html 92 | # https://aws.amazon.com/ec2/instance-types/ 93 | resource "null_resource" "instance" { 94 | for_each = { for instance_type, instance_specs in local.instances : instance_type => instance_specs } 95 | 96 | lifecycle { 97 | precondition { 98 | condition = var.orchestrator.group.ec2.os == "linux" ? contains(["x86_64", "arm64"], each.value.architecture) : false 99 | error_message = "EC2 architecture must for one of linux:[x86_64, arm64]" 100 | } 101 | 102 | precondition { 103 | condition = contains(["inf", "gpu"], each.value.chip_type) ? length(var.orchestrator.group.ec2.instance_types) == 1 : true 104 | error_message = "ec2 inf/gpu instance types must contain only one element, got ${jsonencode(var.orchestrator.group.ec2.instance_types)}" 105 | } 106 | 107 | precondition { 108 | condition = contains(["gpu", "inf"], each.value.chip_type) ? alltrue([for idx in flatten([for container in var.orchestrator.group.deployment.containers : coalesce(container.device_idxs, [])]) : idx > 0 && idx < local.instances[var.orchestrator.group.ec2.instance_types[0]].device_count]) : true 109 | error_message = < Clusters -> Infrastructure -> Container instances -> Memory available 11 | { 12 | name = "first-half" 13 | cpu = 32 * 1024 14 | memory = 64 * 1024 - 2500 # minus the approximate size taken by the orchestrator 15 | # ... 16 | }, 17 | { 18 | name = "second" 19 | cpu = 22 * 1024 20 | memory = 44 * 1024 - 2000 # minus the approximate size taken by the orchestrator 21 | # ... 22 | }, 23 | { 24 | name = "third" 25 | cpu = 10 * 1024 26 | memory = 20 * 1024 - 500 # minus the approximate size taken by the orchestrator 27 | # ... 28 | } 29 | ] 30 | # ... 31 | } 32 | ec2 = { 33 | instance_types = ["c7i.16xlarge"] 34 | os = "linux" 35 | os_version = "2" 36 | # ... 37 | } 38 | } 39 | ecs = {} 40 | } 41 | 42 | vpc = {} # ... 43 | } 44 | -------------------------------------------------------------------------------- /examples/docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker example for the microservice 2 | 3 | This example displays all the possible docker possibilities. It can be ECR or any other registry. It can be an ECR registry from this account or another. It can be an ECR public or private repository. -------------------------------------------------------------------------------- /examples/docker/content/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM httpd:2.4 2 | COPY ./index.html /usr/local/apache2/htdocs/index.html 3 | 4 | EXPOSE 80 -------------------------------------------------------------------------------- /examples/docker/content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hello World!

5 | 6 | -------------------------------------------------------------------------------- /examples/docker/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-ecs" 5 | 6 | orchestrator = { 7 | group = { 8 | deployment = { 9 | containers = [ 10 | { 11 | docker = { 12 | registry = { 13 | ecr = { 14 | privacy = "private" 15 | } 16 | } 17 | repository = { 18 | # name if not specified will be var.name 19 | lifecycle_policy = jsonencode({ 20 | rules = [ 21 | { 22 | rulePriority = 1, 23 | description = "Keep last 30 images", 24 | selection = { 25 | tagStatus = "any", 26 | countType = "imageCountMoreThan", 27 | countNumber = 30 28 | }, 29 | action = { 30 | type = "expire" 31 | } 32 | } 33 | ] 34 | }) 35 | } 36 | build = { 37 | # the platform is set based on the architecture of the ec2 instance 38 | args = { "BUILD_DATE" = "2021-06-01T00:00:00Z" } # default {} 39 | context_path = "./content" # default "." 40 | file_path = "Dockerfile" # default "Dockerfile" 41 | path_include = ["**"] 42 | path_exclude = ["**/.git/**", "**/.github/**", "**/.terraform/**"] # these are files to be ignored for generating a new image such as files in .gitignore 43 | } 44 | } 45 | # ... 46 | } 47 | ] 48 | } 49 | ec2 = { 50 | instance_types = ["t4g.medium"] 51 | os = "linux" 52 | os_version = "2023" 53 | capacities = [{ 54 | type = "ON_DEMAND" 55 | }] 56 | } 57 | } 58 | # ... 59 | } 60 | 61 | vpc = {} # ... 62 | } 63 | -------------------------------------------------------------------------------- /examples/ec2/README.md: -------------------------------------------------------------------------------- 1 | # EC2 example for the microservice -------------------------------------------------------------------------------- /examples/ec2/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-ec2-2023" 5 | 6 | orchestrator = { 7 | group = { 8 | ec2 = { 9 | key_name = "name_of_key_to_ssh_with" 10 | instance_types = ["t2.micro"] 11 | os = "linux" 12 | os_version = "2023" 13 | capacities = [ 14 | # if no capacity provider is specified, `ON_DEMAND` will be used 15 | { 16 | type = "ON_DEMAND" 17 | base = true 18 | weight = 60 19 | }, 20 | { 21 | type = "SPOT" 22 | weight = 30 23 | } 24 | ] 25 | } 26 | # ... 27 | } 28 | # ... 29 | } 30 | vpc = {} # ... 31 | } 32 | 33 | module "microservice" { 34 | source = "vistimi/microservice/aws" 35 | 36 | name = "microservice-with-ec2-2" 37 | 38 | orchestrator = { 39 | group = { 40 | ec2 = { 41 | os = "linux" 42 | os_version = "2" 43 | # ... 44 | } 45 | # ... 46 | } 47 | # ... 48 | } 49 | 50 | vpc = {} # ... 51 | } 52 | 53 | module "microservice" { 54 | source = "vistimi/microservice/aws" 55 | 56 | name = "microservice-with-ec2-many-instance-types" 57 | 58 | orchestrator = { 59 | group = { 60 | ec2 = { 61 | instance_types = ["t2.medium", "t3.medium"] 62 | # ... 63 | } 64 | # ... 65 | } 66 | # ... 67 | } 68 | vpc = {} # ... 69 | } 70 | -------------------------------------------------------------------------------- /examples/ecs/README.md: -------------------------------------------------------------------------------- 1 | # ECS example for the microservice -------------------------------------------------------------------------------- /examples/ecs/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-ecs" 5 | 6 | orchestrator = { 7 | group = {} # ... 8 | ecs = {} # there is no overriding the default ecs config yet 9 | } 10 | 11 | vpc = {} # ... 12 | } 13 | -------------------------------------------------------------------------------- /examples/external_docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | EXPOSE 80 4 | EXPOSE 81 5 | 6 | RUN apt update && apt install apache2 ufw systemctl curl -y 7 | RUN ufw app list 8 | RUN echo -e 'Listen 81' >> /etc/apache2/ports.conf; echo print /etc/apache2/ports.conf.....; cat /etc/apache2/ports.conf 9 | RUN echo -e '\nServerAdmin webmaster@localhost\nDocumentRoot /var/www/html\nErrorLog $${APACHE_LOG_DIR}/error.log\nCustomLog $${APACHE_LOG_DIR}/access.log combined\n' >> /etc/apache2/sites-enabled/000-default.conf; echo print /etc/apache2/sites-enabled/000-default.conf.....; cat /etc/apache2/sites-enabled/000-default.conf 10 | 11 | CMD ["/bin/sh", "-c", "systemctl start apache2; sleep infinity"] -------------------------------------------------------------------------------- /examples/external_docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker example for the microservice 2 | 3 | This example displays how to create a custom build using Docker. It can be ECR or any other registry. It can be an ECR registry from this account or another. It can be an ECR public or private repository. 4 | `docker.tf` will build and push the image to the repository created in `ecr.tf`. -------------------------------------------------------------------------------- /examples/external_docker/docker.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | source_path = ".." 3 | path_include = ["**"] 4 | path_exclude = ["**/node_modules/**", "**/.next/**"] 5 | files_include = setunion([for f in local.path_include : fileset(local.source_path, f)]...) 6 | files_exclude = setunion([for f in local.path_exclude : fileset(local.source_path, f)]...) 7 | files = sort(setsubtract(local.files_include, local.files_exclude)) 8 | dir_sha = sha1(join("", [for f in local.files : filesha1("${local.source_path}/${f}")])) 9 | 10 | # ECR 11 | ecr_address = format("%v.dkr.ecr.%v.amazonaws.com", data.aws_caller_identity.current.account_id, data.aws_region.current.name) 12 | # ecr_adress = "public.ecr.aws" 13 | ecr_image_tag = format("%v:%v", module.ecr.repository_url, local.ecr_image_tag) 14 | } 15 | 16 | data "aws_ecr_authorization_token" "token" {} 17 | 18 | provider "docker" { 19 | registry_auth { 20 | address = local.ecr_address 21 | username = data.aws_ecr_authorization_token.token.user_name 22 | password = data.aws_ecr_authorization_token.token.password 23 | } 24 | } 25 | 26 | # local image 27 | resource "docker_image" "this" { 28 | name = local.ecr_image_tag 29 | 30 | build { 31 | platform = "linux/amd64" # "linux/arm64", this needs to match the architecture of the ECS instance 32 | context = local.source_path 33 | dockerfile = "Dockerfile" 34 | build_args = {} 35 | tag = [local.ecr_image_tag] 36 | } 37 | 38 | force_remove = false 39 | keep_locally = false 40 | triggers = { 41 | dir_sha = local.dir_sha 42 | } 43 | } 44 | 45 | # registry image 46 | resource "docker_registry_image" "this" { 47 | name = docker_image.this.name 48 | 49 | keep_remotely = true 50 | 51 | triggers = { 52 | dir_sha = local.dir_sha 53 | } 54 | } -------------------------------------------------------------------------------- /examples/external_docker/ecr.tf: -------------------------------------------------------------------------------- 1 | module "ecr" { 2 | source = "terraform-aws-modules/ecr/aws" 3 | version = "2.2.0" 4 | 5 | repository_name = local.ecr_repo_name 6 | repository_force_delete = true 7 | repository_image_tag_mutability = "MUTABLE" 8 | 9 | create_lifecycle_policy = true 10 | repository_lifecycle_policy = jsonencode({ 11 | rules = [ 12 | { 13 | rulePriority = 1, 14 | description = "Keep last 30 images", 15 | selection = { 16 | tagStatus = "any", 17 | countType = "imageCountMoreThan", 18 | countNumber = 30 19 | }, 20 | action = { 21 | type = "expire" 22 | } 23 | } 24 | ] 25 | }) 26 | 27 | repository_read_access_arns = [module.microservice.ecs.service.task_exec_iam_role_arn] 28 | 29 | tags = {} 30 | } -------------------------------------------------------------------------------- /examples/external_docker/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | ecr_repo_name = "my_private_ecr_repo_name" 3 | ecr_image_tag = "latest" 4 | } 5 | 6 | module "microservice" { 7 | source = "vistimi/microservice/aws" 8 | 9 | name = "microservice-external-docker" 10 | 11 | orchestrator = { 12 | group = { 13 | deployment = { 14 | containers = [ 15 | { 16 | # own private ecr repository 17 | docker = { 18 | registry = { 19 | ecr = { 20 | privacy = "private" 21 | } 22 | } 23 | repository = { 24 | name = local.ecr_repo_name 25 | } 26 | image = { 27 | tag = local.ecr_image_tag 28 | } 29 | } 30 | # ... 31 | }, 32 | ] 33 | } 34 | # ... 35 | } 36 | # ... 37 | } 38 | 39 | vpc = {} # ... 40 | } 41 | -------------------------------------------------------------------------------- /examples/external_ecr/README.md: -------------------------------------------------------------------------------- 1 | # Docker example for the microservice 2 | 3 | This example displays all the possible docker possibilities. It can be ECR or any other registry. It can be an ECR registry from this account or another. It can be an ECR public or private repository. -------------------------------------------------------------------------------- /examples/external_ecr/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-docker" 5 | 6 | orchestrator = { 7 | group = { 8 | deployment = { 9 | containers = [ 10 | { 11 | # own private ecr repository 12 | docker = { 13 | registry = { 14 | ecr = { 15 | privacy = "private" 16 | } 17 | } 18 | repository = { 19 | name = "my_private_ecr_repo_name" 20 | } 21 | image = { 22 | tag = "latest" 23 | } 24 | } 25 | # ... 26 | }, 27 | { 28 | # own ecr public repository 29 | docker = { 30 | registry = { 31 | ecr = { 32 | privacy = "public" 33 | public_alias = "my_public_registry_alias" 34 | } 35 | } 36 | repository = { 37 | name = "my_public_ecr_repo_name" 38 | } 39 | image = { 40 | tag = "latest" 41 | } 42 | } 43 | # ... 44 | }, 45 | { 46 | # other account 47 | docker = { 48 | registry = { 49 | ecr = { 50 | privacy = "private" 51 | account_id = "763104351884" 52 | region_name = "us-east-1" 53 | } 54 | } 55 | repository = { 56 | name = "pytorch-training" 57 | } 58 | image = { 59 | tag = "1.8.1-gpu-py36-cu111-ubuntu18.04-v1.7" 60 | } 61 | } 62 | # ... 63 | }, 64 | { 65 | # https://hub.docker.com/r/pytorch/torchserve 66 | docker = { 67 | registry = { 68 | name = "pytorch" 69 | } 70 | repository = { 71 | name = "torchserve" 72 | } 73 | image = { 74 | tag = "latest" 75 | } 76 | } 77 | # ... 78 | }, 79 | { 80 | # https://hub.docker.com/_/ubuntu 81 | docker = { 82 | repository = { 83 | name = "ubuntu" 84 | } 85 | image = { 86 | tag = "latest" 87 | } 88 | } 89 | # ... 90 | } 91 | ] 92 | } 93 | # ... 94 | } 95 | # ... 96 | } 97 | 98 | vpc = {} # ... 99 | } 100 | -------------------------------------------------------------------------------- /examples/external_resources/README.md: -------------------------------------------------------------------------------- 1 | # Environment file example with the microservice 2 | 3 | This will create a microservice and will grant the permission to the containers to access other resources. -------------------------------------------------------------------------------- /examples/external_resources/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = "external-resource" 3 | } 4 | 5 | module "microservice" { 6 | source = "vistimi/microservice/aws" 7 | 8 | name = local.name 9 | 10 | vpc = {} # ... 11 | orchestrator = {} # ... 12 | traffics = [] # ... 13 | } 14 | 15 | ################################################################################ 16 | # RDS access with container 17 | ################################################################################ 18 | 19 | # To create and attach a custom role 20 | module "rds" { 21 | source = "terraform-aws-modules/rds/aws" 22 | 23 | name = local.name 24 | # ... 25 | } 26 | 27 | data "aws_iam_policy_document" "rds_custom_access" { 28 | statement { 29 | effect = "Allow" 30 | 31 | actions = [ 32 | "rds:*", 33 | # ... 34 | ] 35 | 36 | resources = [ 37 | module.rds.db_instance_master_user_secret_arn 38 | ] 39 | } 40 | } 41 | 42 | resource "aws_iam_policy" "rds_custom_access" { 43 | name = "${local.name}-rds-access" 44 | description = "Policy to allow access to rds" 45 | policy = data.aws_iam_policy_document.rds_custom_access.json 46 | } 47 | 48 | resource "aws_iam_role_policy_attachment" "rds_custom_access" { 49 | policy_arn = aws_iam_policy.rds_custom_access.arn 50 | role = module.microservice.ecs.service.task_iam_role_name 51 | } 52 | 53 | # To attach an existing policy 54 | 55 | resource "aws_iam_role_policy_attachment" "rds_full_access" { 56 | count = var.create_rds && var.create_microservice ? 1 : 0 57 | 58 | policy_arn = "arn:aws:iam::aws:policy/AmazonRDSFullAccess" 59 | role = module.microservice[0].ecs.service.task_iam_role_name 60 | } 61 | 62 | resource "aws_iam_role_policy_attachment" "rds_data_full_access" { 63 | count = var.create_rds && var.create_microservice ? 1 : 0 64 | 65 | policy_arn = "arn:aws:iam::aws:policy/AmazonRDSDataFullAccess" 66 | role = module.microservice[0].ecs.service.task_iam_role_name 67 | } 68 | -------------------------------------------------------------------------------- /examples/fargate/README.md: -------------------------------------------------------------------------------- 1 | # Fargate example for the microservice -------------------------------------------------------------------------------- /examples/fargate/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-fargate" 5 | 6 | orchestrator = { 7 | group = { 8 | cpu = 512 9 | memory = 1024 10 | 11 | deployment = { 12 | cpu = 512 13 | memory = 1024 14 | # ... 15 | } 16 | fargate = { 17 | os = "linux" 18 | architecture = "x86_64" 19 | capacities = [ 20 | # if no capacity provider is specified, `ON_DEMAND` will be used 21 | { 22 | type = "ON_DEMAND" 23 | base = true 24 | weight = 60 25 | }, 26 | { 27 | type = "SPOT" 28 | weight = 30 29 | } 30 | ] 31 | } 32 | # ... 33 | } 34 | # ... 35 | } 36 | 37 | vpc = {} # ... 38 | } 39 | -------------------------------------------------------------------------------- /examples/gRPC/README.md: -------------------------------------------------------------------------------- 1 | # gRPC example with the microservice 2 | 3 | This will create a load balancer that will work with gRPC. It requires HTTPS and HTTP2. Specify that the target `protocol_version` is `grpc` and give the `status_code` for successful requests. -------------------------------------------------------------------------------- /examples/gRPC/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-grpc" 5 | 6 | route53 = { 7 | # ... 8 | } 9 | 10 | orchestrator = { 11 | group = { 12 | deployment = { 13 | containers = [ 14 | { 15 | traffics = [ 16 | { 17 | listener = { 18 | # port is by default 443 with https 19 | protocol = "https" 20 | } 21 | target = { 22 | port = 50051 23 | protocol_version = "grpc" 24 | health_check_path = "/helloworld.Greeter/SayHello" 25 | status_code = "0" 26 | } 27 | }, 28 | { 29 | # this will redirect https:444 to grpc:50051 30 | listener = { 31 | port = 444 32 | protocol = "https" 33 | } 34 | target = { 35 | port = 50051 36 | protocol_version = "grpc" 37 | health_check_path = "/helloworld.Greeter/SayHello" 38 | status_code = "0" 39 | } 40 | } 41 | ] 42 | # ... 43 | } 44 | ] 45 | # ... 46 | } 47 | # ... 48 | } 49 | # ... 50 | } 51 | 52 | vpc = {} # ... 53 | } 54 | -------------------------------------------------------------------------------- /examples/gpu/README.md: -------------------------------------------------------------------------------- 1 | # GPU example for the microservice 2 | 3 | The GPU instances can contains multiple GPUs, meaning that the instance is in fact a cluster of GPUs. 4 | If multiple containers are used, there is the need to specify which container will run on which GPU. -------------------------------------------------------------------------------- /examples/gpu/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-gpu-one-chip-one-container" 5 | 6 | 7 | orchestrator = { 8 | group = { 9 | deployment = { 10 | containers = [ 11 | { 12 | name = "unique" 13 | # ... 14 | } 15 | # ... 16 | ] 17 | # ... 18 | } 19 | ec2 = { 20 | instance_types = ["g4dn.xlarge"] 21 | os = "linux" 22 | os_version = "2" 23 | # ... 24 | } 25 | } 26 | ecs = {} 27 | } 28 | 29 | vpc = {} # ... 30 | } 31 | 32 | # TODO: this configuration has not been tested yet 33 | module "microservice" { 34 | source = "vistimi/microservice/aws" 35 | 36 | name = "microservice-with-gpu-four-chips-two-containers" 37 | 38 | orchestrator = { 39 | group = { 40 | deployment = { 41 | containers = [ 42 | { 43 | name = "first-two-gpus" 44 | cpu = 24576 45 | memory = 98304 - 4000 # minus the approximate size taken by the orchestrator 46 | device_idxs = [0, 1] 47 | # ... 48 | }, 49 | { 50 | name = "second-two-gpus" 51 | cpu = 24576 52 | memory = 98304 - 4000 # minus the approximate size taken by the orchestrator 53 | device_idxs = [2, 3] 54 | # ... 55 | } 56 | ] 57 | # ... 58 | } 59 | ec2 = { 60 | instance_types = ["g4dn.12xlarge"] 61 | os = "linux" 62 | os_version = "2" 63 | # ... 64 | } 65 | } 66 | ecs = {} 67 | } 68 | 69 | vpc = {} # ... 70 | } 71 | -------------------------------------------------------------------------------- /examples/inferentia/README.md: -------------------------------------------------------------------------------- 1 | # Inferentia example for the microservice 2 | 3 | The inferentia instances can contain multiple chips, meaning that if multiple containers are used, there is the need to specify which container will run on which chip. 4 | Note that each chip contain four cores/workers. -------------------------------------------------------------------------------- /examples/inferentia/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-inferentia-one-chip-one-container" 5 | 6 | 7 | orchestrator = { 8 | group = { 9 | deployment = { 10 | containers = [ 11 | { 12 | name = "unique" 13 | # ... 14 | } 15 | # ... 16 | ] 17 | # ... 18 | } 19 | ec2 = { 20 | instance_types = ["inf1.xlarge"] 21 | os = "linux" 22 | os_version = "2" 23 | # ... 24 | } 25 | } 26 | ecs = {} 27 | } 28 | 29 | vpc = {} # ... 30 | } 31 | 32 | # TODO: this configuration has not been tested yet 33 | module "microservice" { 34 | source = "vistimi/microservice/aws" 35 | 36 | name = "microservice-with-inferentia-four-chips-two-containers" 37 | 38 | orchestrator = { 39 | group = { 40 | deployment = { 41 | containers = [ 42 | { 43 | name = "first-two-chips" 44 | cpu = 12288 45 | memory = 24576 - 2500 # minus the approximate size taken by the orchestrator 46 | device_idxs = [0, 1] 47 | # ... 48 | }, 49 | { 50 | name = "second-two-chips" 51 | cpu = 12288 52 | memory = 24576 - 2500 # minus the approximate size taken by the orchestrator 53 | device_idxs = [2, 3] 54 | # ... 55 | } 56 | ] 57 | # ... 58 | } 59 | ec2 = { 60 | instance_types = ["inf1.6xlarge"] 61 | os = "linux" 62 | os_version = "2" 63 | # ... 64 | } 65 | } 66 | ecs = {} 67 | } 68 | 69 | vpc = {} # ... 70 | } 71 | -------------------------------------------------------------------------------- /examples/rest/README.md: -------------------------------------------------------------------------------- 1 | # Rest example with the microservice 2 | 3 | This will create a load balancer that will work with HTTP. It supports HTTP1.1 and HTTP2. -------------------------------------------------------------------------------- /examples/rest/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-rest-http1" 5 | 6 | traffics = [ 7 | { 8 | listener = { 9 | # port is by default 80 with http 10 | protocol = "http" 11 | protocol_version = "http1" # by default it is `http1` 12 | } 13 | target = { 14 | port = 8080 15 | protocol = "http" # if not specified, the protocol will be the same as the listener 16 | health_check_path = "/ping" # if not specified, the health_check_path will be "/" 17 | protocol_version = "http1" # by default it is `http1` 18 | } 19 | }, 20 | { 21 | # this will redirect http:81 to http:8080 22 | listener = { 23 | port = 81 24 | protocol = "http" 25 | } 26 | }, 27 | ] 28 | 29 | vpc = {} # ... 30 | orchestrator = {} # ... 31 | } 32 | 33 | # TODO: this configuration has not been tested yet 34 | module "microservice" { 35 | source = "vistimi/microservice/aws" 36 | 37 | name = "microservice-with-rest-http2" 38 | 39 | traffics = [ 40 | { 41 | listener = { 42 | # port is by default 80 with http 43 | protocol = "http" 44 | protocol_version = "http2" # by default it is `http1` 45 | } 46 | target = { 47 | port = 8080 48 | protocol = "http" # if not specified, the protocol will be the same as the listener 49 | health_check_path = "/ping" # if not specified, the health_check_path will be "/" 50 | protocol_version = "http2" # by default it is `http1` 51 | } 52 | }, 53 | { 54 | # this will redirect http:81 to http:8080 55 | listener = { 56 | port = 81 57 | protocol = "http" 58 | } 59 | }, 60 | ] 61 | 62 | vpc = {} # ... 63 | orchestrator = {} # ... 64 | } 65 | -------------------------------------------------------------------------------- /examples/route53/README.md: -------------------------------------------------------------------------------- 1 | # Route53 example with the microservice 2 | 3 | This will add a record in the already existing hosted zone. The prefixes are something like `www` to add but are not necessary. -------------------------------------------------------------------------------- /examples/route53/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = "microservice-with-dns" 3 | hosted_zone_name = "mydomain.com" 4 | } 5 | 6 | module "microservice" { 7 | source = "vistimi/microservice/aws" 8 | 9 | name = local.name 10 | 11 | # will create the following DNS records: 12 | # - microservice-with-dns.mydomain.com 13 | # - www.microservice-with-dns.mydomain.com 14 | # - whatever.microservice-with-dns.mydomain.com 15 | route53 = { 16 | zones = [{ 17 | name = local.hosted_zone_name 18 | }] 19 | record = { 20 | prefixes = ["www", "whatever"] # optional 21 | subdomain_name = local.name 22 | } 23 | } 24 | 25 | vpc = {} # ... 26 | traffics = [] # ... 27 | orchestrator = {} # ... 28 | } 29 | -------------------------------------------------------------------------------- /examples/traffics/README.md: -------------------------------------------------------------------------------- 1 | # Traffics example for the microservice 2 | 3 | Traffics can be for many listeners to many targets. :warning: Make sure that the target to the same port have the same protocol and protocol version. -------------------------------------------------------------------------------- /examples/traffics/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-with-container-traffics" 5 | 6 | orchestrator = { 7 | group = { 8 | deployment = { 9 | containers = [ 10 | { 11 | traffics = [ 12 | { 13 | # this will redirect http:80 to http:80 14 | listener = { 15 | # port is by default 80 with http 16 | protocol = "http" 17 | } 18 | target = { 19 | port = 80 20 | protocol = "http" # if not specified, the protocol will be the same as the listener 21 | health_check_path = "/" # if not specified, the health_check_path will be "/" 22 | } 23 | }, 24 | { 25 | # this will redirect http:81 to http:80 26 | listener = { 27 | port = 81 28 | protocol = "http" 29 | redirect = { 30 | port = 80 31 | protocol = "http" 32 | status_code = 301 33 | } 34 | } 35 | }, 36 | { 37 | # this will redirect https:444 to http:80 38 | listener = { 39 | port = 444 40 | protocol = "https" 41 | } 42 | target = { 43 | port = 80 44 | protocol = "http" # if not specified, the protocol will be the same as the listener 45 | health_check_path = "/" # if not specified, the health_check_path will be "/" 46 | } 47 | }, 48 | { 49 | # this will redirect http:81 to http:81 50 | listener = { 51 | port = 81 52 | protocol = "http" 53 | } 54 | target = { 55 | port = 81 56 | protocol = "http" # if not specified, the protocol will be the same as the listener 57 | health_check_path = "/" # if not specified, the health_check_path will be "/" 58 | } 59 | }, 60 | ] 61 | # ... 62 | } 63 | ] 64 | # ... 65 | } 66 | # ... 67 | } 68 | # ... 69 | } 70 | 71 | vpc = {} # ... 72 | } 73 | -------------------------------------------------------------------------------- /examples/vpc/README.md: -------------------------------------------------------------------------------- 1 | # Complete example with ECS for the microservice 2 | 3 | It requires a VPC with at least two subnets. It is not necessary to specify the subnets if they have the following tags `public`, `infra`, `private` -------------------------------------------------------------------------------- /examples/vpc/main.tf: -------------------------------------------------------------------------------- 1 | module "microservice" { 2 | source = "vistimi/microservice/aws" 3 | 4 | name = "microservice-vpc-with-tags" 5 | 6 | vpc = { 7 | id = "my_vpc_id" 8 | tag_tier = "public", 9 | } 10 | 11 | orchestrator = {} # ... 12 | traffics = [] # ... 13 | } 14 | 15 | module "microservice" { 16 | source = "vistimi/microservice/aws" 17 | 18 | name = "microservice-vpc-without-tags" 19 | 20 | vpc = { 21 | id = "my_vpc_id" 22 | subnet_tier_ids = ["id_subnet_tier_1", "id_subnet_tier_2"] 23 | subnet_intra_ids = ["id_subnet_intra_1", "id_subnet_intra_2"] 24 | } 25 | 26 | orchestrator = {} # ... 27 | traffics = [] # ... 28 | } 29 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vistimi/terraform-aws-microservice/694ecbd150510b136b16a7f353089ec13b857338/images/architecture.png -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | tags = merge(var.tags, { VpcId = var.vpc.id }) 3 | } 4 | 5 | module "ecs" { 6 | source = "./modules/ecs" 7 | 8 | name = var.name 9 | vpc = local.vpc 10 | acm = var.acm 11 | route53 = var.route53 12 | bucket_env = var.bucket_env != null ? { 13 | name = join("-", [var.name, "env"]) 14 | file_key = var.bucket_env.file_key 15 | } : null 16 | ecs = { 17 | service = { 18 | name = var.orchestrator.group.name 19 | task = merge( 20 | var.orchestrator.group.deployment, 21 | { 22 | cpu = var.orchestrator.group.fargate != null ? var.orchestrator.group.deployment.cpu : null 23 | memory = var.orchestrator.group.fargate != null ? var.orchestrator.group.deployment.memory : null 24 | containers = [ 25 | for container in var.orchestrator.group.deployment.containers : 26 | merge( 27 | container, 28 | { 29 | cpu = coalesce(container.cpu, try(local.instances[var.orchestrator.group.ec2.instance_types[0]].cpu, null)) 30 | # all instances for one container should have the same memory 31 | memory = coalesce(container.memory, try(local.instances[var.orchestrator.group.ec2.instance_types[0]].memory_available, null)) 32 | device_idxs = coalesce(container.device_idxs, try(range(local.instances[var.orchestrator.group.ec2.instance_types[0]].device_count), null), []) 33 | traffics = [for index, traffic in container.traffics : { 34 | # map the transformed listener and target 35 | listener = local.listeners[container.name][index] 36 | target = traffic.target != null ? local.targets[container.name][index] : null 37 | }] 38 | }, 39 | ) 40 | ] 41 | }, 42 | ) 43 | ec2 = var.orchestrator.group.ec2 != null ? { 44 | key_name = var.orchestrator.group.ec2.key_name 45 | instance_types = var.orchestrator.group.ec2.instance_types 46 | os = var.orchestrator.group.ec2.os 47 | os_version = var.orchestrator.group.ec2.os_version 48 | architecture = values(local.instances)[0].architecture 49 | chip_type = values(local.instances)[0].chip_type 50 | 51 | capacities = var.orchestrator.group.ec2.capacities 52 | asg = var.orchestrator.group.ec2.asg 53 | } : null 54 | fargate = var.orchestrator.group.fargate != null ? { 55 | os = var.orchestrator.group.fargate.os 56 | architecture = var.orchestrator.group.fargate.architecture 57 | capacities = var.orchestrator.group.fargate.capacities 58 | } : null 59 | } 60 | } 61 | 62 | tags = local.tags 63 | } 64 | 65 | # module "eks" { 66 | # source = "./modules/eks" 67 | 68 | # for_each = var.orchestrator.eks != null ? { "${local.name}" = {} } : {} 69 | 70 | # name = local.name 71 | # vpc = local.vpc 72 | # route53 = var.route53 73 | # traffics = var.traffics 74 | # bucket_env = try({ 75 | # name = one(values(module.bucket_env)).bucket.name 76 | # file_key = var.bucket_env.file_key 77 | # }, null) 78 | # eks = { 79 | # create = var.orchestrator.eks != null ? true : false 80 | # cluster_version = var.orchestrator.eks.cluster_version 81 | # group = { 82 | # name = var.orchestrator.group.name 83 | # deployment = var.orchestrator.group.deployment 84 | # ec2 = { 85 | # key_name = var.orchestrator.group.ec2.key_name 86 | # instance_types = var.orchestrator.group.ec2.instance_types 87 | # os = var.orchestrator.group.ec2.os 88 | # os_version = var.orchestrator.group.ec2.os_version 89 | # capacities = var.orchestrator.group.ec2.capacities 90 | 91 | # architecture = one(values(local.instances_specs)).architecture 92 | # chip_type = one(values(local.instances_specs)).chip_type 93 | # } 94 | # fargate = var.orchestrator.group.fargate 95 | # } 96 | # } 97 | 98 | # tags = local.tags 99 | # } 100 | 101 | # ------------------------ 102 | # Bucket env 103 | # ------------------------ 104 | module "bucket_env" { 105 | source = "./modules/bucket" 106 | 107 | for_each = var.bucket_env != null ? { 0 = {} } : {} 108 | 109 | name = var.name 110 | force_destroy = var.bucket_env.force_destroy 111 | versioning = var.bucket_env.versioning 112 | encryption = { 113 | enable = true 114 | } 115 | 116 | tags = local.tags 117 | } 118 | 119 | resource "aws_s3_object" "env" { 120 | for_each = var.bucket_env != null ? { 0 = {} } : {} 121 | 122 | key = var.bucket_env.file_key 123 | bucket = module.bucket_env[each.key].bucket.name 124 | source = var.bucket_env.file_path 125 | server_side_encryption = "aws:kms" 126 | } 127 | -------------------------------------------------------------------------------- /modules/asg/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_partition" "current" {} 3 | data "aws_caller_identity" "current" {} 4 | 5 | locals { 6 | account_id = data.aws_caller_identity.current.account_id 7 | account_arn = data.aws_caller_identity.current.arn 8 | dns_suffix = data.aws_partition.current.dns_suffix // amazonaws.com 9 | partition = data.aws_partition.current.partition // aws 10 | region_name = data.aws_region.current.name 11 | } 12 | -------------------------------------------------------------------------------- /modules/asg/outputs.tf: -------------------------------------------------------------------------------- 1 | # https://registry.terraform.io/module/terraform-aws-modules/autoscaling/aws/6.10.0?utm_content=documentLink&utm_medium=Visual+Studio+Code&utm_source=terraform-ls#outputs 2 | output "autoscaling" { 3 | value = { 4 | group_arn = module.asg.autoscaling_group_arn 5 | group_availability_zones = module.asg.autoscaling_group_availability_zones 6 | group_default_cooldown = module.asg.autoscaling_group_default_cooldown 7 | group_desired_capacity = module.asg.autoscaling_group_desired_capacity 8 | group_enabled_metrics = module.asg.autoscaling_group_enabled_metrics 9 | group_health_check_grace_period = module.asg.autoscaling_group_health_check_grace_period 10 | group_health_check_type = module.asg.autoscaling_group_health_check_type 11 | group_id = module.asg.autoscaling_group_id 12 | group_load_balancers = module.asg.autoscaling_group_load_balancers 13 | group_max_size = module.asg.autoscaling_group_max_size 14 | group_min_size = module.asg.autoscaling_group_min_size 15 | group_name = module.asg.autoscaling_group_name 16 | group_target_group_arns = module.asg.autoscaling_group_target_group_arns 17 | group_vpc_zone_identifier = module.asg.autoscaling_group_vpc_zone_identifier 18 | policy_arns = module.asg.autoscaling_policy_arns 19 | schedule_arns = module.asg.autoscaling_schedule_arns 20 | } 21 | } 22 | 23 | output "iam" { 24 | value = { 25 | instance_profile_arn = module.asg.iam_instance_profile_arn 26 | instance_profile_id = module.asg.iam_instance_profile_id 27 | instance_profile_unique = module.asg.iam_instance_profile_unique 28 | role_arn = module.asg.iam_role_arn 29 | role_name = module.asg.iam_role_name 30 | role_unique_id = module.asg.iam_role_unique_id 31 | } 32 | } 33 | 34 | output "launch" { 35 | value = { 36 | template_arn = module.asg.launch_template_arn 37 | template_default_version = module.asg.launch_template_default_version 38 | template_id = module.asg.launch_template_id 39 | template_latest_version = module.asg.launch_template_latest_version 40 | template_name = module.asg.launch_template_name 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /modules/asg/variable.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The common part of the name used for all resources" 3 | type = string 4 | } 5 | 6 | variable "tags" { 7 | description = "Custom tags to set on the Instances in the ASG" 8 | type = map(string) 9 | default = {} 10 | } 11 | 12 | variable "vpc" { 13 | type = object({ 14 | id = string 15 | subnet_tier_ids = list(string) 16 | }) 17 | } 18 | 19 | variable "port_mapping" { 20 | type = string 21 | nullable = false 22 | 23 | validation { 24 | condition = contains(["dynamic", "host"], var.port_mapping) 25 | error_message = "port mapping must be in [dynamic, host], got ${var.port_mapping}" 26 | } 27 | } 28 | 29 | variable "image_id" { 30 | type = string 31 | nullable = false 32 | } 33 | 34 | variable "user_data_base64" { 35 | type = string 36 | nullable = false 37 | } 38 | 39 | variable "capacity_provider" { 40 | type = object({ 41 | weight = number 42 | }) 43 | nullable = false 44 | } 45 | variable "capacity_weight_total" { 46 | type = number 47 | nullable = false 48 | } 49 | 50 | variable "use_spot" { 51 | type = bool 52 | default = false 53 | } 54 | 55 | variable "instance_type" { 56 | type = string 57 | } 58 | 59 | variable "chip_type" { 60 | type = string 61 | } 62 | 63 | variable "key_name" { 64 | type = string 65 | } 66 | 67 | # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#instance_refresh 68 | variable "instance_refresh" { 69 | type = object({ 70 | strategy = string 71 | preferences = optional(object({ 72 | checkpoint_delay = optional(number) 73 | checkpoint_percentages = optional(list(number)) 74 | instance_warmup = optional(number) 75 | min_healthy_percentage = optional(number) 76 | skip_matching = optional(bool) 77 | auto_rollback = optional(bool) 78 | scale_in_protected_instances = optional(string) 79 | standby_instances = optional(string) 80 | })) 81 | triggers = optional(list(string)) 82 | }) 83 | nullable = false 84 | default = { 85 | strategy = "Rolling" 86 | preferences = { 87 | min_healthy_percentage = 66 88 | auto_rollback = true 89 | scale_in_protected_instances = "Refresh" 90 | standby_instances = "Terminate" 91 | } 92 | triggers = ["tag"] 93 | } 94 | } 95 | 96 | variable "target_group_arns" { 97 | type = list(string) 98 | } 99 | 100 | variable "source_security_group_id" { 101 | type = string 102 | } 103 | 104 | variable "min_count" { 105 | type = number 106 | nullable = false 107 | } 108 | 109 | variable "max_count" { 110 | type = number 111 | nullable = false 112 | } 113 | 114 | variable "desired_count" { 115 | type = number 116 | nullable = false 117 | } 118 | 119 | variable "layer7_to_layer4_mapping" { 120 | type = map(string) 121 | } 122 | 123 | variable "traffics" { 124 | type = list(object({ 125 | listener = object({ 126 | protocol = string 127 | port = number 128 | protocol_version = string 129 | }) 130 | target = object({ 131 | protocol = string 132 | port = number 133 | protocol_version = string 134 | health_check_path = string 135 | status_code = optional(string) 136 | }) 137 | })) 138 | nullable = false 139 | } 140 | 141 | variable "initial_lifecycle_hooks" { 142 | description = "the state of the instance will be influenced by the lifecycle hook. Use it for init or graceful shutdown" 143 | type = list(object({ 144 | name = string 145 | default_result = string 146 | heartbeat_timeout = number 147 | notification_target = string 148 | lifecycle_transition = string 149 | notification_metadata = optional(string) 150 | notification_target_arn = optional(string) 151 | role_arn = optional(string) 152 | })) 153 | nullable = false 154 | default = [] 155 | } 156 | 157 | variable "schedules" { 158 | type = map(object({ 159 | min_size = optional(number) 160 | max_size = optional(number) 161 | desired_capacity = optional(number) 162 | recurrence = optional(string) 163 | start_time = optional(string) 164 | end_time = optional(string) 165 | time_zone = optional(string) 166 | })) 167 | nullable = false 168 | default = {} 169 | } -------------------------------------------------------------------------------- /modules/bucket/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | data "aws_partition" "current" {} 3 | data "aws_region" "current" {} 4 | 5 | locals { 6 | account_id = data.aws_caller_identity.current.account_id 7 | dns_suffix = data.aws_partition.current.dns_suffix // amazonaws.com 8 | partition = data.aws_partition.current.partition // aws 9 | region_name = data.aws_region.current.name 10 | 11 | bucket_name = join("-", [var.name, "env"]) 12 | 13 | // bucket allows those operations 14 | iam_statements = [ 15 | { 16 | actions = ["s3:GetBucketLocation", "s3:ListBucket"] 17 | resources = ["arn:${local.partition}:s3:::${local.bucket_name}"] 18 | effect = "Allow" 19 | }, 20 | { 21 | actions = ["s3:GetObject"] 22 | resources = ["arn:${local.partition}:s3:::${local.bucket_name}/*"] 23 | effect = "Allow" 24 | }, 25 | ] 26 | 27 | iam_conditions = [ 28 | { 29 | test = "ArnLike" 30 | variable = "aws:SourceArn" 31 | values = [ 32 | "arn:${local.partition}:*:${local.region_name}:${local.account_id}:*${var.name}*", 33 | ] 34 | } 35 | ] 36 | } 37 | 38 | resource "aws_kms_key" "objects" { 39 | for_each = var.encryption != null ? { 0 = {} } : {} 40 | 41 | description = "KMS key is used to encrypt bucket objects" 42 | deletion_window_in_days = var.encryption.deletion_window_in_days 43 | 44 | tags = var.tags 45 | } 46 | 47 | resource "aws_kms_alias" "a" { 48 | for_each = var.encryption != null ? { 0 = {} } : {} 49 | 50 | name = "alias/${local.bucket_name}" 51 | target_key_id = aws_kms_key.objects[0].arn 52 | } 53 | 54 | module "s3_bucket" { 55 | source = "terraform-aws-modules/s3-bucket/aws" 56 | version = "3.11.0" 57 | 58 | bucket = local.bucket_name 59 | # acl = "private" # no need if policy is tight 60 | 61 | versioning = var.versioning ? { 62 | enabled = true 63 | } : {} 64 | 65 | attach_policy = true 66 | policy = data.aws_iam_policy_document.bucket_policy.json 67 | force_destroy = var.force_destroy 68 | 69 | # control_object_ownership = true 70 | # object_ownership = "ObjectWriter" 71 | 72 | server_side_encryption_configuration = var.encryption != null ? { 73 | rule = { 74 | apply_server_side_encryption_by_default = { 75 | kms_master_key_id = aws_kms_key.objects[0].arn 76 | sse_algorithm = "aws:kms" 77 | } 78 | } 79 | } : {} 80 | 81 | tags = var.tags 82 | } 83 | 84 | data "aws_iam_policy_document" "bucket_policy" { 85 | dynamic "statement" { 86 | for_each = local.iam_statements 87 | 88 | content { 89 | actions = statement.value.actions 90 | resources = statement.value.resources 91 | effect = statement.value.effect 92 | 93 | principals { 94 | type = "Service" 95 | identifiers = ["ec2.amazonaws.com", "ecs.amazonaws.com", "eks.amazonaws.com"] 96 | } 97 | 98 | dynamic "condition" { 99 | for_each = local.iam_conditions 100 | 101 | content { 102 | test = condition.value.test 103 | variable = condition.value.variable 104 | values = condition.value.values 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modules/bucket/outputs.tf: -------------------------------------------------------------------------------- 1 | output "bucket" { 2 | value = { 3 | arn = module.s3_bucket.s3_bucket_arn 4 | bucket_domain_name = module.s3_bucket.s3_bucket_bucket_domain_name 5 | bucket_regional_domain_name = module.s3_bucket.s3_bucket_bucket_regional_domain_name 6 | hosted_zone_id = module.s3_bucket.s3_bucket_hosted_zone_id 7 | name = module.s3_bucket.s3_bucket_id 8 | lifecycle_configuration_rules = module.s3_bucket.s3_bucket_lifecycle_configuration_rules 9 | policy = module.s3_bucket.s3_bucket_policy 10 | region = module.s3_bucket.s3_bucket_region 11 | website_domain = module.s3_bucket.s3_bucket_website_domain 12 | website_endpoint = module.s3_bucket.s3_bucket_website_endpoint 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/bucket/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the bucket" 3 | type = string 4 | } 5 | 6 | variable "encryption" { 7 | description = "Enable server side encryption" 8 | type = object({ 9 | deletion_window_in_days = optional(number) 10 | }) 11 | default = null 12 | } 13 | 14 | variable "force_destroy" { 15 | description = "If true, will delete the resources that still contain elements" 16 | type = bool 17 | default = true 18 | } 19 | 20 | variable "versioning" { 21 | description = "Enable versioning" 22 | type = bool 23 | default = false 24 | } 25 | 26 | variable "tags" { 27 | description = "Custom tags to set on the Instances in the ASG" 28 | type = map(string) 29 | default = {} 30 | } 31 | -------------------------------------------------------------------------------- /modules/docker/docker.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | files_include = setunion([for f in var.build.path_include : fileset(var.build.context_path, f)]...) 3 | files_exclude = setunion([for f in var.build.path_exclude : fileset(var.build.context_path, f)]...) 4 | files = sort(setsubtract(local.files_include, local.files_exclude)) 5 | dir_sha = sha1(join("", [for f in local.files : filesha1("${var.build.context_path}/${f}")])) 6 | 7 | # ECR 8 | image_tags = compact([try(var.image_tag, null), formatdate("YYYYMMDDhhmmss", timestamp())]) 9 | ecr_image_tags = [for tag in local.image_tags : format("%v:%v", var.repository_url, tag)] 10 | } 11 | 12 | # TODO: push for every tag if many 13 | 14 | # local image 15 | resource "docker_image" "this" { 16 | name = local.ecr_image_tags[0] 17 | 18 | build { 19 | context = var.build.context_path 20 | dockerfile = var.build.file_path 21 | build_args = var.build.args 22 | platform = var.build.platform 23 | tag = local.ecr_image_tags 24 | } 25 | 26 | force_remove = false 27 | keep_locally = false 28 | 29 | triggers = { 30 | dir_sha = local.dir_sha 31 | } 32 | } 33 | 34 | # registry image 35 | resource "docker_registry_image" "this" { 36 | name = docker_image.this.name 37 | 38 | keep_remotely = true 39 | 40 | triggers = { 41 | dir_sha = local.dir_sha 42 | } 43 | } -------------------------------------------------------------------------------- /modules/docker/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dir_sha" { 2 | value = local.dir_sha 3 | } 4 | 5 | output "image_tag" { 6 | value = local.image_tags[0] 7 | } 8 | 9 | output "image" { 10 | value = docker_image.this 11 | } 12 | 13 | output "registry_image" { 14 | value = docker_registry_image.this 15 | } -------------------------------------------------------------------------------- /modules/docker/variables.tf: -------------------------------------------------------------------------------- 1 | variable "repository_url" { 2 | description = "The URL of the ECR repository" 3 | type = string 4 | nullable = false 5 | } 6 | 7 | variable "image_tag" { 8 | description = "The tag of the image" 9 | type = string 10 | default = null 11 | } 12 | 13 | variable "build" { 14 | type = object({ 15 | args = optional(map(string), {}) 16 | platform = string 17 | file_path = optional(string, "Dockerfile") 18 | context_path = optional(string, ".") 19 | path_include = optional(list(string), ["**"]) 20 | path_exclude = optional(list(string), []) 21 | }) 22 | } -------------------------------------------------------------------------------- /modules/docker/version.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | docker = { 4 | source = "kreuzwerker/docker" 5 | version = ">= 3.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/ecr/ecr.tf: -------------------------------------------------------------------------------- 1 | module "ecr" { 2 | source = "terraform-aws-modules/ecr/aws" 3 | version = "2.2.0" 4 | 5 | repository_name = var.name 6 | repository_force_delete = true 7 | repository_image_tag_mutability = "MUTABLE" 8 | 9 | create_lifecycle_policy = var.lifecycle_policy != null 10 | repository_lifecycle_policy = var.lifecycle_policy 11 | 12 | repository_read_access_arns = var.read_access_arns 13 | 14 | tags = var.tags 15 | } -------------------------------------------------------------------------------- /modules/ecr/outputs.tf: -------------------------------------------------------------------------------- 1 | output "repository_arn" { 2 | value = module.ecr.repository_arn 3 | } 4 | 5 | output "repository_name" { 6 | value = module.ecr.repository_name 7 | } 8 | 9 | output "repository_registry_id" { 10 | value = module.ecr.repository_registry_id 11 | } 12 | 13 | output "repository_url" { 14 | value = module.ecr.repository_url 15 | } -------------------------------------------------------------------------------- /modules/ecr/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the ECR repository to create" 3 | type = string 4 | nullable = false 5 | } 6 | 7 | variable "lifecycle_policy" { 8 | description = "The lifecycle policy to apply to the ECR repository" 9 | type = string 10 | default = null 11 | } 12 | 13 | variable "read_access_arns" { 14 | description = "The ARNs of the IAM roles that can read from the ECR repository" 15 | type = list(string) 16 | default = [] 17 | } 18 | 19 | variable "tags" { 20 | description = "The tags to apply to the resources" 21 | type = map(string) 22 | default = {} 23 | } -------------------------------------------------------------------------------- /modules/ecs/README.md: -------------------------------------------------------------------------------- 1 | # ECS 2 | 3 | Instances can be Fargate or EC2 4 | 5 | ```mermaid 6 | flowchart LR 7 | Inbound_traffic -- HTTP(S)/TCP --> ELB -- scaling --> Target_Group -- provision --> ECS_service 8 | ELB -- traffic --> ECS_service 9 | ``` 10 | 11 | ```mermaid 12 | flowchart LR 13 | ECS_service_1 --> task_1 14 | task_1 --> ASG_on_demand & ASG_spot 15 | ASG_on_demand --> instance_on_demand_1 & instance_on_demand_2 16 | ASG_spot --> instance_spot_1 & instance_spot_2 17 | 18 | ECS_service_1 --> task_2... 19 | ``` 20 | 21 | # network mode 22 | 23 | - awsvpc 24 | - for fargate 25 | - bridge 26 | - for EC2 with many instances, it allows dynamic mapping 27 | - host 28 | - for EC2 with a single instance, cannot have dynamic port mapping, hence it is not made for many instances because a port can be taken by only one instance. But it is more performant than bridge network. -------------------------------------------------------------------------------- /modules/ecs/acm.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | requires_certificate = anytrue(flatten([for container in var.ecs.service.task.containers : [for traffic in container.traffics : traffic.listener.protocol == "https"]])) 3 | unique_zone_names = distinct([for zone in try(var.route53.zones, []) : zone.name if local.requires_certificate]) 4 | } 5 | 6 | module "acm" { 7 | source = "terraform-aws-modules/acm/aws" 8 | version = "4.3.2" 9 | 10 | for_each = { for name in local.unique_zone_names : name => {} if var.acm == null } 11 | 12 | create_certificate = true 13 | create_route53_records = true 14 | 15 | key_algorithm = "RSA_2048" 16 | validation_method = "DNS" 17 | 18 | domain_name = var.route53.record.subdomain_name != null ? "${var.route53.record.subdomain_name}.${each.key}" : each.key 19 | zone_id = data.aws_route53_zone.current[each.key].zone_id 20 | 21 | subject_alternative_names = [for prefix in distinct(compact(var.route53.record.prefixes)) : var.route53.record.subdomain_name != null ? "${prefix}.${var.route53.record.subdomain_name}.${each.key}" : "${prefix}.${each.key}"] 22 | 23 | wait_for_validation = true 24 | validation_timeout = "15m" 25 | 26 | tags = var.tags 27 | } 28 | 29 | resource "null_resource" "acm" { 30 | lifecycle { 31 | postcondition { 32 | condition = local.requires_certificate ? one(compact([try(var.acm.arn, null), try(values(module.acm)[0].acm_certificate_arn, null)])) != null : true 33 | error_message = <\\w+)\\.(?P\\d*x*)(?P\\w+)$", instance_type) 6 | instance_type = instance_type 7 | capacity = capacity 8 | } 9 | ]]) : lower(join("-", compact([var.name, substr(obj.capacity.type, 0, 2), "${obj.instance_regex.prefix}-${obj.instance_regex.size_number}${substr(obj.instance_regex.size_name, 0, 1)}"]))) => { instance_type = obj.instance_type, capacity = obj.capacity } 10 | } 11 | 12 | # https://github.com/aws/amazon-ecs-agent/blob/master/README.md 13 | # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-gpu.html 14 | # <<- is required compared to << because there should be no identation for EOT and EOF to work properly 15 | user_data = { 16 | for capacity in try(var.ecs.service.ec2.capacities, []) : capacity.type => <<-EOT 17 | #!/bin/bash 18 | cat <<'EOF' >> /etc/ecs/ecs.config 19 | ECS_CLUSTER=${var.name} 20 | ${capacity.type == "SPOT" ? "ECS_ENABLE_SPOT_INSTANCE_DRAINING=true" : ""} 21 | ECS_ENABLE_TASK_IAM_ROLE=true 22 | ${var.ecs.service.ec2.architecture == "gpu" ? "ECS_ENABLE_GPU_SUPPORT=true" : ""} 23 | ${var.ecs.service.ec2.architecture == "gpu" ? "ECS_NVIDIA_RUNTIME=nvidia" : ""} 24 | ECS_RESERVED_MEMORY=100 25 | EOF 26 | EOT 27 | } 28 | } 29 | 30 | #------------------------ 31 | # EC2 autoscaler 32 | #------------------------ 33 | module "asg" { 34 | source = "../asg" 35 | 36 | for_each = local.asgs 37 | 38 | name = each.key 39 | instance_type = each.value.instance_type 40 | chip_type = var.ecs.service.ec2.chip_type 41 | 42 | capacity_provider = { 43 | weight = each.value.capacity.weight 44 | } 45 | capacity_weight_total = sum([for capacity in var.ecs.service.ec2.capacities : capacity.weight]) 46 | key_name = var.ecs.service.ec2.key_name 47 | instance_refresh = try(var.ecs.service.ec2.asg.instance_refresh, null) 48 | initial_lifecycle_hooks = try(var.ecs.service.ec2.asg.initial_lifecycle_hooks, null) 49 | schedules = try(var.ecs.service.ec2.asg.schedules, null) 50 | use_spot = each.value.capacity.type == "ON_DEMAND" ? false : true 51 | 52 | image_id = local.image_id 53 | user_data_base64 = base64encode(local.user_data[each.value.capacity.type]) 54 | port_mapping = "dynamic" 55 | layer7_to_layer4_mapping = local.layer7_to_layer4_mapping 56 | traffics = flatten([for container in var.ecs.service.task.containers : [for traffic in container.traffics : traffic if traffic.target != null]]) 57 | target_group_arns = [for target_group in module.elb.target_groups : target_group.arn] 58 | source_security_group_id = module.elb.security_group_id 59 | 60 | vpc = var.vpc 61 | min_count = length(local.asgs) > 0 ? ceil(var.ecs.service.task.min_size / length(local.asgs)) : var.ecs.service.task.min_size 62 | max_count = length(local.asgs) > 0 ? ceil(var.ecs.service.task.max_size / length(local.asgs)) : var.ecs.service.task.max_size 63 | desired_count = length(local.asgs) > 0 ? ceil(var.ecs.service.task.desired_size / length(local.asgs)) : var.ecs.service.task.desired_size 64 | 65 | tags = var.tags 66 | } 67 | 68 | # group notification 69 | # resource "aws_autoscaling_notification" "webserver_asg_notifications" { 70 | # } 71 | # resource "aws_sns_topic" "webserver_topic" { 72 | # } 73 | -------------------------------------------------------------------------------- /modules/ecs/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_partition" "current" {} 3 | data "aws_caller_identity" "current" {} 4 | 5 | locals { 6 | account_id = data.aws_caller_identity.current.account_id 7 | account_arn = data.aws_caller_identity.current.arn 8 | dns_suffix = data.aws_partition.current.dns_suffix // amazonaws.com 9 | partition = data.aws_partition.current.partition // aws 10 | region_name = data.aws_region.current.name 11 | } -------------------------------------------------------------------------------- /modules/ecs/docker.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | docker = { 4 | source = "kreuzwerker/docker" 5 | version = ">= 3.0" 6 | } 7 | } 8 | } 9 | 10 | locals { 11 | docker_image_uris = { 12 | for container in var.ecs.service.task.containers : container.name => container.docker.build != null ? ( 13 | module.docker[container.name].image.name 14 | ) : ( 15 | join("/", compact([ 16 | try( 17 | container.docker.registry.ecr.privacy == "private" ? ( 18 | "${coalesce(try(container.docker.registry.ecr.account_id, null), local.account_id)}.dkr.ecr.${container.docker.registry.ecr.privacy == "private" ? coalesce(container.docker.registry.ecr.region_name, local.region_name) : "us-east-1"}.${local.dns_suffix}" 19 | ) : ( 20 | "public.ecr.aws/${container.docker.registry.ecr.public_alias}" 21 | ), 22 | container.docker.registry.name, 23 | null 24 | ), 25 | join(":", compact([local.ecr_repository_names[container.name], try(container.docker.image.tag, "")])) 26 | ])) 27 | ) 28 | } 29 | } 30 | 31 | module "docker" { 32 | source = "../docker" 33 | 34 | for_each = { for container in var.ecs.service.task.containers : container.name => container if container.docker.build != null } 35 | 36 | repository_url = try(module.ecr[each.key].repository_url, data.aws_ecr_repository.given_private[each.key].repository_url, null) 37 | image_tag = try(each.value.docker.image.tag, null) 38 | build = merge( 39 | each.value.docker.build, 40 | { 41 | platform = var.ecs.service.ec2.architecture == "x86_64" ? "linux/amd64" : "linux/arm64" 42 | } 43 | ) 44 | } 45 | 46 | # # make sure the image is available 47 | # data "docker_image" "ecr_external" { 48 | # for_each = { for container in var.ecs.service.task.containers : container.name => container if container.docker.build == null } 49 | 50 | # name = local.docker_image_uris[each.key] 51 | # } -------------------------------------------------------------------------------- /modules/ecs/ecr.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | ecr_services = { 3 | private = "ecr" 4 | public = "ecr-public" 5 | } 6 | 7 | ecr_repository_names = { for container in var.ecs.service.task.containers : container.name => coalesce(try(container.docker.repository.name, null), "${var.name}-${var.ecs.service.name}-${container.name}") } 8 | } 9 | 10 | # TODO: handle registry_id for public repositories 11 | 12 | # Only create repository if it doesn't already exist 13 | module "ecr" { 14 | source = "../ecr" 15 | 16 | for_each = { for container in var.ecs.service.task.containers : container.name => container if try(container.docker.repository.name, null) == null } 17 | 18 | name = local.ecr_repository_names[each.key] 19 | lifecycle_policy = try(each.value.docker.repository.lifecycle_policy, null) 20 | # read_access_arns = module.microservice.ecs.service.task_exec_iam_role_arn 21 | 22 | tags = var.tags 23 | } 24 | 25 | # make sure the private ECR repository exists 26 | data "aws_ecr_repository" "given_private" { 27 | for_each = { for container in var.ecs.service.task.containers : container.name => container if try(container.docker.repository.name, null) != null && try(container.docker.registry.ecr.privacy, null) == "private" } 28 | 29 | name = each.value.docker.repository.name 30 | } -------------------------------------------------------------------------------- /modules/ecs/elb.tf: -------------------------------------------------------------------------------- 1 | module "elb" { 2 | source = "../elb" 3 | 4 | name = var.name 5 | vpc = var.vpc 6 | layer7_to_layer4_mapping = local.layer7_to_layer4_mapping 7 | traffics = flatten([for container in var.ecs.service.task.containers : container.traffics]) 8 | deployment_type = var.ecs.service.ec2 != null ? "ec2" : "fargate" 9 | certificate_arn = one(compact([try(var.acm.arn, null), try(values(module.acm)[0].acm_certificate_arn, null)])) 10 | 11 | tags = var.tags 12 | } -------------------------------------------------------------------------------- /modules/ecs/outputs.tf: -------------------------------------------------------------------------------- 1 | # https://registry.terraform.io/module/terraform-aws-modules/elb/aws/8.6.0?utm_content=documentLink&utm_medium=Visual+Studio+Code&utm_source=terraform-ls#outputs 2 | output "elb" { 3 | value = module.elb 4 | } 5 | 6 | output "acm" { 7 | value = { 8 | for key, acm in module.acm : key => { 9 | acm_certificate_arn = acm.acm_certificate_arn 10 | acm_certificate_domain_validation_options = acm.acm_certificate_domain_validation_options 11 | acm_certificate_status = acm.acm_certificate_status 12 | acm_certificate_validation_emails = acm.acm_certificate_validation_emails 13 | distinct_domain_names = acm.distinct_domain_names 14 | validation_domains = acm.validation_domains 15 | validation_route53_record_fqdns = acm.validation_route53_record_fqdns 16 | } 17 | } 18 | } 19 | 20 | output "route53" { 21 | value = { 22 | records = { 23 | for key, record in module.route53_records : key => { 24 | name = record.name 25 | fqdn = record.fqdn 26 | } 27 | } 28 | } 29 | } 30 | 31 | # https://registry.terraform.io/module/terraform-aws-modules/autoscaling/aws/6.10.0?utm_content=documentLink&utm_medium=Visual+Studio+Code&utm_source=terraform-ls#outputs 32 | output "asg" { 33 | value = module.asg 34 | } 35 | 36 | # https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/outputs.tf 37 | output "cluster" { 38 | value = { 39 | arn = module.ecs_cluster.arn 40 | id = module.ecs_cluster.id 41 | name = module.ecs_cluster.name 42 | cloudwatch_log_group_name = module.ecs_cluster.cloudwatch_log_group_name 43 | cloudwatch_log_group_arn = module.ecs_cluster.cloudwatch_log_group_arn 44 | cluster_capacity_providers = module.ecs_cluster.cluster_capacity_providers 45 | autoscaling_capacity_providers = module.ecs_cluster.autoscaling_capacity_providers 46 | task_exec_iam_role_name = module.ecs_cluster.task_exec_iam_role_name 47 | task_exec_iam_role_arn = module.ecs_cluster.task_exec_iam_role_arn 48 | task_exec_iam_role_unique_id = module.ecs_cluster.task_exec_iam_role_unique_id 49 | } 50 | } 51 | 52 | # https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/module/service/outputs.tf 53 | output "service" { 54 | value = { 55 | # service 56 | id = module.ecs_service.id 57 | name = module.ecs_service.name 58 | # service iam role 59 | iam_role_arn = module.ecs_service.iam_role_arn 60 | iam_role_name = module.ecs_service.iam_role_name 61 | iam_role_unique_id = module.ecs_service.iam_role_unique_id 62 | # container 63 | container_definitions = module.ecs_service.container_definitions 64 | # task definition 65 | task_definition_arn = module.ecs_service.task_definition_arn 66 | task_definition_revision = module.ecs_service.task_definition_revision 67 | task_definition_family = module.ecs_service.task_definition_family 68 | # task execution iam role 69 | task_exec_iam_role_name = module.ecs_service.task_exec_iam_role_name 70 | task_exec_iam_role_arn = module.ecs_service.task_exec_iam_role_arn 71 | task_exec_iam_role_unique_id = module.ecs_service.task_exec_iam_role_unique_id 72 | # task iam role 73 | task_iam_role_arn = module.ecs_service.tasks_iam_role_arn 74 | task_iam_role_name = module.ecs_service.tasks_iam_role_name 75 | task_iam_role_unique_id = module.ecs_service.tasks_iam_role_unique_id 76 | # task set 77 | task_set_id = module.ecs_service.task_set_id 78 | task_set_arn = module.ecs_service.task_set_arn 79 | task_set_stability_status = module.ecs_service.task_set_stability_status 80 | task_set_status = module.ecs_service.task_set_status 81 | # autoscaling 82 | autoscaling_policies = module.ecs_service.autoscaling_policies 83 | autoscaling_scheduled_actions = module.ecs_service.autoscaling_scheduled_actions 84 | # security group 85 | security_group_arn = module.ecs_service.security_group_arn 86 | security_group_id = module.ecs_service.security_group_id 87 | } 88 | } 89 | 90 | output "ecr" { 91 | value = module.ecr 92 | } 93 | 94 | output "docker" { 95 | value = module.docker 96 | } -------------------------------------------------------------------------------- /modules/ecs/route53.tf: -------------------------------------------------------------------------------- 1 | data "aws_route53_zone" "current" { 2 | for_each = { for name in local.unique_zone_names : name => {} } 3 | 4 | name = each.key 5 | private_zone = false 6 | } 7 | 8 | // ecs service discovery is an alternative to route53 9 | module "route53_records" { 10 | source = "../record" 11 | 12 | for_each = { for zone in coalesce(try(var.route53.zones, []), []) : zone.name => {} } 13 | 14 | zone_name = each.key 15 | record = { 16 | subdomain_name = var.route53.record.subdomain_name 17 | prefixes = var.route53.record.prefixes 18 | type = "A" 19 | alias = { 20 | name = "dualstack.${module.elb.dns_name}" 21 | zone_id = module.elb.zone_id 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /modules/ecs/traffic.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # icmp, icmpv6, tcp, udp, or all use the protocol number 3 | # https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 4 | layer7_to_layer4_mapping = { 5 | http = "tcp" 6 | https = "tcp" 7 | tcp = "tcp" 8 | udp = "udp" 9 | tcp_udp = "tcp" 10 | ssl = "tcp" 11 | } 12 | 13 | unique_targets = distinct(flatten([for container in var.ecs.service.task.containers : [for traffic in container.traffics : { 14 | port = traffic.target.port 15 | protocol = traffic.target.protocol 16 | protocol_version = traffic.target.protocol_version 17 | health_check_path = traffic.target.health_check_path 18 | status_code = traffic.target.status_code 19 | } if traffic.target != null]])) 20 | 21 | unique_listeners = distinct(flatten([for container in var.ecs.service.task.containers : [for traffic in container.traffics : { 22 | protocol = traffic.listener.protocol 23 | port = traffic.listener.port 24 | protocol_version = traffic.listener.protocol_version 25 | }]])) 26 | } -------------------------------------------------------------------------------- /modules/ecs/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The common part of the name used for all resources" 3 | type = string 4 | } 5 | 6 | variable "vpc" { 7 | type = object({ 8 | id = string 9 | subnet_tier_ids = list(string) 10 | }) 11 | } 12 | 13 | variable "acm" { 14 | type = object({ 15 | arn = string 16 | }) 17 | default = null 18 | } 19 | 20 | variable "route53" { 21 | type = object({ 22 | zones = list(object({ 23 | name = string 24 | })) 25 | record = object({ 26 | prefixes = optional(list(string)) 27 | subdomain_name = optional(string) 28 | }) 29 | }) 30 | default = null 31 | } 32 | 33 | variable "bucket_env" { 34 | type = object({ 35 | name = string 36 | file_key = string 37 | }) 38 | } 39 | 40 | variable "ecs" { 41 | type = object({ 42 | service = object({ 43 | name = string 44 | task = object({ 45 | min_size = number 46 | max_size = number 47 | desired_size = number 48 | maximum_percent = optional(number) 49 | cpu = number 50 | memory = number 51 | 52 | containers = list(object({ 53 | name = string 54 | base = optional(bool) 55 | cpu = number 56 | memory = number 57 | device_idxs = optional(list(number)) 58 | environments = optional(list(object({ 59 | name = string 60 | value = string 61 | })), []) 62 | docker = object({ 63 | registry = optional(object({ 64 | name = optional(string) 65 | ecr = optional(object({ 66 | privacy = string 67 | public_alias = optional(string, "") 68 | account_id = optional(string) 69 | region_name = optional(string) 70 | })) 71 | })) 72 | repository = object({ 73 | name = optional(string) 74 | lifecycle_policy = optional(string) 75 | }) 76 | image = optional(object({ 77 | tag = string 78 | })) 79 | build = optional(object({ 80 | args = optional(map(string)) 81 | file_path = optional(string) 82 | context_path = optional(string) 83 | path_include = optional(list(string)) 84 | path_exclude = optional(list(string)) 85 | })) 86 | }) 87 | traffics = optional(list(object({ 88 | listener = object({ 89 | protocol = string 90 | port = number 91 | protocol_version = string 92 | redirect = optional(object({ 93 | host = optional(string) 94 | path = optional(string) 95 | port = optional(number) 96 | protocol = optional(string) 97 | query = optional(string) 98 | status_code = optional(number) 99 | })) 100 | }) 101 | target = optional(object({ 102 | protocol = string 103 | port = number 104 | protocol_version = string 105 | health_check_path = string 106 | status_code = optional(string) 107 | })) 108 | }))) 109 | command = optional(list(string), []) 110 | entrypoint = optional(list(string), []) 111 | readonly_root_filesystem = optional(bool) 112 | user = optional(string) 113 | mount_points = optional(list(object({ 114 | s3 = optional(object({ 115 | name = string 116 | })) 117 | container_path = string 118 | read_only = optional(bool) 119 | })), []) 120 | })) 121 | }) 122 | ec2 = optional(object({ 123 | key_name = optional(string) 124 | instance_types = list(string) 125 | os = string 126 | os_version = string 127 | architecture = string 128 | chip_type = string 129 | 130 | asg = optional(object({ 131 | instance_refresh = optional(object({ 132 | strategy = string 133 | preferences = optional(object({ 134 | checkpoint_delay = optional(number) 135 | checkpoint_percentages = optional(list(number)) 136 | instance_warmup = optional(number) 137 | min_healthy_percentage = optional(number) 138 | skip_matching = optional(bool) 139 | auto_rollback = optional(bool) 140 | scale_in_protected_instances = optional(string) 141 | standby_instances = optional(string) 142 | })) 143 | triggers = optional(list(string)) 144 | })) 145 | initial_lifecycle_hooks = optional(list(object({ 146 | name = string 147 | default_result = string 148 | heartbeat_timeout = number 149 | notification_target = string 150 | lifecycle_transition = string 151 | notification_metadata = optional(string) 152 | notification_target_arn = optional(string) 153 | role_arn = optional(string) 154 | }))) 155 | schedules = optional(map(object({ 156 | min_size = optional(number) 157 | max_size = optional(number) 158 | desired_capacity = optional(number) 159 | recurrence = optional(string) 160 | start_time = optional(string) 161 | end_time = optional(string) 162 | time_zone = optional(string) 163 | }))) 164 | })) 165 | capacities = optional(list(object({ 166 | type = optional(string, "ON_DEMAND") 167 | base = optional(number) 168 | weight = optional(number, 1) 169 | target_capacity_cpu_percent = optional(number, 66) 170 | maximum_scaling_step_size = optional(number) 171 | minimum_scaling_step_size = optional(number) 172 | }))) 173 | })) 174 | fargate = optional(object({ 175 | os = string 176 | architecture = string 177 | 178 | capacities = optional(list(object({ 179 | type = optional(string, "ON_DEMAND") 180 | base = optional(number) 181 | weight = optional(number, 1) 182 | target_capacity_cpu_percent = optional(number, 66) 183 | }))) 184 | })) 185 | }) 186 | }) 187 | } 188 | 189 | variable "tags" { 190 | description = "Custom tags to set on the Instances in the ASG" 191 | type = map(string) 192 | default = {} 193 | } -------------------------------------------------------------------------------- /modules/eks/README.md: -------------------------------------------------------------------------------- 1 | EKS Managed Node Groups -------------------------------------------------------------------------------- /modules/eks/ami.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html 3 | ami_ssm_name = { 4 | amazon-linux-2-x86_64-cpu = "/aws/service/eks/optimized-ami/${var.eks.cluster_version}/amazon-linux-2/recommended/image_id" 5 | amazon-linux-2-x86_64-gpu = "/aws/service/eks/optimized-ami/${var.eks.cluster_version}/amazon-linux-2-gpu/recommended/image_id" 6 | 7 | amazon-linux-2-arm64-cpu = "/aws/service/eks/optimized-ami/${var.eks.cluster_version}/amazon-linux-2-arm64/recommended/image_id" 8 | 9 | amazon-bottlerocket-latest-x86_64-cpu = "/aws/service/bottlerocket/aws-k8s-1.27/x86_64/latest/image_id" 10 | amazon-bottlerocket-latest-x86_64-gpu = "/aws/service/bottlerocket/aws-k8s-1.27-nvidia/x86_64/latest/image_id" 11 | 12 | amazon-bottlerocket-latest-arm64-cpu = "/aws/service/bottlerocket/aws-k8s-1.27/arm64/latest/image_id" 13 | amazon-bottlerocket-latest-arm64-gpu = "/aws/service/bottlerocket/aws-k8s-1.27-nvidia/arm64/latest/image_id" 14 | } 15 | } 16 | 17 | data "aws_ssm_parameter" "eks_optimized_ami_id" { 18 | # TODO: handle no ec2 19 | name = local.ami_ssm_name[join("-", ["amazon", var.eks.group.ec2.os, var.eks.group.ec2.os_version, var.eks.group.ec2.architecture, var.eks.group.ec2.chip_type])] 20 | } 21 | 22 | locals { 23 | image_id = data.aws_ssm_parameter.eks_optimized_ami_id.value 24 | } 25 | -------------------------------------------------------------------------------- /modules/eks/cluster-role.tf: -------------------------------------------------------------------------------- 1 | # # Create AWS iam role for EKS service. 2 | # resource "aws_iam_role" "iam_role" { 3 | # name = format("%s-eks-role", var.name) 4 | # force_detach_policies = true 5 | # tags = var.tags 6 | # assume_role_policy = jsonencode({ 7 | # Statement = [ 8 | # { 9 | # Action = "sts:AssumeRole" 10 | # Effect = "Allow" 11 | # Principal = { 12 | # Service = "eks.amazonaws.com" 13 | # } 14 | # } 15 | # ] 16 | # Version = "2012-10-17" 17 | # }) 18 | # lifecycle { 19 | # create_before_destroy = true 20 | # } 21 | # } 22 | 23 | # # create AWS cluster iam role policy attachment. 24 | # resource "aws_iam_role_policy_attachment" "clusterPolicy_iam_role_policy_attachment" { 25 | # policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" 26 | # role = aws_iam_role.iam_role.name 27 | # } 28 | 29 | # # Create AWS service iam role policy attachment. 30 | # resource "aws_iam_role_policy_attachment" "servicePolicy_iam_role_policy_attachment" { 31 | # policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy" 32 | # role = aws_iam_role.iam_role.name 33 | # } 34 | 35 | # # Create AWS EKSVPCResourceController iam role policy attachment. Enable Security Groups for Pods. 36 | # # Reference: https://docs.aws.amazon.com/eks/latest/userguide/security-groups-for-pods.html 37 | # resource "aws_iam_role_policy_attachment" "EKSVPCResourceController_iam_role_policy_attachment" { 38 | # policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController" 39 | # role = aws_iam_role.iam_role.name 40 | # } 41 | -------------------------------------------------------------------------------- /modules/eks/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_partition" "current" {} 3 | data "aws_caller_identity" "current" {} 4 | 5 | data "aws_vpc" "current" { 6 | id = var.vpc.id 7 | } 8 | data "aws_subnets" "tier" { 9 | filter { 10 | name = "vpc-id" 11 | values = [data.aws_vpc.current.id] 12 | } 13 | tags = { 14 | Tier = var.vpc.tier 15 | } 16 | 17 | lifecycle { 18 | postcondition { 19 | condition = length(self.ids) >= 2 20 | error_message = "For a Load Balancer: At least two ${var.vpc.tier} subnets in two different Availability Zones must be specified, tier: ${var.vpc.tier}, subnets: ${jsonencode(self.ids)}" 21 | } 22 | } 23 | } 24 | 25 | 26 | 27 | locals { 28 | account_id = data.aws_caller_identity.current.account_id 29 | account_arn = data.aws_caller_identity.current.arn 30 | dns_suffix = data.aws_partition.current.dns_suffix // amazonaws.com 31 | partition = data.aws_partition.current.partition // aws 32 | region_name = data.aws_region.current.name 33 | 34 | # icmp, icmpv6, tcp, udp, or all use the protocol number 35 | # https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 36 | layer7_to_layer4_mapping = { 37 | http = "tcp" 38 | https = "tcp" 39 | tcp = "tcp" 40 | udp = "udp" 41 | tcp_udp = "tcp" 42 | ssl = "tcp" 43 | } 44 | 45 | ecr_services = { 46 | private = "ecr" 47 | public = "ecr-public" 48 | } 49 | fargate_os = { 50 | linux = "LINUX" 51 | } 52 | fargate_architecture = { 53 | x86_64 = "X86_64" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/eks/ec2-role.tf: -------------------------------------------------------------------------------- 1 | # locals { 2 | # create_ec2 = var.eks.group.ec2 != null ? { "${var.name}" = {} } : {} 3 | # } 4 | 5 | # # Create AWS worker iam role. 6 | # resource "aws_iam_role" "worker_iam_role" { 7 | # for_each = local.create_ec2 8 | 9 | # name = format("%s-eks-worker-role", var.name) 10 | # force_detach_policies = true 11 | # tags = var.tags 12 | # assume_role_policy = jsonencode({ 13 | # Statement = [ 14 | # { 15 | # Action = "sts:AssumeRole" 16 | # Effect = "Allow" 17 | # Principal = { 18 | # Service = "ec2.amazonaws.com" 19 | # } 20 | # } 21 | # ] 22 | # Version = "2012-10-17" 23 | # }) 24 | # lifecycle { 25 | # create_before_destroy = true 26 | # } 27 | # } 28 | 29 | # # Create AWS workernode iam role policy attachment. 30 | # resource "aws_iam_role_policy_attachment" "WorkerNode_iam_role_policy_attachment" { 31 | # for_each = local.create_ec2 32 | 33 | # policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" 34 | # role = aws_iam_role.worker_iam_role[each.key].name 35 | # } 36 | 37 | # # Create AWS iam role CNI policy attachment. 38 | # resource "aws_iam_role_policy_attachment" "CNI_policy_iam_role_policy_attachment" { 39 | # for_each = local.create_ec2 40 | 41 | # policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" 42 | # role = aws_iam_role.worker_iam_role[each.key].name 43 | # } 44 | 45 | # # Create AWS EC2Container registry read only iam role policy attachment. 46 | # resource "aws_iam_role_policy_attachment" "EC2ContainerRegistryReadOnly_iam_role_policy_attachment" { 47 | # for_each = local.create_ec2 48 | 49 | # policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" 50 | # role = aws_iam_role.worker_iam_role[each.key].name 51 | # } 52 | 53 | # # Create AWS iam instance profile group. 54 | # resource "aws_iam_instance_profile" "iam_instance_profile" { 55 | # for_each = local.create_ec2 56 | 57 | # name = format("%s-eks-instance-profile", var.name) 58 | # role = aws_iam_role.worker_iam_role[each.key].name 59 | 60 | # lifecycle { 61 | # create_before_destroy = true 62 | # } 63 | # } 64 | -------------------------------------------------------------------------------- /modules/eks/elb.tf: -------------------------------------------------------------------------------- 1 | # # ----------------- 2 | # # ACM 3 | # # ----------------- 4 | # data "aws_route53_zone" "current" { 5 | # for_each = { 6 | # for name in flatten([ 7 | # for traffic in var.traffics : [ 8 | # for zone in try(var.route53.zones, []) : zone.name 9 | # ] if traffic.listener.protocol == "https" 10 | # ]) : name => {} 11 | # } 12 | 13 | # name = each.key 14 | # private_zone = false 15 | # } 16 | 17 | # module "acm" { 18 | # source = "terraform-aws-modules/acm/aws" 19 | # version = "4.3.2" 20 | 21 | # for_each = { 22 | # for name in flatten([ 23 | # for traffic in var.traffics : [ 24 | # for zone in try(var.route53.zones, []) : zone.name 25 | # ] if traffic.listener.protocol == "https" 26 | # ]) : name => {} 27 | # } 28 | 29 | # create_certificate = true 30 | # create_route53_records = true 31 | 32 | # key_algorithm = "RSA_2048" 33 | # validation_method = "DNS" 34 | 35 | # domain_name = var.route53.record.subdomain_name != null ? "${var.route53.record.subdomain_name}.${each.key}" : each.key 36 | # zone_id = data.aws_route53_zone.current[each.key].zone_id 37 | 38 | # subject_alternative_names = [for prefix in distinct(compact(var.route53.record.prefixes)) : var.route53.record.subdomain_name != null ? "${prefix}.${var.route53.record.subdomain_name}.${each.key}" : "${prefix}.${each.key}"] 39 | 40 | # wait_for_validation = true 41 | # validation_timeout = "15m" 42 | 43 | # tags = var.tags 44 | # } 45 | 46 | # # ----------------- 47 | # # Route53 48 | # # ----------------- 49 | # // ecs service discovery is alternative to route53 50 | # module "route53_records" { 51 | # source = "../record" 52 | 53 | # for_each = { for zone in coalesce(try(var.route53.zones, []), []) : zone.name => {} } 54 | 55 | # zone_name = each.key 56 | # record = { 57 | # subdomain_name = var.route53.record.subdomain_name 58 | # prefixes = var.route53.record.prefixes 59 | # type = "A" 60 | # alias = { 61 | # name = "dualstack.${module.elb.dns_name}" 62 | # zone_id = module.elb.zone_id 63 | # } 64 | # } 65 | # } 66 | 67 | # module "elb" { 68 | # source = "../elb" 69 | 70 | # name = var.name 71 | # vpc = var.vpc 72 | # layer7_to_layer4_mapping = local.layer7_to_layer4_mapping 73 | # traffics = var.traffics 74 | # deployment_type = var.service.deployment_type 75 | # certificate_arn = try(one(values(module.acm)).acm_certificate_arn, null) 76 | 77 | # tags = var.tags 78 | # } 79 | -------------------------------------------------------------------------------- /modules/eks/microservice-role.tf: -------------------------------------------------------------------------------- 1 | # # Create AWS LabelStudio iam role policy attachment to the worker nodes. 2 | # resource "aws_iam_role_policy_attachment" "microservice_role_policy_attachment" { 3 | # policy_arn = aws_iam_policy.microservice_iam_policy.arn 4 | # role = aws_iam_role.worker_iam_role.name 5 | # } 6 | 7 | # # Create AWS iam policy document to create access to the s3 bucket to store LS files(upload, avatars, exports). 8 | # resource "aws_iam_policy" "microservice_iam_policy" { 9 | # name = format("%s-eks-ls-s3-access", var.name) 10 | 11 | # policy = jsonencode({ 12 | # Version = "2012-10-17" 13 | # Statement = [ 14 | # { 15 | # Action = [] 16 | # Effect = "Allow" 17 | # Resource = ["*"] 18 | # }, 19 | # ] 20 | # }) 21 | # } 22 | -------------------------------------------------------------------------------- /modules/eks/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster" { 2 | value = { 3 | arn = module.eks.cluster_arn 4 | certificate_authority_data = module.eks.cluster_certificate_authority_data 5 | endpoint = module.eks.cluster_endpoint 6 | id = module.eks.cluster_id 7 | name = module.eks.cluster_name 8 | oidc_issuer_url = module.eks.cluster_oidc_issuer_url 9 | version = module.eks.cluster_version 10 | platform_version = module.eks.cluster_platform_version 11 | status = module.eks.cluster_status 12 | primary_security_group_id = module.eks.cluster_primary_security_group_id 13 | 14 | addons = module.eks.cluster_addons 15 | 16 | identity_providers = module.eks.cluster_identity_providers 17 | } 18 | } 19 | 20 | output "kms_key" { 21 | value = { 22 | arn = module.eks.kms_key_arn 23 | id = module.eks.kms_key_id 24 | policy = module.eks.kms_key_policy 25 | } 26 | } 27 | 28 | output "cluster_security_group" { 29 | value = { 30 | arn = module.eks.cluster_security_group_arn 31 | id = module.eks.cluster_security_group_id 32 | } 33 | } 34 | 35 | output "node_security_group" { 36 | value = { 37 | arn = module.eks.node_security_group_arn 38 | id = module.eks.node_security_group_id 39 | } 40 | } 41 | 42 | output "oidc_provider" { 43 | value = { 44 | url = module.eks.oidc_provider 45 | arn = module.eks.oidc_provider_arn 46 | certificate = module.eks.cluster_tls_certificate_sha1_fingerprint 47 | } 48 | } 49 | 50 | output "cluster_iam_role" { 51 | value = { 52 | name = module.eks.cluster_iam_role_name 53 | arn = module.eks.cluster_iam_role_arn 54 | unique_id = module.eks.cluster_iam_role_unique_id 55 | } 56 | } 57 | 58 | output "cloudwatch_log_group" { 59 | value = { 60 | name = module.eks.cloudwatch_log_group_name 61 | arn = module.eks.cloudwatch_log_group_arn 62 | } 63 | } 64 | 65 | output "fargate_profiles" { 66 | value = module.eks.fargate_profiles 67 | } 68 | 69 | output "eks_managed_node_groups" { 70 | value = { 71 | attributes = module.eks.eks_managed_node_groups 72 | autoscaling_group_names = module.eks.eks_managed_node_groups_autoscaling_group_names 73 | } 74 | } 75 | 76 | output "aws_auth_configmap_yaml" { 77 | value = module.eks.aws_auth_configmap_yaml 78 | } 79 | -------------------------------------------------------------------------------- /modules/eks/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The common part of the name used for all resources" 3 | type = string 4 | } 5 | 6 | variable "tags" { 7 | description = "Custom tags to set on the Instances in the ASG" 8 | type = map(string) 9 | default = {} 10 | } 11 | 12 | variable "vpc" { 13 | type = object({ 14 | id = string 15 | subnet_tier_ids = list(string) 16 | subnet_intra_ids = list(string) 17 | }) 18 | } 19 | 20 | variable "route53" { 21 | type = object({ 22 | zones = list(object({ 23 | name = string 24 | })) 25 | record = object({ 26 | prefixes = optional(list(string)) 27 | subdomain_name = optional(string) 28 | }) 29 | }) 30 | default = null 31 | } 32 | 33 | variable "bucket_env" { 34 | type = object({ 35 | name = string 36 | file_key = string 37 | }) 38 | default = null 39 | } 40 | 41 | variable "traffics" { 42 | type = list(object({ 43 | listener = object({ 44 | protocol = string 45 | port = optional(number) 46 | protocol_version = optional(string) 47 | }) 48 | target = object({ 49 | protocol = string 50 | port = number 51 | protocol_version = optional(string) 52 | health_check_path = optional(string) 53 | status_code = optional(string) 54 | }) 55 | base = optional(bool) 56 | })) 57 | } 58 | 59 | variable "eks" { 60 | type = object({ 61 | create = optional(bool, true) 62 | cluster_version = string 63 | group = object({ 64 | name = string 65 | deployment = object({ 66 | min_size = number 67 | max_size = number 68 | desired_size = number 69 | maximum_percent = optional(number) 70 | 71 | container = object({ 72 | name = string 73 | }) 74 | }) 75 | ec2 = optional(object({ 76 | key_name = optional(string) 77 | instance_types = list(string) 78 | os = string 79 | os_version = string 80 | architecture = string 81 | chip_type = string 82 | 83 | capacities = optional(list(object({ 84 | type = optional(string, "ON_DEMAND") 85 | }))) 86 | })) 87 | fargate = optional(object({})) 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /modules/elb/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_partition" "current" {} 3 | data "aws_caller_identity" "current" {} 4 | 5 | locals { 6 | account_id = data.aws_caller_identity.current.account_id 7 | account_arn = data.aws_caller_identity.current.arn 8 | dns_suffix = data.aws_partition.current.dns_suffix // amazonaws.com 9 | partition = data.aws_partition.current.partition // aws 10 | region_name = data.aws_region.current.name 11 | } 12 | -------------------------------------------------------------------------------- /modules/elb/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | protocols = { 3 | http = "HTTP" 4 | https = "HTTPS" 5 | tcp = "TCP" 6 | udp = "UDP" 7 | tcp_udp = "TCP_UDP" 8 | ssl = "SSL" 9 | } 10 | protocol_versions = { 11 | http1 = "HTTP1" 12 | http2 = "HTTP2" 13 | grpc = "GRPC" 14 | } 15 | 16 | load_balancer_types = { 17 | http = "application" 18 | https = "application" 19 | tls = "network" 20 | tcp = "network" 21 | tcp_udp = "network" 22 | udp = "network" 23 | } 24 | 25 | # unique_targets = { 26 | # for traffic in var.traffics : join("-", [traffic.target.protocol, traffic.target.port]) => { 27 | # port = traffic.target.port 28 | # protocol = traffic.target.protocol 29 | # protocol_version = traffic.target.protocol_version 30 | # health_check_path = traffic.target.health_check_path 31 | # status_code = traffic.target.status_code 32 | # } if traffic.target != null 33 | # } 34 | 35 | unique_targets = { 36 | for traffic in distinct([for traffic in var.traffics : { 37 | port = traffic.target.port 38 | protocol = traffic.target.protocol 39 | protocol_version = traffic.target.protocol_version 40 | health_check_path = traffic.target.health_check_path 41 | status_code = traffic.target.status_code 42 | } if traffic.target != null]) : join("-", [traffic.protocol, traffic.port]) => traffic 43 | } 44 | 45 | unique_listeners = { 46 | for traffic in var.traffics : join("-", [traffic.listener.protocol, traffic.listener.port]) => { 47 | protocol = traffic.listener.protocol 48 | port = traffic.listener.port 49 | protocol_version = traffic.listener.protocol_version 50 | } 51 | } 52 | } 53 | 54 | # Cognito for authentication: https://github.com/terraform-aws-modules/terraform-aws-alb/blob/master/examples/complete-alb/main.tf 55 | module "elb" { 56 | source = "terraform-aws-modules/alb/aws" 57 | version = "9.9.0" 58 | 59 | name = var.name 60 | enable_deletion_protection = false 61 | 62 | load_balancer_type = local.load_balancer_types[var.traffics[0].listener.protocol] // map listener base to load balancer 63 | 64 | vpc_id = var.vpc.id 65 | subnets = var.vpc.subnet_tier_ids 66 | 67 | security_group_ingress_rules = { 68 | for listener in local.unique_listeners : "${listener.protocol}-${listener.port}" => { 69 | from_port = listener.port 70 | to_port = listener.port 71 | protocol = var.layer7_to_layer4_mapping[listener.protocol] 72 | description = "listener port ${var.layer7_to_layer4_mapping[listener.protocol]} ${listener.port}" 73 | cidr_ipv4 = "0.0.0.0/0" 74 | } if contains(["http", "https"], listener.protocol) 75 | # local.load_balancer_types[var.traffics[0].listener.protocol] == "application" 76 | } 77 | security_group_egress_rules = { 78 | all = { 79 | ip_protocol = "-1" 80 | cidr_ipv4 = "0.0.0.0/0" 81 | } 82 | } 83 | 84 | listeners = merge( 85 | { 86 | for traffic in var.traffics : join("-", [var.name, traffic.listener.protocol, traffic.listener.port]) => { 87 | port = traffic.listener.port 88 | protocol = local.protocols[traffic.listener.protocol] 89 | redirect = merge( 90 | traffic.listener.redirect, 91 | { 92 | port = "${traffic.listener.redirect.port}" 93 | protocol = local.protocols[traffic.listener.redirect.protocol] 94 | status_code = "HTTP_${traffic.listener.redirect.status_code}" 95 | } 96 | ) 97 | } if traffic.listener.redirect != null 98 | }, 99 | { 100 | for traffic in var.traffics : join("-", [var.name, traffic.listener.protocol, traffic.listener.port]) => { 101 | port = traffic.listener.port 102 | protocol = local.protocols[traffic.listener.protocol] 103 | certificate_arn = traffic.listener.protocol == "https" ? var.certificate_arn : null 104 | forward = { 105 | target_group_key = one([for key, target in local.unique_targets : key if target.port == traffic.target.port]) 106 | } 107 | } if traffic.listener.redirect == null 108 | } 109 | ) 110 | 111 | // forward listener to target 112 | // HTTP2 can work for grpc and rest 113 | // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-protocol-version 114 | target_groups = { 115 | for key, target in local.unique_targets : key => { 116 | name = join("-", [var.name, key]) 117 | protocol = local.protocols[target.protocol] 118 | port = target.port 119 | target_type = var.deployment_type == "fargate" ? "ip" : "instance" # "ip" for awsvpc network, instance for host or bridge 120 | name = join("-", [var.name, key]) 121 | protocol = local.protocols[target.protocol] 122 | port = target.port 123 | target_type = var.deployment_type == "fargate" ? "ip" : "instance" # "ip" for awsvpc network, instance for host or bridge 124 | health_check = { 125 | enabled = true 126 | interval = 15 // seconds before new request 127 | path = target.health_check_path 128 | port = var.deployment_type == "ec2" ? null : target.port // traffic port by default 129 | healthy_threshold = 3 // consecutive health check failures before healthy 130 | unhealthy_threshold = 3 // consecutive health check failures before unhealthy 131 | timeout = 5 // seconds for timeout of request 132 | protocol = local.protocols[target.protocol] 133 | matcher = target.status_code != null ? target.status_code : ( 134 | contains(["http", "http2"], target.protocol_version) ? "200-299" : (contains(["grpc"], target.protocol_version) ? "0" : null) 135 | ) 136 | } 137 | protocol_version = try(local.protocol_versions[target.protocol_version], null) 138 | create_attachment = false 139 | } 140 | } 141 | 142 | tags = var.tags 143 | } -------------------------------------------------------------------------------- /modules/elb/outputs.tf: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Load Balancer 3 | ################################################################################ 4 | 5 | output "id" { 6 | description = "The ID and ARN of the load balancer we created" 7 | value = module.elb.id 8 | } 9 | 10 | output "arn" { 11 | description = "The ID and ARN of the load balancer we created" 12 | value = module.elb.arn 13 | } 14 | 15 | output "arn_suffix" { 16 | description = "ARN suffix of our load balancer - can be used with CloudWatch" 17 | value = module.elb.arn_suffix 18 | } 19 | 20 | output "dns_name" { 21 | description = "The DNS name of the load balancer" 22 | value = module.elb.dns_name 23 | } 24 | 25 | output "zone_id" { 26 | description = "The zone_id of the load balancer to assist with creating DNS records" 27 | value = module.elb.zone_id 28 | } 29 | 30 | ################################################################################ 31 | # Listener(s) 32 | ################################################################################ 33 | 34 | output "listeners" { 35 | description = "Map of listeners created and their attributes" 36 | value = module.elb.listeners 37 | } 38 | 39 | output "listener_rules" { 40 | description = "Map of listeners rules created and their attributes" 41 | value = module.elb.listener_rules 42 | } 43 | 44 | ################################################################################ 45 | # Target Group(s) 46 | ################################################################################ 47 | 48 | output "target_groups" { 49 | description = "Map of target groups created and their attributes" 50 | value = module.elb.target_groups 51 | } 52 | 53 | ################################################################################ 54 | # Security Group 55 | ################################################################################ 56 | 57 | output "security_group_arn" { 58 | description = "Amazon Resource Name (ARN) of the security group" 59 | value = module.elb.security_group_arn 60 | } 61 | 62 | output "security_group_id" { 63 | description = "ID of the security group" 64 | value = module.elb.security_group_id 65 | } 66 | 67 | ################################################################################ 68 | # Route53 Record(s) 69 | ################################################################################ 70 | 71 | output "route53_records" { 72 | description = "The Route53 records created and attached to the load balancer" 73 | value = module.elb.route53_records 74 | } -------------------------------------------------------------------------------- /modules/elb/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The common part of the name used for all resources" 3 | type = string 4 | } 5 | 6 | variable "tags" { 7 | description = "Custom tags to set on the Instances in the ASG" 8 | type = map(string) 9 | default = {} 10 | } 11 | 12 | variable "vpc" { 13 | type = object({ 14 | id = string 15 | subnet_tier_ids = list(string) 16 | }) 17 | } 18 | 19 | variable "layer7_to_layer4_mapping" { 20 | type = map(string) 21 | } 22 | 23 | variable "traffics" { 24 | type = list(object({ 25 | listener = object({ 26 | protocol = string 27 | port = number 28 | protocol_version = string 29 | redirect = optional(object({ 30 | host = optional(string) 31 | path = optional(string) 32 | port = optional(number, 80) 33 | protocol = optional(string, "http") 34 | query = optional(string) 35 | status_code = optional(number, 301) 36 | })) 37 | }) 38 | target = optional(object({ 39 | protocol = string 40 | port = number 41 | protocol_version = string 42 | health_check_path = string 43 | status_code = optional(string) 44 | })) 45 | })) 46 | nullable = false 47 | 48 | validation { 49 | condition = length(var.traffics) > 0 50 | error_message = "traffics must have at least one element" 51 | } 52 | 53 | validation { 54 | condition = length([for traffic in var.traffics : traffic.target if traffic.target != null]) > 0 55 | error_message = "traffics must have at least one target" 56 | } 57 | 58 | validation { 59 | condition = alltrue([for traffic in var.traffics : traffic.target != null if traffic.listener.redirect == null]) 60 | error_message = "target must be set if listener.redirect is not set" 61 | } 62 | 63 | validation { 64 | condition = alltrue([for traffic in var.traffics : traffic.target == null if traffic.listener.redirect != null]) 65 | error_message = "target must not be set if listener.redirect is set" 66 | } 67 | } 68 | 69 | resource "null_resource" "traffics" { 70 | lifecycle { 71 | precondition { 72 | condition = length(distinct([for traffic in var.traffics : local.load_balancer_types[traffic.listener.protocol]])) == 1 73 | error_message = "listeners must either use http/https or tls/tcp/tcp_udp/udp, not both/none: ${jsonencode(distinct([for traffic in var.traffics : local.load_balancer_types[traffic.listener.protocol]]))}" 74 | } 75 | } 76 | } 77 | 78 | variable "deployment_type" { 79 | type = string 80 | nullable = false 81 | } 82 | 83 | variable "certificate_arn" { 84 | type = string 85 | default = null 86 | } 87 | -------------------------------------------------------------------------------- /modules/record/main.tf: -------------------------------------------------------------------------------- 1 | # data "aws_route53_zone" "this" { 2 | # name = var.zone_name 3 | # private_zone = false 4 | # } 5 | 6 | module "records" { 7 | source = "terraform-aws-modules/route53/aws//modules/records" 8 | version = "2.10.2" 9 | 10 | # zone_id = data.aws_route53_zone.this.zone_id 11 | zone_name = var.zone_name 12 | private_zone = false 13 | 14 | # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html 15 | records = [ 16 | for prefix in setunion(compact(var.record.prefixes), [""]) : 17 | { 18 | name = trimprefix(var.record.subdomain_name != null ? "${prefix}.${var.record.subdomain_name}" : prefix, ".") 19 | type = var.record.type 20 | alias = var.record.alias 21 | ttl = var.record.ttl 22 | records = var.record.records 23 | set_identifier = var.record.set_identifier 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /modules/record/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = module.records.route53_record_name 3 | } 4 | 5 | output "fqdn" { 6 | value = module.records.route53_record_fqdn 7 | } 8 | -------------------------------------------------------------------------------- /modules/record/variables.tf: -------------------------------------------------------------------------------- 1 | variable "zone_name" { 2 | description = "The name of the hosted zone" 3 | type = string 4 | } 5 | 6 | variable "record" { 7 | description = "The record configuration" 8 | type = object({ 9 | subdomain_name = optional(string) 10 | prefixes = optional(list(string), []) 11 | type = string 12 | alias = optional(object({ 13 | name = string 14 | zone_id = string 15 | })) 16 | ttl = optional(number) 17 | records = optional(list(string)) 18 | set_identifier = optional(string) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "ecs" { 2 | value = module.ecs 3 | } 4 | 5 | # output "eks" { 6 | # value = one(values(module.eks)) 7 | # } 8 | 9 | output "env" { 10 | value = module.bucket_env 11 | } 12 | 13 | output "instances" { 14 | value = local.instances 15 | } -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # test 2 | 3 | For docker add `sudo` to the command. 4 | To test a unique file in the devcontainer, run the following command: 5 | 6 | ```bash 7 | terraform init 8 | terraform test -filter=tests/rest_ecs_ec2_docker.tftest.hcl 9 | ``` 10 | 11 | # clean 12 | ```bash 13 | cloud-nuke aws \ 14 | --region us-east-1 \ 15 | --exclude-resource-type vpc \ 16 | --exclude-resource-type transit-gateway \ 17 | --exclude-resource-type transit-gateway-attachment \ 18 | --exclude-resource-type transit-gateway-route-table \ 19 | --force 20 | 21 | for policyArn in $(aws iam list-policies --max-items 200 --scope 'Local' --no-only-attached --query 'Policies[].Arn' | tr -d '[],' | tr -d '"') 22 | do 23 | echo $policyArn 24 | for groupName in $(aws iam list-entities-for-policy --policy-arn $policyArn --query 'PolicyGroups[].GroupName' | tr -d '[],' | tr -d '"') 25 | do 26 | aws iam detach-group-policy --group-name $groupName --policy-arn $policyArn || true 27 | done 28 | for userName in $(aws iam list-entities-for-policy --policy-arn $policyArn --query 'PolicyUsers[].UserName' | tr -d '[],' | tr -d '"') 29 | do 30 | aws iam detach-user-policy --user-name $userName --policy-arn $policyArn || true 31 | done 32 | for roleName in $(aws iam list-entities-for-policy --policy-arn $policyArn --query 'PolicyRoles[].RoleName' | tr -d '[],' | tr -d '"') 33 | do 34 | aws iam detach-role-policy --role-name $roleName --policy-arn $policyArn || true 35 | done 36 | for version in $(aws iam list-policy-versions --policy-arn $policyArn --query 'Versions[?IsDefaultVersion==`false`].VersionId' | tr -d '[],' | tr -d '"') 37 | do 38 | aws iam delete-policy-version --policy-arn $policyArn --version-id $version 39 | done 40 | aws iam delete-policy --policy-arn $policyArn 41 | done 42 | 43 | for roleName in $(aws iam list-roles --max-items 200 --query 'Roles[].RoleName' | tr -d '[],' | tr -d '"') 44 | do 45 | echo $roleName 46 | 47 | # Detach attached policies 48 | attached_policies=$(aws iam list-attached-role-policies --role-name $roleName --query 'AttachedPolicies[].PolicyArn' | tr -d '[],' | tr -d '"') 49 | for policy_arn in $attached_policies; do 50 | aws iam detach-role-policy --role-name $roleName --policy-arn $policy_arn 51 | echo "Detached policy: $policy_arn" 52 | done 53 | 54 | # Get instance profiles and remove role 55 | for profile in $(aws iam list-instance-profiles-for-role --role-name $roleName --query 'InstanceProfiles[].InstanceProfileName' | tr -d '[],' | tr -d '"') 56 | do 57 | echo "Removing role from profile: $profile" 58 | aws iam remove-role-from-instance-profile --instance-profile-name $profile --role-name $roleName 59 | done 60 | aws iam delete-role --role-name $roleName 61 | done 62 | 63 | An error occurred (DeleteConflict) when calling the DeleteRole operation: Cannot delete entity, must detach all policies first. 64 | rest-ecs-ec2-comp-b2fe-on-t3-m 65 | ``` -------------------------------------------------------------------------------- /tests/acm/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | unique_zone_names = distinct([for zone in try(var.route53.zones, []) : zone.name]) 3 | } 4 | 5 | data "aws_route53_zone" "current" { 6 | for_each = { for name in local.unique_zone_names : name => {} } 7 | 8 | name = each.key 9 | private_zone = false 10 | } 11 | 12 | module "acm" { 13 | source = "terraform-aws-modules/acm/aws" 14 | version = "4.3.2" 15 | 16 | for_each = { for name in local.unique_zone_names : name => {} } 17 | 18 | create_certificate = true 19 | create_route53_records = true 20 | 21 | key_algorithm = "RSA_2048" 22 | validation_method = "DNS" 23 | 24 | domain_name = var.route53.record.subdomain_name != null ? "${var.route53.record.subdomain_name}.${each.key}" : each.key 25 | zone_id = data.aws_route53_zone.current[each.key].zone_id 26 | 27 | subject_alternative_names = [for prefix in distinct(compact(var.route53.record.prefixes)) : var.route53.record.subdomain_name != null ? "${prefix}.${var.route53.record.subdomain_name}.${each.key}" : "${prefix}.${each.key}"] 28 | 29 | wait_for_validation = true 30 | # validation_timeout = "15m" 31 | 32 | tags = var.tags 33 | } -------------------------------------------------------------------------------- /tests/acm/outputs.tf: -------------------------------------------------------------------------------- 1 | output "acm" { 2 | value = module.acm 3 | } -------------------------------------------------------------------------------- /tests/acm/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } -------------------------------------------------------------------------------- /tests/acm/variables.tf: -------------------------------------------------------------------------------- 1 | variable "route53" { 2 | type = object({ 3 | zones = list(object({ 4 | name = string 5 | })) 6 | record = object({ 7 | prefixes = optional(list(string)) 8 | subdomain_name = optional(string) 9 | }) 10 | }) 11 | default = null 12 | } 13 | 14 | variable "tags" { 15 | type = map(string) 16 | default = {} 17 | } -------------------------------------------------------------------------------- /tests/aws/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_partition" "current" {} 3 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /tests/aws/outputs.tf: -------------------------------------------------------------------------------- 1 | output "account_id" { 2 | value = data.aws_caller_identity.current.account_id 3 | } 4 | 5 | output "account_arn" { 6 | value = data.aws_caller_identity.current.arn 7 | } 8 | 9 | output "dns_suffix" { 10 | value = data.aws_partition.current.dns_suffix // amazonaws.com 11 | } 12 | 13 | output "partition" { 14 | value = data.aws_partition.current.partition // aws 15 | } 16 | 17 | output "region_name" { 18 | value = data.aws_region.current.name 19 | } 20 | 21 | output "user_id" { 22 | value = data.aws_caller_identity.current.user_id 23 | } 24 | 25 | output "user_name" { 26 | value = try(regex("^arn:aws:iam::(?P\\d+):user/(?P\\w+)$", data.aws_caller_identity.current.arn).user_name, null) 27 | } 28 | 29 | output "account_name" { 30 | value = try(regex("^arn:aws:iam::(?P\\d+):(?P\\w+)$", data.aws_caller_identity.current.arn).account_name, null) 31 | } 32 | -------------------------------------------------------------------------------- /tests/aws/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } -------------------------------------------------------------------------------- /tests/check_acm/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | unique_zone_names = distinct([for zone in try(var.route53.zones, []) : zone.name]) 3 | } 4 | 5 | data "aws_acm_certificate" "amazon_issued" { 6 | 7 | for_each = { for name in local.unique_zone_names : name => {} } 8 | 9 | domain = var.route53.record.subdomain_name != null ? "${var.route53.record.subdomain_name}.${each.key}" : each.key 10 | types = ["AMAZON_ISSUED"] 11 | most_recent = true 12 | } -------------------------------------------------------------------------------- /tests/check_acm/outputs.tf: -------------------------------------------------------------------------------- 1 | output "acm" { 2 | value = data.aws_acm_certificate.amazon_issued 3 | } -------------------------------------------------------------------------------- /tests/check_acm/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } -------------------------------------------------------------------------------- /tests/check_acm/variables.tf: -------------------------------------------------------------------------------- 1 | variable "route53" { 2 | type = object({ 3 | zones = list(object({ 4 | name = string 5 | })) 6 | record = object({ 7 | prefixes = optional(list(string)) 8 | subdomain_name = optional(string) 9 | }) 10 | }) 11 | default = null 12 | } 13 | 14 | variable "tags" { 15 | type = map(string) 16 | default = {} 17 | } -------------------------------------------------------------------------------- /tests/check_ecr/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_ecr_image" "service_image" { 2 | for_each = toset(var.image_tags) 3 | 4 | repository_name = var.repository_name 5 | image_tag = each.key 6 | } -------------------------------------------------------------------------------- /tests/check_ecr/outputs.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vistimi/terraform-aws-microservice/694ecbd150510b136b16a7f353089ec13b857338/tests/check_ecr/outputs.tf -------------------------------------------------------------------------------- /tests/check_ecr/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } -------------------------------------------------------------------------------- /tests/check_ecr/variables.tf: -------------------------------------------------------------------------------- 1 | variable "repository_name" { 2 | type = string 3 | } 4 | 5 | variable "image_tags" { 6 | type = list(string) 7 | } -------------------------------------------------------------------------------- /tests/check_ecs_asg/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_autoscaling_group" "this" { 2 | name = var.name 3 | } 4 | 5 | resource "null_resource" "desired_capacity" { 6 | 7 | lifecycle { 8 | postcondition { 9 | condition = data.aws_autoscaling_group.this.desired_capacity == var.expect.desired_capacity 10 | error_message = < health_check } 19 | 20 | provisioner "local-exec" { 21 | command = "./grpcurl -d '${each.value.request}' ${each.value.adress} ${each.value.service}.${each.value.method}" 22 | } 23 | 24 | depends_on = [null_resource.grpcurl_download] 25 | } 26 | 27 | resource "null_resource" "grpcurl_clean" { 28 | provisioner "local-exec" { 29 | command = "rm grpcurl" 30 | } 31 | 32 | depends_on = [null_resource.health_checks] 33 | } 34 | -------------------------------------------------------------------------------- /tests/check_grpc/outputs.tf: -------------------------------------------------------------------------------- 1 | output "health_checks" { 2 | value = null_resource.health_checks 3 | } 4 | -------------------------------------------------------------------------------- /tests/check_grpc/variables.tf: -------------------------------------------------------------------------------- 1 | variable "health_checks" { 2 | type = list(object({ 3 | request = string 4 | adress = string 5 | service = string 6 | method = string 7 | })) 8 | } 9 | -------------------------------------------------------------------------------- /tests/check_rest/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "command_previous" { 2 | provisioner "local-exec" { 3 | command = var.command.previous 4 | } 5 | } 6 | 7 | data "http" "health_checks" { 8 | for_each = { for health_check in var.health_checks : health_check.url => health_check } 9 | 10 | url = each.key 11 | request_headers = each.value.header 12 | method = each.value.method 13 | request_body = each.value.request_body 14 | 15 | lifecycle { 16 | postcondition { 17 | condition = contains(each.value.response_status_codes, self.status_code) 18 | error_message = "${each.key} returned an unhealthy status code: ${jsonencode(self)}, expected: ${jsonencode(each.value.response_status_codes)}" 19 | } 20 | } 21 | 22 | depends_on = [null_resource.command_previous] 23 | } 24 | 25 | resource "null_resource" "command_after" { 26 | provisioner "local-exec" { 27 | command = var.command.after 28 | } 29 | 30 | depends_on = [data.http.health_checks] 31 | } 32 | -------------------------------------------------------------------------------- /tests/check_rest/outputs.tf: -------------------------------------------------------------------------------- 1 | output "health_checks" { 2 | value = data.http.health_checks 3 | } 4 | 5 | output "command_previous" { 6 | value = null_resource.command_previous 7 | } 8 | 9 | output "command_after" { 10 | value = null_resource.command_after 11 | } 12 | -------------------------------------------------------------------------------- /tests/check_rest/variables.tf: -------------------------------------------------------------------------------- 1 | variable "health_checks" { 2 | type = list(object({ 3 | url = string 4 | header = optional(any) 5 | method = optional(string) 6 | request_body = optional(string) 7 | response_status_codes = list(number) 8 | })) 9 | } 10 | 11 | variable "command" { 12 | type = object({ 13 | previous = optional(string, "echo nothing to do") 14 | after = optional(string, "echo nothing to do") 15 | }) 16 | default = { 17 | previous = "echo nothing to do" 18 | after = "echo nothing to do" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/cuda_ecs_ec2_complete.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | variables { 30 | orchestrator = { 31 | group = { 32 | name = "g1" 33 | deployment = { 34 | min_size = 1 35 | max_size = 1 36 | desired_size = 1 37 | 38 | containers = [ 39 | { 40 | name = "c1" 41 | docker = { 42 | registry = { 43 | ecr = { 44 | privacy = "private" 45 | account_id = "763104351884" 46 | region_name = "us-east-1" 47 | } 48 | } 49 | repository = { 50 | name = "pytorch-training" 51 | } 52 | image = { 53 | tag = "1.8.1-gpu-py36-cu111-ubuntu18.04-v1.7" 54 | } 55 | } 56 | traffics = [ 57 | # add an api or visualization tool to monitor the training 58 | # tensorboard or else 59 | ] 60 | entrypoint = ["/bin/bash", "-c"] 61 | command = [ 62 | < 2 | 3 | 4 |

Hello World!

5 | 6 | -------------------------------------------------------------------------------- /tests/ecr/ecr.tf: -------------------------------------------------------------------------------- 1 | module "ecr" { 2 | source = "terraform-aws-modules/ecr/aws" 3 | version = "2.2.0" 4 | 5 | repository_name = var.name 6 | repository_force_delete = true 7 | repository_image_tag_mutability = "MUTABLE" 8 | 9 | create_lifecycle_policy = var.lifecycle_policy != null 10 | repository_lifecycle_policy = var.lifecycle_policy 11 | 12 | repository_read_access_arns = var.read_access_arns 13 | 14 | tags = var.tags 15 | } -------------------------------------------------------------------------------- /tests/ecr/outputs.tf: -------------------------------------------------------------------------------- 1 | output "repository_arn" { 2 | value = module.ecr.repository_arn 3 | } 4 | 5 | output "repository_name" { 6 | value = module.ecr.repository_name 7 | } 8 | 9 | output "repository_registry_id" { 10 | value = module.ecr.repository_registry_id 11 | } 12 | 13 | output "repository_url" { 14 | value = module.ecr.repository_url 15 | } -------------------------------------------------------------------------------- /tests/ecr/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } -------------------------------------------------------------------------------- /tests/ecr/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the ECR repository to create" 3 | type = string 4 | nullable = false 5 | } 6 | 7 | variable "lifecycle_policy" { 8 | description = "The lifecycle policy to apply to the ECR repository" 9 | type = string 10 | default = null 11 | } 12 | 13 | variable "read_access_arns" { 14 | description = "The ARNs of the IAM roles that can read from the ECR repository" 15 | type = list(string) 16 | default = [] 17 | } 18 | 19 | variable "tags" { 20 | description = "The tags to apply to the resources" 21 | type = map(string) 22 | default = {} 23 | } -------------------------------------------------------------------------------- /tests/env_file/main.tf: -------------------------------------------------------------------------------- 1 | resource "local_file" "env" { 2 | content = var.content 3 | filename = var.filename 4 | } 5 | -------------------------------------------------------------------------------- /tests/env_file/variables.tf: -------------------------------------------------------------------------------- 1 | variable "content" { 2 | type = string 3 | } 4 | 5 | variable "filename" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /tests/get_env/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = var.vpc_id 3 | } 4 | 5 | output "domain_name" { 6 | value = var.domain_name 7 | } 8 | -------------------------------------------------------------------------------- /tests/get_env/variables.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | type = string 3 | nullable = false 4 | } 5 | 6 | variable "domain_name" { 7 | type = string 8 | nullable = false 9 | } -------------------------------------------------------------------------------- /tests/grpc_ecs_ec2_complete.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | variables { 30 | name = "grpc-ecs-ec2-com" 31 | orchestrator = { 32 | group = { 33 | name = "g1" 34 | deployment = { 35 | min_size = 1 36 | max_size = 1 37 | desired_size = 1 38 | 39 | containers = [ 40 | { 41 | name = "c1" 42 | docker = { 43 | repository = { 44 | name = "ubuntu" 45 | } 46 | image = { 47 | tag = "latest" 48 | } 49 | } 50 | traffics = [ 51 | { 52 | listener = { 53 | port = 443 54 | protocol = "https" 55 | } 56 | target = { 57 | port = 50051 58 | protocol_version = "grpc" 59 | status_code = 0 60 | health_check_path = "/helloworld.Greeter/SayHello" 61 | } 62 | }, 63 | { 64 | listener = { 65 | port = 444 66 | protocol = "https" 67 | } 68 | target = { 69 | port = 50051 70 | protocol_version = "grpc" 71 | status_code = 0 72 | health_check_path = "/helloworld.Greeter/SayHello" 73 | } 74 | }, 75 | ] 76 | entrypoint = ["/bin/bash", "-c"] 77 | command = [ 78 | # listen to 80 by default 79 | # $${VAR} and %%{VAR} is terraform herodoc 80 | < /dev/null 2>&1 82 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 83 | ufw app list 84 | echo -e 'Listen 81' >> /etc/apache2/ports.conf; echo print /etc/apache2/ports.conf.....; cat /etc/apache2/ports.conf 85 | echo -e '\nServerAdmin webmaster@localhost\nDocumentRoot /var/www/html\nErrorLog $${APACHE_LOG_DIR}/error.log\nCustomLog $${APACHE_LOG_DIR}/access.log combined\n' >> /etc/apache2/sites-enabled/000-default.conf; echo print /etc/apache2/sites-enabled/000-default.conf.....; cat /etc/apache2/sites-enabled/000-default.conf 86 | systemctl start apache2 87 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 88 | echo test localhost:81:: $(curl -s -o /dev/null -w '%%{http_code}' localhost:81) 89 | sleep infinity 90 | EOT 91 | ] 92 | readonly_root_filesystem = false 93 | } 94 | ] 95 | } 96 | ec2 = { 97 | instance_types = ["t3.medium"] 98 | os = "linux" 99 | os_version = "2023" 100 | capacities = [{ 101 | type = "ON_DEMAND" 102 | }] 103 | } 104 | } 105 | ecs = {} 106 | } 107 | 108 | 109 | bucket_env = { 110 | force_destroy = true 111 | versioning = false 112 | file_key = "my_branch_name.env" 113 | file_path = "override.env" 114 | } 115 | 116 | } 117 | 118 | run "env_file" { 119 | command = apply 120 | 121 | variables { 122 | content = < /dev/null 2>&1 92 | apt install git wget curl -y > /dev/null 2>&1 93 | git clone https://github.com/pytorch/serve.git; cd serve; ls examples/image_classifier/densenet_161/ 94 | wget https://download.pytorch.org/models/densenet161-8d451a50.pth; torch-model-archiver --model-name densenet161 --version 1.0 --model-file examples/image_classifier/densenet_161/model.py --serialized-file densenet161-8d451a50.pth --handler image_classifier --extra-files examples/image_classifier/index_to_name.json 95 | mkdir -p model_store; mv densenet161.mar model_store/ 96 | echo -e 'load_models=ALL\ninference_address=http://0.0.0.0:8080\nmanagement_address=http://0.0.0.0:8081\nmetrics_address=http://0.0.0.0:8082\nmodel_store=model_store' >> config.properties 97 | torchserve --start --ncs --ts-config config.properties 98 | sleep infinity 99 | EOT 100 | ] 101 | readonly_root_filesystem = false 102 | user = "root" 103 | } 104 | ] 105 | } 106 | ec2 = { 107 | instance_types = ["inf1.xlarge"] 108 | os = "linux" 109 | os_version = "2" 110 | capacities = [{ 111 | type = "ON_DEMAND" 112 | }] 113 | } 114 | } 115 | ecs = {} 116 | } 117 | 118 | } 119 | 120 | run "microservice" { 121 | command = apply 122 | 123 | variables { 124 | name = "${var.name}-${run.random_id.id}" 125 | 126 | vpc = { 127 | id = run.get_env.vpc_id 128 | tag_tier = "public" 129 | } 130 | orchestrator = var.orchestrator 131 | tags = { 132 | TestID = run.random_id.id 133 | AccountID = run.aws.account_id 134 | } 135 | } 136 | 137 | module { 138 | source = "./" 139 | } 140 | } 141 | 142 | run "wait" { 143 | command = apply 144 | variables { duration = "3m" } 145 | module { source = "./tests/wait" } 146 | } 147 | 148 | run "check_rest" { 149 | command = apply 150 | 151 | variables { 152 | health_checks = [ 153 | { 154 | url = "http://${run.microservice.ecs.elb.dns_name}:8080/ping" 155 | header = { 156 | Accept = "application/json" 157 | } 158 | method = "GET" 159 | response_status_codes = [200] 160 | }, 161 | { 162 | url = "http://${run.microservice.ecs.elb.dns_name}:8081/models" 163 | header = { 164 | Accept = "application/json" 165 | } 166 | method = "GET" 167 | response_status_codes = [200] 168 | }, 169 | { 170 | url = "http://${run.microservice.ecs.elb.dns_name}:8082/metrics" 171 | header = { 172 | Accept = "application/json" 173 | } 174 | method = "GET" 175 | response_status_codes = [200] 176 | }, 177 | ] 178 | command = { 179 | after = "curl -O https://s3.amazonaws.com/model-server/inputs/kitten.jpg; curl -v -X POST http://${run.microservice.ecs.elb.dns_name}:8080/predictions/densenet161 -T kitten.jpg; rm kitten.jpg" 180 | } 181 | } 182 | 183 | module { 184 | source = "./tests/check_rest" 185 | } 186 | } -------------------------------------------------------------------------------- /tests/random_id/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "generator" { 2 | keepers = { 3 | first = "${timestamp()}" 4 | } 5 | byte_length = var.byte_length 6 | } 7 | -------------------------------------------------------------------------------- /tests/random_id/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = random_id.generator.hex 3 | } 4 | -------------------------------------------------------------------------------- /tests/random_id/variables.tf: -------------------------------------------------------------------------------- 1 | variable "byte_length" { 2 | type = number 3 | } 4 | -------------------------------------------------------------------------------- /tests/rest_ecr.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | # ------------------- 30 | # Plan 31 | # ------------------- 32 | run "microservice-docker-registry-name" { 33 | command = plan 34 | 35 | variables { 36 | name = "variables" 37 | vpc = { 38 | id = run.get_env.vpc_id 39 | tag_tier = "public" 40 | } 41 | orchestrator = { 42 | group = { 43 | name = "g1" 44 | deployment = { 45 | min_size = 1 46 | max_size = 1 47 | desired_size = 1 48 | 49 | containers = [{ 50 | name = "c1" 51 | docker = { 52 | registry = {} 53 | repository = { name = "ubuntu" } 54 | } 55 | }] 56 | } 57 | } 58 | ecs = {} 59 | } 60 | } 61 | 62 | module { 63 | source = "./" 64 | } 65 | 66 | expect_failures = [var.orchestrator] 67 | } 68 | 69 | run "microservice-docker-registry-ecr-privacy" { 70 | command = plan 71 | 72 | variables { 73 | name = "variables" 74 | vpc = { 75 | id = run.get_env.vpc_id 76 | tag_tier = "public" 77 | } 78 | orchestrator = { 79 | group = { 80 | name = "g1" 81 | deployment = { 82 | min_size = 1 83 | max_size = 1 84 | desired_size = 1 85 | 86 | containers = [{ 87 | name = "c1" 88 | docker = { 89 | registry = { ecr = { privacy = "error" } } 90 | repository = { name = "ubuntu" } 91 | } 92 | }] 93 | } 94 | } 95 | ecs = {} 96 | } 97 | } 98 | 99 | module { 100 | source = "./" 101 | } 102 | 103 | expect_failures = [var.orchestrator] 104 | } 105 | 106 | run "microservice-docker-registry-ecr-public-no-alias" { 107 | command = plan 108 | 109 | variables { 110 | name = "variables" 111 | vpc = { 112 | id = run.get_env.vpc_id 113 | tag_tier = "public" 114 | } 115 | orchestrator = { 116 | group = { 117 | name = "g1" 118 | deployment = { 119 | min_size = 1 120 | max_size = 1 121 | desired_size = 1 122 | 123 | containers = [{ 124 | name = "c1" 125 | docker = { 126 | registry = { ecr = { privacy = "public", public_alias = null } } 127 | repository = { name = "ubuntu" } 128 | } 129 | }] 130 | } 131 | } 132 | ecs = {} 133 | } 134 | } 135 | 136 | module { 137 | source = "./" 138 | } 139 | 140 | expect_failures = [var.orchestrator] 141 | } -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_acm.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | variables { 30 | name = "rest-ecs-ec2-acm" 31 | container = { 32 | name = "c1" 33 | docker = { 34 | repository = { 35 | name = "ubuntu" 36 | } 37 | image = { 38 | tag = "latest" 39 | } 40 | } 41 | traffics = [ 42 | { 43 | listener = { 44 | port = 80 45 | protocol = "http" 46 | } 47 | target = { 48 | port = 80 49 | } 50 | }, 51 | { 52 | listener = { 53 | port = 81 54 | protocol = "http" 55 | } 56 | redirect = { 57 | port = 443 58 | protocol = "https" 59 | status_code = 301 60 | } 61 | }, 62 | { 63 | listener = { 64 | port = 443 65 | protocol = "https" 66 | } 67 | target = { 68 | port = 80 69 | protocol = "http" 70 | } 71 | }, 72 | ] 73 | entrypoint = ["/bin/bash", "-c"] 74 | command = [ 75 | # listen to 80 by default 76 | # $${VAR} and %%{VAR} is terraform herodoc 77 | < /dev/null 2>&1 79 | apt install apache2 ufw systemctl curl -y > /dev/null 2>&1 80 | ufw app list 81 | systemctl start apache2 82 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 83 | sleep infinity 84 | EOT 85 | ] 86 | readonly_root_filesystem = false 87 | } 88 | } 89 | 90 | # ------------------- 91 | # Plan 92 | # ------------------- 93 | 94 | # run "microservice-https-no-acm" { 95 | # command = plan 96 | 97 | # variables { 98 | # name = "variables" 99 | # vpc = { 100 | # id = run.get_env.vpc_id 101 | # tag_tier = "public" 102 | # } 103 | # orchestrator = { 104 | # group = { 105 | # name = "g1" 106 | # deployment = { 107 | # min_size = 1 108 | # max_size = 1 109 | # desired_size = 1 110 | 111 | # containers = [var.container] 112 | # } 113 | # ec2 = { 114 | # instance_types = ["t3.medium"] 115 | # os = "linux" 116 | # os_version = "2023" 117 | # } 118 | # } 119 | # ecs = {} 120 | # } 121 | # } 122 | 123 | # module { 124 | # source = "./" 125 | # } 126 | 127 | # expect_failures = [module.ecs.null_resource.acm] 128 | # } 129 | 130 | 131 | # ------------------- 132 | # Apply 133 | # ------------------- 134 | # TODO: test with external acm 135 | # TODO: test with several zones 136 | 137 | run "microservice" { 138 | command = apply 139 | 140 | variables { 141 | name = "${var.name}-${run.random_id.id}" 142 | vpc = { 143 | id = run.get_env.vpc_id 144 | tag_tier = "public" 145 | } 146 | orchestrator = { 147 | group = { 148 | name = "g1" 149 | deployment = { 150 | min_size = 1 151 | max_size = 1 152 | desired_size = 1 153 | 154 | containers = [var.container] 155 | } 156 | ec2 = { 157 | instance_types = ["t3.medium"] 158 | os = "linux" 159 | os_version = "2023" 160 | } 161 | } 162 | ecs = {} 163 | } 164 | route53 = { 165 | zones = [{ 166 | name = "${run.get_env.domain_name}" 167 | }] 168 | record = { 169 | prefixes = ["www"] 170 | subdomain_name = run.random_id.id 171 | } 172 | } 173 | } 174 | 175 | module { 176 | source = "./" 177 | } 178 | } 179 | 180 | run "wait" { 181 | command = apply 182 | variables { duration = "3m" } 183 | module { source = "./tests/wait" } 184 | } 185 | 186 | run "check_rest" { 187 | command = apply 188 | 189 | variables { 190 | health_checks = [ 191 | { 192 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 193 | method = "GET" 194 | response_status_codes = [200] 195 | }, 196 | # { 197 | # url = "http://${run.microservice.ecs.elb.dns_name}:81/" 198 | # method = "GET" 199 | # response_status_codes = [301] 200 | # }, 201 | { 202 | url = "https://${run.microservice.ecs.elb.dns_name}:443/" 203 | method = "GET" 204 | response_status_codes = [200] 205 | }, 206 | ] 207 | } 208 | 209 | module { 210 | source = "./tests/check_rest" 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_acm_external.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | variables { 30 | name = "rest-ecs-ec2-acm-ext" 31 | container = { 32 | name = "c1" 33 | docker = { 34 | repository = { 35 | name = "ubuntu" 36 | } 37 | image = { 38 | tag = "latest" 39 | } 40 | } 41 | traffics = [ 42 | { 43 | listener = { 44 | port = 80 45 | protocol = "http" 46 | } 47 | target = { 48 | port = 80 49 | } 50 | }, 51 | { 52 | listener = { 53 | port = 443 54 | protocol = "https" 55 | } 56 | target = { 57 | port = 80 58 | protocol = "http" 59 | } 60 | }, 61 | ] 62 | entrypoint = ["/bin/bash", "-c"] 63 | command = [ 64 | # listen to 80 by default 65 | # $${VAR} and %%{VAR} is terraform herodoc 66 | < /dev/null 2>&1 68 | apt install apache2 ufw systemctl curl -y > /dev/null 2>&1 69 | ufw app list 70 | systemctl start apache2 71 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 72 | sleep infinity 73 | EOT 74 | ] 75 | readonly_root_filesystem = false 76 | } 77 | } 78 | 79 | # ------------------- 80 | # Plan 81 | # ------------------- 82 | 83 | # ------------------- 84 | # Apply 85 | # ------------------- 86 | 87 | run "acm" { 88 | command = apply 89 | 90 | variables { 91 | route53 = { 92 | zones = [{ 93 | name = "${run.get_env.domain_name}" 94 | }] 95 | record = { 96 | prefixes = ["www"] 97 | # subdomain_name = run.random_id.id 98 | } 99 | } 100 | } 101 | 102 | module { 103 | source = "./tests/check_acm" 104 | } 105 | } 106 | 107 | run "microservice" { 108 | command = apply 109 | 110 | variables { 111 | name = "${var.name}-${run.random_id.id}" 112 | vpc = { 113 | id = run.get_env.vpc_id 114 | tag_tier = "public" 115 | } 116 | orchestrator = { 117 | group = { 118 | name = "g1" 119 | deployment = { 120 | min_size = 1 121 | max_size = 1 122 | desired_size = 1 123 | 124 | containers = [var.container] 125 | } 126 | ec2 = { 127 | instance_types = ["t3.medium"] 128 | os = "linux" 129 | os_version = "2023" 130 | } 131 | } 132 | ecs = {} 133 | } 134 | route53 = { 135 | zones = [{ 136 | name = "${run.get_env.domain_name}" 137 | }] 138 | record = { 139 | prefixes = ["www"] 140 | # subdomain_name = run.random_id.id 141 | } 142 | } 143 | acm = { 144 | arn = run.acm.acm["${run.get_env.domain_name}"].acm_certificate_arn 145 | } 146 | } 147 | 148 | module { 149 | source = "./" 150 | } 151 | } 152 | 153 | run "wait" { 154 | command = apply 155 | variables { duration = "3m" } 156 | module { source = "./tests/wait" } 157 | } 158 | 159 | run "check_rest" { 160 | command = apply 161 | 162 | variables { 163 | health_checks = [ 164 | { 165 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 166 | header = { 167 | Accept = "application/json" 168 | } 169 | method = "GET" 170 | response_status_codes = [200] 171 | }, 172 | ] 173 | } 174 | 175 | module { 176 | source = "./tests/check_rest" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_docker.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "random_id" { 2 | command = apply 3 | 4 | variables { 5 | byte_length = 2 6 | } 7 | 8 | module { 9 | source = "./tests/random_id" 10 | } 11 | } 12 | 13 | run "get_env" { 14 | command = apply 15 | 16 | module { 17 | source = "./tests/get_env" 18 | } 19 | } 20 | 21 | # ------------------- 22 | # Plan 23 | # ------------------- 24 | run "microservice-docker-no-build" { 25 | command = plan 26 | 27 | variables { 28 | name = "variables" 29 | vpc = { 30 | id = run.get_env.vpc_id 31 | tag_tier = "public" 32 | } 33 | orchestrator = { 34 | group = { 35 | name = "g1" 36 | deployment = { 37 | min_size = 1 38 | max_size = 1 39 | desired_size = 1 40 | 41 | containers = [{ 42 | name = "c1" 43 | docker = { 44 | repository = { name = "ubuntu" } 45 | image = { tag = "latest" } 46 | } 47 | }] 48 | } 49 | } 50 | ecs = {} 51 | } 52 | } 53 | 54 | module { 55 | source = "./" 56 | } 57 | 58 | expect_failures = [var.orchestrator] 59 | } 60 | 61 | # ------------------- 62 | # Apply 63 | # ------------------- 64 | variables { 65 | name = "rest-ecs-ec2-dock" 66 | orchestrator = { 67 | group = { 68 | name = "g1" 69 | deployment = { 70 | min_size = 1 71 | max_size = 1 72 | desired_size = 1 73 | 74 | containers = [ 75 | { 76 | name = "c1" 77 | docker = { 78 | build = { 79 | args = {} 80 | context_path = "./tests/docker_content" # the commands in dockerfile should use the tests/ directory as the root 81 | file_path = "Dockerfile" # file_path from the context_path 82 | path_include = ["**"] 83 | path_exclude = ["**/.git/**", "**/.github/**", "**/.terraform/**"] # these are files to be ignored for generating a new image such as files in .gitignore 84 | } 85 | } 86 | traffics = [ 87 | { 88 | listener = { 89 | port = 80 90 | protocol = "http" 91 | } 92 | target = { 93 | port = 80 94 | } 95 | } 96 | ] 97 | readonly_root_filesystem = false 98 | } 99 | ] 100 | } 101 | ec2 = { 102 | instance_types = ["t4g.medium"] 103 | os = "linux" 104 | os_version = "2023" 105 | capacities = [{ 106 | type = "ON_DEMAND" 107 | }] 108 | } 109 | } 110 | ecs = {} 111 | } 112 | } 113 | 114 | run "microservice" { 115 | command = apply 116 | 117 | variables { 118 | name = "${var.name}-${run.random_id.id}" 119 | vpc = { 120 | id = run.get_env.vpc_id 121 | tag_tier = "public" 122 | } 123 | orchestrator = var.orchestrator 124 | } 125 | 126 | module { 127 | source = "./" 128 | } 129 | } 130 | 131 | run "wait" { 132 | command = apply 133 | variables { duration = "3m" } 134 | module { source = "./tests/wait" } 135 | } 136 | 137 | run "check_rest" { 138 | command = apply 139 | 140 | variables { 141 | health_checks = [ 142 | { 143 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 144 | header = { 145 | Accept = "application/json" 146 | } 147 | method = "GET" 148 | response_status_codes = [200] 149 | } 150 | ] 151 | } 152 | 153 | module { 154 | source = "./tests/check_rest" 155 | } 156 | } 157 | 158 | run "check_ecr" { 159 | command = apply 160 | 161 | variables { 162 | repository_name = run.microservice.ecs.ecr["c1"].repository_name 163 | image_tags = [run.microservice.ecs.docker["c1"].image_tag] 164 | } 165 | 166 | module { 167 | source = "./tests/check_ecr" 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_docker_external_ecr.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "random_id" { 2 | command = apply 3 | 4 | variables { 5 | byte_length = 2 6 | } 7 | 8 | module { 9 | source = "./tests/random_id" 10 | } 11 | } 12 | 13 | run "get_env" { 14 | command = apply 15 | 16 | module { 17 | source = "./tests/get_env" 18 | } 19 | } 20 | 21 | variables { 22 | name = "rest-ecs-ec2-d-ecr" 23 | } 24 | 25 | run "ecr" { 26 | command = apply 27 | 28 | variables { 29 | name = "${var.name}-${run.random_id.id}" 30 | } 31 | 32 | module { 33 | source = "./tests/ecr" 34 | } 35 | } 36 | 37 | 38 | # ------------------- 39 | # Apply 40 | # ------------------- 41 | run "microservice" { 42 | command = apply 43 | 44 | variables { 45 | name = "${var.name}-${run.random_id.id}" 46 | vpc = { 47 | id = run.get_env.vpc_id 48 | tag_tier = "public" 49 | } 50 | orchestrator = { 51 | group = { 52 | name = "g1" 53 | deployment = { 54 | min_size = 1 55 | max_size = 1 56 | desired_size = 1 57 | 58 | containers = [ 59 | { 60 | name = "c1" 61 | docker = { 62 | repository = { 63 | name = "${var.name}-${run.random_id.id}" 64 | } 65 | build = { 66 | args = {} 67 | context_path = "./tests/docker_content" # the commands in dockerfile should use the tests/ directory as the root 68 | file_path = "Dockerfile" # file_path from the context_path 69 | path_include = ["**"] 70 | path_exclude = ["**/.git/**", "**/.github/**", "**/.terraform/**"] # these are files to be ignored for generating a new image such as files in .gitignore 71 | } 72 | } 73 | traffics = [ 74 | { 75 | listener = { 76 | port = 80 77 | protocol = "http" 78 | } 79 | target = { 80 | port = 80 81 | } 82 | } 83 | ] 84 | readonly_root_filesystem = false 85 | } 86 | ] 87 | } 88 | ec2 = { 89 | instance_types = ["t4g.medium"] 90 | os = "linux" 91 | os_version = "2023" 92 | capacities = [{ 93 | type = "ON_DEMAND" 94 | }] 95 | } 96 | } 97 | ecs = {} 98 | } 99 | } 100 | 101 | module { 102 | source = "./" 103 | } 104 | } 105 | 106 | run "wait" { 107 | command = apply 108 | variables { duration = "3m" } 109 | module { source = "./tests/wait" } 110 | } 111 | 112 | run "check_rest" { 113 | command = apply 114 | 115 | variables { 116 | health_checks = [ 117 | { 118 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 119 | header = { 120 | Accept = "application/json" 121 | } 122 | method = "GET" 123 | response_status_codes = [200] 124 | } 125 | ] 126 | } 127 | 128 | module { 129 | source = "./tests/check_rest" 130 | } 131 | } 132 | 133 | run "check_ecr" { 134 | command = apply 135 | 136 | variables { 137 | repository_name = "${var.name}-${run.random_id.id}" 138 | image_tags = [run.microservice.ecs.docker["c1"].image_tag] 139 | } 140 | 141 | module { 142 | source = "./tests/check_ecr" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_instance_type_amd.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "random_id" { 2 | command = apply 3 | 4 | variables { 5 | byte_length = 2 6 | } 7 | 8 | module { 9 | source = "./tests/random_id" 10 | } 11 | } 12 | 13 | run "get_env" { 14 | command = apply 15 | 16 | module { 17 | source = "./tests/get_env" 18 | } 19 | } 20 | 21 | variables { 22 | name = "rest-ecs-ec2-a" 23 | orchestrator = { 24 | group = { 25 | name = "g1" 26 | deployment = { 27 | min_size = 1 28 | max_size = 1 29 | desired_size = 1 30 | 31 | containers = [ 32 | { 33 | name = "c1" 34 | docker = { 35 | repository = { 36 | name = "ubuntu" 37 | } 38 | image = { 39 | tag = "latest" 40 | } 41 | } 42 | traffics = [ 43 | { 44 | listener = { 45 | port = 80 46 | protocol = "http" 47 | } 48 | target = { 49 | port = 80 50 | } 51 | } 52 | ] 53 | entrypoint = ["/bin/bash", "-c"] 54 | command = [ 55 | # listen to 80 by default 56 | # $${VAR} and %%{VAR} is terraform herodoc 57 | < /dev/null 2>&1 59 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 60 | ufw app list 61 | systemctl start apache2 62 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 63 | sleep infinity 64 | EOT 65 | ] 66 | readonly_root_filesystem = false 67 | } 68 | ] 69 | } 70 | ec2 = { 71 | instance_types = ["t3a.medium"] 72 | os = "linux" 73 | os_version = "2023" 74 | capacities = [{ 75 | type = "ON_DEMAND" 76 | }] 77 | } 78 | } 79 | ecs = {} 80 | } 81 | } 82 | 83 | # ------------------- 84 | # Apply 85 | # ------------------- 86 | run "microservice" { 87 | command = apply 88 | 89 | variables { 90 | name = "${var.name}-${run.random_id.id}" 91 | vpc = { 92 | id = run.get_env.vpc_id 93 | tag_tier = "public" 94 | } 95 | orchestrator = var.orchestrator 96 | } 97 | 98 | module { 99 | source = "./" 100 | } 101 | } 102 | 103 | run "wait" { 104 | command = apply 105 | variables { duration = "3m" } 106 | module { source = "./tests/wait" } 107 | } 108 | 109 | run "check_rest" { 110 | command = apply 111 | 112 | variables { 113 | health_checks = [ 114 | { 115 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 116 | header = { 117 | Accept = "application/json" 118 | } 119 | method = "GET" 120 | response_status_codes = [200] 121 | } 122 | ] 123 | } 124 | 125 | module { 126 | source = "./tests/check_rest" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_instance_type_graviton.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "random_id" { 2 | command = apply 3 | 4 | variables { 5 | byte_length = 2 6 | } 7 | 8 | module { 9 | source = "./tests/random_id" 10 | } 11 | } 12 | 13 | run "get_env" { 14 | command = apply 15 | 16 | module { 17 | source = "./tests/get_env" 18 | } 19 | } 20 | 21 | 22 | variables { 23 | name = "rest-ecs-ec2-g" 24 | orchestrator = { 25 | group = { 26 | name = "g1" 27 | deployment = { 28 | min_size = 1 29 | max_size = 1 30 | desired_size = 1 31 | 32 | containers = [ 33 | { 34 | name = "c1" 35 | docker = { 36 | repository = { 37 | name = "ubuntu" 38 | } 39 | image = { 40 | tag = "latest" 41 | } 42 | } 43 | traffics = [ 44 | { 45 | listener = { 46 | port = 80 47 | protocol = "http" 48 | } 49 | target = { 50 | port = 80 51 | } 52 | } 53 | ] 54 | entrypoint = ["/bin/bash", "-c"] 55 | command = [ 56 | # listen to 80 by default 57 | # $${VAR} and %%{VAR} is terraform herodoc 58 | < /dev/null 2>&1 60 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 61 | ufw app list 62 | systemctl start apache2 63 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 64 | sleep infinity 65 | EOT 66 | ] 67 | readonly_root_filesystem = false 68 | } 69 | ] 70 | } 71 | ec2 = { 72 | instance_types = ["t4g.medium"] 73 | os = "linux" 74 | os_version = "2023" 75 | capacities = [{ 76 | type = "ON_DEMAND" 77 | }] 78 | } 79 | } 80 | ecs = {} 81 | } 82 | } 83 | 84 | # ------------------- 85 | # Apply 86 | # ------------------- 87 | run "microservice" { 88 | command = apply 89 | 90 | variables { 91 | name = "${var.name}-${run.random_id.id}" 92 | vpc = { 93 | id = run.get_env.vpc_id 94 | tag_tier = "public" 95 | } 96 | orchestrator = var.orchestrator 97 | } 98 | 99 | module { 100 | source = "./" 101 | } 102 | } 103 | 104 | run "wait" { 105 | command = apply 106 | variables { duration = "3m" } 107 | module { source = "./tests/wait" } 108 | } 109 | 110 | run "check_rest" { 111 | command = apply 112 | 113 | variables { 114 | health_checks = [ 115 | { 116 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 117 | header = { 118 | Accept = "application/json" 119 | } 120 | method = "GET" 121 | response_status_codes = [200] 122 | } 123 | ] 124 | } 125 | 126 | module { 127 | source = "./tests/check_rest" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_instance_type_intel.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "random_id" { 2 | command = apply 3 | 4 | variables { 5 | byte_length = 2 6 | } 7 | 8 | module { 9 | source = "./tests/random_id" 10 | } 11 | } 12 | 13 | run "get_env" { 14 | command = apply 15 | 16 | module { 17 | source = "./tests/get_env" 18 | } 19 | } 20 | 21 | 22 | variables { 23 | name = "rest-ecs-ec2-i" 24 | orchestrator = { 25 | group = { 26 | name = "g1" 27 | deployment = { 28 | min_size = 1 29 | max_size = 1 30 | desired_size = 1 31 | 32 | containers = [ 33 | { 34 | name = "c1" 35 | docker = { 36 | repository = { 37 | name = "ubuntu" 38 | } 39 | image = { 40 | tag = "latest" 41 | } 42 | } 43 | traffics = [ 44 | { 45 | listener = { 46 | port = 80 47 | protocol = "http" 48 | } 49 | target = { 50 | port = 80 51 | } 52 | } 53 | ] 54 | entrypoint = ["/bin/bash", "-c"] 55 | command = [ 56 | # listen to 80 by default 57 | # $${VAR} and %%{VAR} is terraform herodoc 58 | < /dev/null 2>&1 60 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 61 | ufw app list 62 | systemctl start apache2 63 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 64 | sleep infinity 65 | EOT 66 | ] 67 | readonly_root_filesystem = false 68 | } 69 | ] 70 | } 71 | ec2 = { 72 | instance_types = ["m6i.large"] 73 | os = "linux" 74 | os_version = "2023" 75 | capacities = [{ 76 | type = "ON_DEMAND" 77 | }] 78 | } 79 | } 80 | ecs = {} 81 | } 82 | } 83 | 84 | # ------------------- 85 | # Apply 86 | # ------------------- 87 | run "microservice" { 88 | command = apply 89 | 90 | variables { 91 | name = "${var.name}-${run.random_id.id}" 92 | vpc = { 93 | id = run.get_env.vpc_id 94 | tag_tier = "public" 95 | } 96 | orchestrator = var.orchestrator 97 | } 98 | 99 | module { 100 | source = "./" 101 | } 102 | } 103 | 104 | run "wait" { 105 | command = apply 106 | variables { duration = "3m" } 107 | module { source = "./tests/wait" } 108 | } 109 | 110 | run "check_rest" { 111 | command = apply 112 | 113 | variables { 114 | health_checks = [ 115 | { 116 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 117 | header = { 118 | Accept = "application/json" 119 | } 120 | method = "GET" 121 | response_status_codes = [200] 122 | } 123 | ] 124 | } 125 | 126 | module { 127 | source = "./tests/check_rest" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_instance_type_none.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "random_id" { 2 | command = apply 3 | 4 | variables { 5 | byte_length = 2 6 | } 7 | 8 | module { 9 | source = "./tests/random_id" 10 | } 11 | } 12 | 13 | run "get_env" { 14 | command = apply 15 | 16 | module { 17 | source = "./tests/get_env" 18 | } 19 | } 20 | 21 | 22 | variables { 23 | name = "rest-ecs-ec2-none" 24 | orchestrator = { 25 | group = { 26 | name = "g1" 27 | deployment = { 28 | min_size = 1 29 | max_size = 1 30 | desired_size = 1 31 | 32 | containers = [ 33 | { 34 | name = "c1" 35 | docker = { 36 | repository = { 37 | name = "ubuntu" 38 | } 39 | image = { 40 | tag = "latest" 41 | } 42 | } 43 | traffics = [ 44 | { 45 | listener = { 46 | port = 80 47 | protocol = "http" 48 | } 49 | target = { 50 | port = 80 51 | } 52 | } 53 | ] 54 | entrypoint = ["/bin/bash", "-c"] 55 | command = [ 56 | # listen to 80 by default 57 | # $${VAR} and %%{VAR} is terraform herodoc 58 | < /dev/null 2>&1 60 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 61 | ufw app list 62 | systemctl start apache2 63 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 64 | sleep infinity 65 | EOT 66 | ] 67 | readonly_root_filesystem = false 68 | } 69 | ] 70 | } 71 | ec2 = { 72 | instance_types = ["t3.medium"] 73 | os = "linux" 74 | os_version = "2023" 75 | capacities = [{ 76 | type = "ON_DEMAND" 77 | }] 78 | } 79 | } 80 | ecs = {} 81 | } 82 | } 83 | 84 | # ------------------- 85 | # Apply 86 | # ------------------- 87 | run "microservice" { 88 | command = apply 89 | 90 | variables { 91 | name = "${var.name}-${run.random_id.id}" 92 | vpc = { 93 | id = run.get_env.vpc_id 94 | tag_tier = "public" 95 | } 96 | orchestrator = var.orchestrator 97 | } 98 | 99 | module { 100 | source = "./" 101 | } 102 | } 103 | 104 | run "wait" { 105 | command = apply 106 | variables { duration = "3m" } 107 | module { source = "./tests/wait" } 108 | } 109 | 110 | run "check_rest" { 111 | command = apply 112 | 113 | variables { 114 | health_checks = [ 115 | { 116 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 117 | header = { 118 | Accept = "application/json" 119 | } 120 | method = "GET" 121 | response_status_codes = [200] 122 | } 123 | ] 124 | } 125 | 126 | module { 127 | source = "./tests/check_rest" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/rest_ecs_ec2_schedules.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | 30 | variables { 31 | name = "rest-ecs-ec2-sched" 32 | orchestrator = { 33 | group = { 34 | name = "g1" 35 | deployment = { 36 | min_size = 1 37 | max_size = 1 38 | desired_size = 1 39 | 40 | containers = [ 41 | { 42 | name = "c1" 43 | docker = { 44 | repository = { 45 | name = "ubuntu" 46 | } 47 | image = { 48 | tag = "latest" 49 | } 50 | } 51 | traffics = [ 52 | { 53 | listener = { 54 | port = 80 55 | protocol = "http" 56 | } 57 | target = { 58 | port = 80 59 | } 60 | }, 61 | ] 62 | entrypoint = ["/bin/bash", "-c"] 63 | command = [ 64 | # listen to 80 by default 65 | # $${VAR} and %%{VAR} is terraform herodoc 66 | < /dev/null 2>&1 68 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 69 | ufw app list 70 | systemctl start apache2 71 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 72 | sleep infinity 73 | EOT 74 | ] 75 | readonly_root_filesystem = false 76 | } 77 | ] 78 | } 79 | ec2 = { 80 | instance_types = ["t3.medium"] 81 | os = "linux" 82 | os_version = "2023" 83 | capacities = [{ 84 | type = "ON_DEMAND" 85 | }] 86 | asg = { 87 | schedules = { 88 | shut = { 89 | min_size = 0 90 | max_size = 0 91 | desired_capacity = 0 92 | recurrence = "0 22 * * 1-5" # Mon-Fri in the evening 93 | # time_zone = "Europe/Rome" # default UTC 94 | }, 95 | up = { 96 | min_size = 1 97 | max_size = 1 98 | desired_capacity = 1 99 | recurrence = "0 23 * * 1-5" # Mon-Fri in the morning 100 | }, 101 | } 102 | } 103 | } 104 | } 105 | ecs = {} 106 | } 107 | } 108 | 109 | # ------------------- 110 | # Apply 111 | # ------------------- 112 | run "microservice" { 113 | command = apply 114 | 115 | variables { 116 | name = "${var.name}-${run.random_id.id}" 117 | vpc = { 118 | id = run.get_env.vpc_id 119 | tag_tier = "public" 120 | } 121 | orchestrator = var.orchestrator 122 | route53 = { 123 | zones = [{ 124 | name = "${run.get_env.domain_name}" 125 | }] 126 | record = { 127 | prefixes = ["www"] 128 | subdomain_name = "${var.name}-${run.random_id.id}" 129 | } 130 | } 131 | } 132 | 133 | module { 134 | source = "./" 135 | } 136 | } 137 | 138 | run "wait" { 139 | command = apply 140 | variables { duration = "3m" } 141 | module { source = "./tests/wait" } 142 | } 143 | 144 | run "check_rest" { 145 | command = apply 146 | 147 | variables { 148 | health_checks = [ 149 | { 150 | url = "http://${run.microservice.ecs.elb.dns_name}:80/" 151 | method = "GET" 152 | response_status_codes = [200] 153 | }, 154 | ] 155 | } 156 | 157 | module { 158 | source = "./tests/check_rest" 159 | } 160 | } -------------------------------------------------------------------------------- /tests/rest_vpc.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | run "get_env" { 22 | command = apply 23 | 24 | module { 25 | source = "./tests/get_env" 26 | } 27 | } 28 | 29 | # ------------------- 30 | # Plan 31 | # ------------------- 32 | run "microservice-no-vpc-subnets" { 33 | command = plan 34 | 35 | variables { 36 | name = "variables" 37 | vpc = { 38 | id = run.get_env.vpc_id 39 | subnet_tier_ids = [] 40 | } 41 | orchestrator = { 42 | group = { 43 | name = "g1" 44 | deployment = { 45 | min_size = 1 46 | max_size = 1 47 | desired_size = 1 48 | 49 | containers = [ 50 | { 51 | name = "c1" 52 | docker = { 53 | repository = { 54 | name = "ubuntu" 55 | } 56 | image = { 57 | tag = "latest" 58 | } 59 | } 60 | traffics = [ 61 | { 62 | listener = { 63 | port = 80 64 | protocol = "http" 65 | } 66 | target = { 67 | port = 80 68 | } 69 | } 70 | ] 71 | entrypoint = ["/bin/bash", "-c"] 72 | command = [ 73 | # listen to 80 by default 74 | # $${VAR} and %%{VAR} is terraform herodoc 75 | < /dev/null 2>&1 77 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 78 | ufw app list 79 | systemctl start apache2 80 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 81 | sleep infinity 82 | EOT 83 | ] 84 | readonly_root_filesystem = false 85 | } 86 | ] 87 | } 88 | ec2 = { 89 | instance_types = ["t3.medium"] 90 | os = "linux" 91 | os_version = "2023" 92 | capacities = [{ 93 | type = "ON_DEMAND" 94 | }] 95 | } 96 | } 97 | ecs = {} 98 | } 99 | } 100 | 101 | module { 102 | source = "./" 103 | } 104 | 105 | expect_failures = [var.vpc] 106 | } -------------------------------------------------------------------------------- /tests/variables.tftest.hcl: -------------------------------------------------------------------------------- 1 | run "aws" { 2 | command = apply 3 | 4 | module { 5 | source = "./tests/aws" 6 | } 7 | } 8 | 9 | run "random_id" { 10 | command = apply 11 | 12 | variables { 13 | byte_length = 2 14 | } 15 | 16 | module { 17 | source = "./tests/random_id" 18 | } 19 | } 20 | 21 | 22 | run "get_env" { 23 | command = apply 24 | 25 | module { 26 | source = "./tests/get_env" 27 | } 28 | } 29 | 30 | variables { 31 | orchestrator = { 32 | group = { 33 | name = "g1" 34 | deployment = { 35 | min_size = 1 36 | max_size = 1 37 | desired_size = 1 38 | 39 | containers = [ 40 | { 41 | name = "c1" 42 | docker = { 43 | repository = { 44 | name = "ubuntu" 45 | } 46 | image = { 47 | tag = "latest" 48 | } 49 | } 50 | traffics = [ 51 | { 52 | listener = { 53 | port = 80 54 | protocol = "http" 55 | } 56 | target = { 57 | port = 80 58 | } 59 | } 60 | ] 61 | entrypoint = ["/bin/bash", "-c"] 62 | command = [ 63 | # listen to 80 by default 64 | # $${VAR} and %%{VAR} is terraform herodoc 65 | < /dev/null 2>&1 67 | apt install apache2 ufw systemctl curl -yq > /dev/null 2>&1 68 | ufw app list 69 | systemctl start apache2 70 | echo test localhost:: $(curl -s -o /dev/null -w '%%{http_code}' localhost) 71 | sleep infinity 72 | EOT 73 | ] 74 | readonly_root_filesystem = false 75 | } 76 | ] 77 | } 78 | ec2 = { 79 | instance_types = ["t3.medium"] 80 | os = "linux" 81 | os_version = "2023" 82 | capacities = [{ 83 | type = "ON_DEMAND" 84 | }] 85 | } 86 | } 87 | ecs = {} 88 | } 89 | } 90 | 91 | run "microservice-two-orcherstrators" { 92 | command = plan 93 | 94 | variables { 95 | name = "variables" 96 | vpc = { 97 | id = run.get_env.vpc_id 98 | tag_tier = "public" 99 | } 100 | orchestrator = { 101 | group = { 102 | name = "g1" 103 | deployment = { 104 | min_size = 1 105 | max_size = 1 106 | desired_size = 1 107 | 108 | containers = [] 109 | } 110 | } 111 | ecs = {} 112 | eks = { 113 | cluster_version = "1" 114 | } 115 | } 116 | } 117 | 118 | module { 119 | source = "./" 120 | } 121 | 122 | expect_failures = [var.orchestrator] 123 | } 124 | 125 | run "microservice-ec2-and-fargate" { 126 | command = plan 127 | 128 | variables { 129 | name = "variables" 130 | vpc = { 131 | id = run.get_env.vpc_id 132 | tag_tier = "public" 133 | } 134 | orchestrator = { 135 | group = { 136 | name = "g1" 137 | deployment = { 138 | min_size = 1 139 | max_size = 1 140 | desired_size = 1 141 | 142 | containers = [] 143 | } 144 | ec2 = { 145 | instance_types = ["t3.medium"] 146 | os = "linux" 147 | os_version = "2023" 148 | } 149 | fargate = { 150 | os = "linux" 151 | architecture = "x86_64" 152 | } 153 | } 154 | ecs = {} 155 | } 156 | } 157 | 158 | module { 159 | source = "./" 160 | } 161 | 162 | expect_failures = [var.orchestrator] 163 | } -------------------------------------------------------------------------------- /tests/wait/main.tf: -------------------------------------------------------------------------------- 1 | resource "time_sleep" "wait" { 2 | create_duration = var.duration 3 | } -------------------------------------------------------------------------------- /tests/wait/variables.tf: -------------------------------------------------------------------------------- 1 | variable "duration" { 2 | type = string 3 | } -------------------------------------------------------------------------------- /traffic.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # complete the listener and target with default values 3 | 4 | listeners = { for container in var.orchestrator.group.deployment.containers : container.name => [ 5 | for traffic in container.traffics : merge(traffic.listener, { 6 | port = coalesce( 7 | traffic.listener.port, 8 | traffic.listener.protocol == "http" ? 80 : null, 9 | traffic.listener.protocol == "https" ? 443 : null, 10 | traffic.listener.protocol_version == "grpc" ? 443 : null, 11 | ) 12 | protocol_version = coalesce( 13 | traffic.listener.protocol_version, 14 | traffic.listener.protocol == "http" ? "http1" : null, 15 | traffic.listener.protocol == "https" ? "http1" : null, 16 | try(traffic.target.protocol_version, null) == "grpc" ? "grpc" : null, 17 | ) 18 | }) 19 | ] 20 | } 21 | 22 | targets = { for container in var.orchestrator.group.deployment.containers : container.name => [ 23 | for index, traffic in container.traffics : traffic.listener.redirect == null ? merge( 24 | traffic.target, 25 | { 26 | protocol = coalesce( 27 | traffic.target.protocol, 28 | local.listeners[container.name][index].protocol, 29 | ) 30 | protocol_version = coalesce( 31 | traffic.target.protocol_version, 32 | traffic.target.protocol == "http" ? "http1" : null, 33 | traffic.target.protocol == "https" ? "http1" : null, 34 | local.listeners[container.name][index].protocol_version, 35 | ) 36 | health_check_path = coalesce( 37 | traffic.target.health_check_path, 38 | "/", 39 | ) 40 | } 41 | ) : null 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /version.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = ">= 5.0.1" 6 | } 7 | null = { 8 | source = "hashicorp/null" 9 | version = ">= 3.2.0" 10 | } 11 | 12 | # Tests versions 13 | random = { 14 | source = "hashicorp/random" 15 | version = ">= 3.5.1" 16 | } 17 | time = { 18 | source = "hashicorp/time" 19 | version = "0.9.1" 20 | } 21 | 22 | # docker 23 | docker = { 24 | source = "kreuzwerker/docker" 25 | version = ">= 3.0" 26 | } 27 | 28 | # Kubernetes versions 29 | # kubectl = { 30 | # source = "gavinbunney/kubectl" 31 | # version = "= 1.14.0" 32 | # } 33 | # helm = { 34 | # source = "hashicorp/helm" 35 | # version = "2.5.0" 36 | # } 37 | # kubernetes = { 38 | # source = "hashicorp/kubernetes" 39 | # version = "2.0.1" 40 | # } 41 | } 42 | required_version = ">= 1.4.0" 43 | } 44 | -------------------------------------------------------------------------------- /vpc.tf: -------------------------------------------------------------------------------- 1 | data "aws_vpc" "current" { 2 | id = var.vpc.id 3 | } 4 | 5 | data "aws_subnets" "tier" { 6 | for_each = var.vpc.subnet_tier_ids == null ? { 0 = {} } : {} 7 | 8 | filter { 9 | name = "vpc-id" 10 | values = [data.aws_vpc.current.id] 11 | } 12 | tags = { 13 | Tier = var.vpc.tag_tier 14 | } 15 | 16 | lifecycle { 17 | postcondition { 18 | condition = length(self.ids) >= 2 19 | error_message = "For a Load Balancer: At least two tier subnets in two different Availability Zones must be specified, tier: ${var.vpc.tag_tier}, subnets: ${jsonencode(self.ids)}" 20 | } 21 | } 22 | } 23 | 24 | data "aws_subnets" "intra" { 25 | for_each = var.vpc.subnet_intra_ids == null && var.orchestrator.eks != null ? { 0 = {} } : {} 26 | 27 | filter { 28 | name = "vpc-id" 29 | values = [data.aws_vpc.current.id] 30 | } 31 | tags = { 32 | Tier = "intra" 33 | } 34 | 35 | lifecycle { 36 | postcondition { 37 | condition = length(self.ids) >= 2 38 | error_message = "For a Load Balancer: At least two intra subnets in two different Availability Zones must be specified, tier: intra, subnets: ${jsonencode(self.ids)}" 39 | } 40 | } 41 | } 42 | 43 | data "aws_region" "current" {} 44 | 45 | locals { 46 | region_name = data.aws_region.current.name 47 | vpc = { 48 | id = var.vpc.id 49 | # max 3 subnets if not specified 50 | subnet_tier_ids = coalesce(var.vpc.subnet_tier_ids, try(slice(data.aws_subnets.tier[0].ids, 0, 3), data.aws_subnets.tier[0].ids, null), []) 51 | subnet_intra_ids = coalesce(var.vpc.subnet_intra_ids, try(slice(data.aws_subnets.intra[0].ids, 0, 3), data.aws_subnets.intra[0].ids, null), []) 52 | } 53 | } 54 | --------------------------------------------------------------------------------