diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000000..a29b6bea80 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,113 @@ +name: 'Deployment' +on: + workflow_dispatch: + push: + branches: + - + pull_request: + branches: [] +defaults: + run: + shell: bash + +permissions: + id-token: write + contents: read + +concurrency: ${{ github.head_ref || github.ref_name || github.run_id }} +jobs: + terraform: + name: ${{matrix.runner}} - dev + runs-on: ['${{ matrix.runner }}'] + strategy: + max-parallel: 1 + matrix: + include: + - environment: dev + runner: ubuntu-latest + env: + AWS_DEFAULT_REGION: us-east-1 + steps: + - uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ secrets.OIDC_IAM_ROLE_ARN }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push the web image to Amazon ECR + id: build-web-image + env: + ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} + ECR_REPOSITORY: ${{ secrets.WEB_ECR_REPOSITORY }} + OKTA_DOMAIN: ${{ secrets.OKTA_DOMAIN }} + TEALIUM_ENV: 'dev' + LD_CLIENT_ID: ${{ secrets.LD_CLIENT_ID }} + TEALIUM_TAG: ${{ secrets.TEALIUM_TAG }} + API_URL: ${{ secrets.API_URL }} + + run: | + # Build a docker container and push it to ECR + export IMAGE_TAG=$(git rev-parse --short "$GITHUB_SHA") + docker build --quiet -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg LD_CLIENT_ID=$LD_CLIENT_ID --build-arg TEALIUM_ENV=$TEALIUM_ENV --build-arg TEALIUM_TAG=TEALIUM_TAG --build-arg OKTA_DOMAIN=$OKTA_DOMAIN -f web/DockerfileECS . + echo "Pushing image to ECR..." + export WEB_IMAGE=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker push $WEB_IMAGE + echo "::set-output name=image::${WEB_IMAGE}" + + - name: Build, tag, and push the api image to Amazon ECR + id: build-api-image + env: + ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} + ECR_REPOSITORY: ${{ secrets.API_ECR_REPOSITORY }} + run: | + # Build a docker container and push it to ECR + export IMAGE_TAG=$(git rev-parse --short "$GITHUB_SHA") + docker build --quiet -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f api/DockerfileECS . + echo "Pushing image to ECR..." + export API_IMAGE=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker push $API_IMAGE + echo "::set-output name=image::${API_IMAGE}" + + - uses: hashicorp/setup-terraform@v1 + with: + terraform_wrapper: false + - name: Terraform Init + id: init + working-directory: terraform/greenfield/ecs + run: | + rm -rf .terraform + terraform init -backend-config=envs/dev/backend.tfvars -upgrade=true -no-color -input=false + - name: Terraform Plan + id: plan + working-directory: terraform/greenfield/ecs + run: | + terraform plan -input=false -var-file=envs/dev/inputs.tfvars -var "web_image=$WEB_IMAGE" -var "aws_account=$AWS_ACCOUNT" -var "api_image=$API_IMAGE" -no-color + env: + AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} + WEB_IMAGE: ${{steps.build-web-image.outputs.image}} + API_IMAGE: ${{steps.build-api-image.outputs.image}} + - name: Terraform Apply + if: github.ref == 'refs/heads/gf-ecs' + id: apply + working-directory: terraform/greenfield/ecs + run: | + terraform apply -auto-approve -input=false -var-file=envs/dev/inputs.tfvars -var "aws_account=$AWS_ACCOUNT" -var "web_image=$WEB_IMAGE" -var "api_image=$API_IMAGE" + env: + AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} + WEB_IMAGE: ${{steps.build-web-image.outputs.image}} + API_IMAGE: ${{steps.build-api-image.outputs.image}} + - name: Terraform destroy + if: github.ref == 'refs/heads/destroy' + id: destroy + working-directory: terraform/greenfiel/ecs + run: | + terraform destroy -auto-approve -input=false -var-file=envs/dev/inputs.tfvars + env: + WEB_IMAGE: ${{steps.build-web-image.outputs.image}} + API_IMAGE: ${{steps.build-api-image.outputs.image}} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000..7f30f96260 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,74 @@ +name: 'Deployment' +on: + workflow_dispatch: + push: + branches: + - + pull_request: + branches: [] +defaults: + run: + shell: bash + +permissions: + id-token: write + contents: read + +concurrency: ${{ github.head_ref || github.ref_name || github.run_id }} +jobs: + dependency_vulnerability_scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16.19.1' + # - name: save yarn package cache + # continue-on-error: true + # with: + # id: cache-npm + # uses: actions/cache@v3 + # # npm cache files are stored in `~/.npm` on Linux/macOS + # path: ~/.cache/yarn + # key: cms-eapd-yarn-packages-{{ checksum "yarn.lock" }} + # restore-keys: | + # ${{ runner.os }}-build-${{ cms-eapd-yarn-packages }}- + # ${{ runner.os }}-build- + # ${{ runner.os }}- + - name: install dependencies + continue-on-error: true + run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + - name: setup nvm + continue-on-error: true + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + nvm install 16.19.1 + nvm alias default 16.19.1 + echo 'export NVM_DIR="$HOME/.nvm"' >> $GITHUB_ENV + echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $GITHUB_ENV + + - name: dependency vulnerability scan + continue-on-error: true + run: yarn run audit + - name: yaml test + working-directory: ./web + run: | + yarn install --frozen-lockfile + yarn add glob + yarn add js-yaml + node yaml-tests.js + - name: backend_lint + continue-on-error: true + working-directory: ./api + run: | + npm install -g eslint + yarn lint + - name: frontend_lint + continue-on-error: true + working-directory: ./web + run: | + npm install -g eslint + yarn lint diff --git a/api/DockerfileECS b/api/DockerfileECS new file mode 100644 index 0000000000..9ace107621 --- /dev/null +++ b/api/DockerfileECS @@ -0,0 +1,33 @@ +FROM node:16.19.1-bullseye-slim as builder + +RUN mkdir /app +WORKDIR /app + +COPY package.json . +COPY ./api/package.json ./api/package.json +COPY yarn.lock . +COPY common ./common + +RUN chown -R node:node /app +USER node:node + +RUN yarn install --frozen-lockfile --non-interactive +RUN npm rebuild + +COPY api ./api + +# --- + +FROM node:16.19.1-bullseye-slim + +USER node +WORKDIR /home/node + +COPY --from=builder --chown=node:node /app/package.json ./ +COPY --from=builder --chown=node:node /app/node_modules/ ./node_modules/ +COPY --from=builder --chown=node:node /app/common/ ./common/ +COPY --from=builder --chown=node:node /app/api/ ./api/ + +WORKDIR /home/node/api + +CMD yarn run start \ No newline at end of file diff --git a/terraform/greenfield/.gitignore b/terraform/greenfield/.gitignore new file mode 100644 index 0000000000..7a3e2fd094 --- /dev/null +++ b/terraform/greenfield/.gitignore @@ -0,0 +1,29 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* diff --git a/terraform/greenfield/bootstrap/.gitignore b/terraform/greenfield/bootstrap/.gitignore new file mode 100644 index 0000000000..7a3e2fd094 --- /dev/null +++ b/terraform/greenfield/bootstrap/.gitignore @@ -0,0 +1,29 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* diff --git a/terraform/greenfield/bootstrap/README.md b/terraform/greenfield/bootstrap/README.md new file mode 100644 index 0000000000..8ca63e5b72 --- /dev/null +++ b/terraform/greenfield/bootstrap/README.md @@ -0,0 +1,48 @@ +# Terraform for GitHub OIDC + +## Usage + +This Terraform module is located in a sub-directory, since some users may wish +to consume this module even if they don't need to set up self-hosted runners. +Note that to refer to a sub-directory as a Terraform module source, you need to +[include a double slash before the sub-directory](https://developer.hashicorp.com/terraform/language/modules/sources#modules-in-package-sub-directories). + +```hcl + module "github-actions-aws" { + source = "modules//oidc/github" # double-slash denotes a sub-directory + + subject_claim_filters = ["repo:{your GitHub org}/{your GitHub repo}:{GitHub ref}"] + # audience_list = [] # optional, defaults to ["sts.amazonaws.com"] + # thumbprint_list = [] # optional, defaults to ["6938fd4d98bab03faadb97b34396831e3780aea1"] + # github_actions_permissions_policy_json_path = "" # optional, defaults to "github_actions_permission_policy.json" + # add_read_only_access = bool # optional, defaults to false + } +``` + +## Permissions policy + +This module assumes that the permissions policy for the IAM role will be named +`github_actions_permission_policy.json` and located in the same folder as the +root module (the path and filename are configurable via the +`github_actions_permissions_policy_json_path` variable). An example policy: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": ["securityhub:BatchImportFindings"], + "Effect": "Allow", + "Resource": "*" + }, + { + "Sid": "UpdateService", + "Effect": "Allow", + "Action": ["ecs:UpdateService"], + "Resource": [ + "arn:aws:ecs:{your region}:{your account number}:service/{your self-hosted runner cluster name}/{your github runner service name}" + ] + } + ] +} +``` diff --git a/terraform/greenfield/bootstrap/environments/dev/backend.tfvars b/terraform/greenfield/bootstrap/environments/dev/backend.tfvars new file mode 100644 index 0000000000..560f0b17ea --- /dev/null +++ b/terraform/greenfield/bootstrap/environments/dev/backend.tfvars @@ -0,0 +1,4 @@ + +region = "us-east-1" +bucket = "eapd-tf-" +key = "bootstrap/dev/terraform.tfstate" diff --git a/terraform/greenfield/bootstrap/environments/dev/ecs_permission_policy.json b/terraform/greenfield/bootstrap/environments/dev/ecs_permission_policy.json new file mode 100644 index 0000000000..072cf63043 --- /dev/null +++ b/terraform/greenfield/bootstrap/environments/dev/ecs_permission_policy.json @@ -0,0 +1,84 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ec2:AuthorizeSecurityGroupEgress", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:CreateSecurityGroup", + "ec2:CreateTags", + "ec2:DeleteSecurityGroup", + "ec2:DescribeAccountAttributes", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSecurityGroups", + "ec2:DetachNetworkInterface", + "ec2:RevokeSecurityGroupEgress", + "ecr:CreateRepository", + "ecr:DeleteRepository", + "ecr:DescribeRepositories", + "ecr:ListTagsForResource", + "ecr:CompleteLayerUpload", + "ecr:GetAuthorizationToken", + "ecr:UploadLayerPart", + "ecr:InitiateLayerUpload", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage", + "ecs:CreateCluster", + "ecs:CreateService", + "ecs:DeleteCluster", + "ecs:DeleteService", + "ecs:DeregisterTaskDefinition", + "ecs:DescribeClusters", + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:PutClusterCapacityProviders", + "ecs:RegisterTaskDefinition", + "ecs:UpdateService", + "elasticloadbalancing:AddTags", + "elasticloadbalancing:CreateListener", + "elasticloadbalancing:CreateLoadBalancer", + "elasticloadbalancing:CreateRule", + "elasticloadbalancing:CreateTargetGroup", + "elasticloadbalancing:DeleteListener", + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:DeleteRule", + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeRules", + "elasticloadbalancing:DescribeTags", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:ModifyTargetGroupAttributes", + "elasticloadbalancing:SetSecurityGroups", + "iam:AttachRolePolicy", + "iam:CreatePolicy", + "iam:CreateRole", + "iam:DeletePolicy", + "iam:DeleteRole", + "iam:DetachRolePolicy", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:PassRole", + "iam:TagRole", + "iam:UntagRole", + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:DescribeLogGroups", + "logs:ListTagsLogGroup", + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": "*" + } + ] +} diff --git a/terraform/greenfield/bootstrap/environments/dev/inputs.tfvars b/terraform/greenfield/bootstrap/environments/dev/inputs.tfvars new file mode 100644 index 0000000000..2213f4caf8 --- /dev/null +++ b/terraform/greenfield/bootstrap/environments/dev/inputs.tfvars @@ -0,0 +1,131 @@ +environment = "dev" +subject_claim_filters = ["repo:Enterprise-CMCS/eAPD:*"] +github_actions_roles = [ + { + role_name: "github_ecs_dev_role", + description: "github actions ecs dev role ", + policy_name: "github_actions_ecs_dev_pipline_policy", + role_permissions_policy_json_path: "environments/dev/ecs_permission_policy.json" + } +] + +#rds +#rds +identifier = "eapd-dev" +engine = "postgres" +engine_version = "13.7" +instance_class = "db.t3.large" +allocated_storage = 100 +db_name = "eapd-dev-db" +username = "postgres" +port = 5432 +subnet_group_name = "eapd-dev-subnet-group" +parameter_group_name = "postgres13" +family = "postgres13" +subnet_ids = [ "subnet-01c495bc99a10d98b", "subnet-05f418b1af86a8610" ] +skip_final_snapshot = true +final_snapshot_identifier = "eapd-dev-postgres" +deletion_protection = false +aws_secretsmanager_secret_name = "eapd-dev-postgres-pass" +recovery_window_in_days = 0 +enabled_cloudwatch_logs_exports = [ "postgresql", "upgrade" ] + +#rds security group +rds_security_group_name = "eapd-dev-rds-sg" +rds_security_group_description = "eapd-dev-rds-sg" +rds_security_group_tags = { + Name: "eapd-dev-rds-sg" +} + +rds_ingresses = [ + { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + description = "db port" + cidr_blocks = [ "10.240.236.128/25", "10.240.236.0/25", "10.240.237.0/25", "10.0.0.0/8"] + self = false + security_groups = [] + } + +] +rds_egresses = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + description = "default egresses" + cidr_blocks = [ "0.0.0.0/0" ] + self = false + security_groups = [] + } +] + + +#EC2 +instance_type = "t3a.large" +subnet_id = "subnet-0f98328f0e04ca493" +additional_security_groups = [] +key_name = "eapd-dev" +monitoring = true +tags = { + Name: "mdb ec2 instance" +} + +volume_tags = { + Name: "mdb ec2 instance volume" +} + +root_block_device = [ + { + volume_size: "100" + } +] +ebs_block_device = [ + { + volume_size: "100" + device_name: "/dev/sdc" + } +] + +vpc_id = "vpc-00871580886ad7b56" +security_group_name = "mdn-sg" +security_group_description = "mdb instance" +security_group_tags = { + Name: "mdb sgs" +} + +ingresses = [ + { + from_port = 22 + to_port = 22 + protocol = "tcp" + description = "ssh" + cidr_blocks = [ "10.0.0.0/8"] + self = false + security_groups = [] + + }, + { + from_port = 27017 + to_port = 27017 + protocol = "tcp" + description = "db port" + cidr_blocks = [ "10.240.236.128/25", "10.240.236.0/25", "10.240.237.0/25", "10.0.0.0/8"] + self = false + security_groups = [] + } + +] +egresses = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + description = "default egresses" + cidr_blocks = [ "0.0.0.0/0" ] + self = false + security_groups = [] + } +] + diff --git a/terraform/greenfield/bootstrap/main.tf b/terraform/greenfield/bootstrap/main.tf new file mode 100644 index 0000000000..115367946c --- /dev/null +++ b/terraform/greenfield/bootstrap/main.tf @@ -0,0 +1,147 @@ +locals { + name_prefix = "${var.project}-${var.environment}" +} + +# Github OIDC +module "actions-aws-oidc-provider" { + source = "./modules/oidc/github" + + subject_claim_filters = var.subject_claim_filters + audience_list = var.audience_list + thumbprint_list = var.thumbprint_list + github_actions_roles = var.github_actions_roles +} + +# S3 Bucket Creation + + + +# ECR Repositories + +resource "aws_ecr_repository" "web-ecr" { + name = "${var.project}-${var.environment}-web" + image_scanning_configuration { + scan_on_push = true + } + + tags = { + Project = var.project + Team = var.team + Environment = var.environment + } +} + +resource "aws_ecr_repository" "api-ecr" { + name = "${var.project}-${var.environment}-api" + image_scanning_configuration { + scan_on_push = true + } + + tags = { + Project = var.project + Team = var.team + Environment = var.environment + } +} + +#RDS + +data "aws_caller_identity" "current" {} + +module "aws-parameter-group" { + source = "./modules/db_parameter_group" + name = var.parameter_group_name + family = var.family +} + +module "aws-subnet-group" { + source = "./modules/db_subnet_group" + name = var.subnet_group_name + subnet_ids = var.subnet_ids +} + + +module "aws-rds" { + source = "./modules/rds" + identifier = var.identifier + + engine = var.engine + engine_version = var.engine_version + instance_class = var.instance_class + allocated_storage = var.allocated_storage + license_model = var.license_model + + + username = var.username + password = var.password + port = var.port + + final_snapshot_identifier = var.final_snapshot_identifier + skip_final_snapshot = var.skip_final_snapshot + recovery_window_in_days = var.recovery_window_in_days + additional_security_groups = var.additional_security_groups + db_subnet_group_name = module.aws-subnet-group.db_subnet_group_name + parameter_group_name = module.aws-parameter-group.db_parameter_group_name + enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + + vpc_id = var.vpc_id + security_group_tags = var.rds_security_group_tags + ingresses = var.rds_ingresses + egresses = var.rds_egresses + security_group_name = var.rds_security_group_name + security_group_description = var.rds_security_group_description +} + +# EC2 +module "aws-ec2" { + source = "./modules/ec2" + instance_type = var.instance_type + subnet_id = var.subnet_id + additional_security_groups = var.additional_security_groups + key_name = var.key_name + monitoring = var.monitoring + root_block_device = var.root_block_device + ebs_block_device = var.ebs_block_device + tags = var.tags + volume_tags = var.volume_tags + + vpc_id = var.vpc_id + security_group_tags = var.security_group_tags + ingresses = var.ingresses + egresses = var.egresses + security_group_name = var.security_group_name + security_group_description = var.security_group_description + + +} + +#iam policy +resource "aws_iam_policy" "task_secrets" { + name_prefix = "${local.name_prefix}-task-secrets-policy" + path = "/delegatedadmin/developer/" + policy = <"] + + filter { + name = "name" + values = ["eAPD gf-dev Mongo AMI*"] + } + +} + +module "ec2-sgs" { + source = "../security-groups" + vpc_id = var.vpc_id + tags = var.security_group_tags + ingresses = var.ingresses + egresses = var.egresses + security_group_name = var.security_group_name + security_group_description = var.security_group_description +} + + +resource "aws_instance" "mongoDB-instance" { + + ami = data.aws_ami.mongoDB-ami.id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = concat([ module.ec2-sgs.security_group_id ], var.additional_security_groups ) + key_name = var.key_name + monitoring = var.monitoring + + dynamic "root_block_device" { + for_each = var.root_block_device + content { + volume_size = lookup(root_block_device.value, "volume_size", null) + } + } + + dynamic "ebs_block_device" { + for_each = var.ebs_block_device + content { + device_name = lookup(ebs_block_device.value, "device_name", null) + volume_size = lookup(ebs_block_device.value, "volume_size", null) + } + } + + tags = var.tags + volume_tags = var.volume_tags +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/ec2/outputs.tf b/terraform/greenfield/bootstrap/modules/ec2/outputs.tf new file mode 100644 index 0000000000..c1da5e5a70 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/ec2/outputs.tf @@ -0,0 +1,18 @@ +output "id" { + description = "The ID of the instance" + value = aws_instance.mongoDB-instance.id +} +output "instance_state" { + description = "The state of the instance. One of: `pending`, `running`, `shutting-down`, `terminated`, `stopping`, `stopped`" + value = aws_instance.mongoDB-instance.instance_state +} +output "private_ip" { + description = "The private IP address assigned to the instance." + value = aws_instance.mongoDB-instance.private_ip +} + +output "public_ip" { + description = "The public IP address assigned to the instance" + value = aws_instance.mongoDB-instance.public_ip +} + diff --git a/terraform/greenfield/bootstrap/modules/ec2/variables.tf b/terraform/greenfield/bootstrap/modules/ec2/variables.tf new file mode 100644 index 0000000000..eb18a3deaf --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/ec2/variables.tf @@ -0,0 +1,114 @@ +# EC2 +variable "ami" { + description = "ID of AMI to use for the instance" + type = string + default = null +} + +variable "instance_type" { + description = "The type of instance to start" + type = string + default = "t3.micro" +} + +variable "subnet_id" { + description = "The VPC Subnet ID to launch in" + type = string + default = null +} + +variable "key_name" { + description = "Key name of the Key Pair to use for the instance; which can be managed using the `aws_key_pair` resource" + type = string + default = null +} + +variable "monitoring" { + description = "If true, the launched EC2 instance will have detailed monitoring enabled" + type = bool + default = true +} + +variable "additional_security_groups" { + description = "A list of additional security group IDs to associate with" + type = list(string) + default = null +} + +variable "root_block_device" { + description = "Customize details about the root block device of the instance. See Block Devices below for details" + type = list(any) + default = [] +} + +variable "ebs_block_device" { + description = "Additional EBS block devices to attach to the instance" + type = list(map(string)) + default = [] +} + +variable "volume_tags" { + description = "A mapping of tags to assign to the devices created by the instance at launch time" + type = map(string) + default = {} +} + +variable "tags" { + description = "A map of tags to add to the ec2 instance created" + type = map(string) + default = {} +} + + +# Security group +variable "security_group_name" { + description = "security group name" + type = string + default = "" +} + +variable "security_group_description" { + description = "security group description" + type = string + default = "" +} + +variable "vpc_id" { + description = "vpc ID" + type = string + default = "" +} + +variable "security_group_tags" { + description = "tags to add " + type = map(any) + default = null +} + +variable "ingresses" { + description = "List of ingress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + +variable "egresses" { + description = "List of egress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/mongodb/main.tf b/terraform/greenfield/bootstrap/modules/mongodb/main.tf new file mode 100644 index 0000000000..c269e4afae --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/mongodb/main.tf @@ -0,0 +1,44 @@ +resource "random_password" "db_password" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret" "db_password" { + name = var.aws_secretsmanager_secret_name + recovery_window_in_days = var.recovery_window_in_days +} + +resource "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret.db_password.id + secret_string = random_password.db_password.result + +} + +data "aws_ami" "latest_mongo_image" { + most_recent = true + owners = [var.owners] + filter { + name = "name" + values = [var.ami_filter_values] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +resource "aws_instance" "mongo_db" { + ami = data.aws_ami.latest_mongo_image.id + instance_type = var.instance_type + vpc_security_group_ids = var.vpc_security_group_ids + key_name = var.key_name + iam_instance_profile = var.iam_instance_profile + hibernation = final_snapshot_identifier + depends_on = [aws_security_group.eapd-production-mongo-ec2] + tags = var.tags + user_data = <<-EOL + #!/bin/bash -xe + sudo sh -c "echo license_key: ${var.newrelic_liscense_key} >> /etc/newrelic-infra.yml" + EOL +} diff --git a/terraform/greenfield/bootstrap/modules/mongodb/outputs.tf b/terraform/greenfield/bootstrap/modules/mongodb/outputs.tf new file mode 100644 index 0000000000..f304f97e94 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/mongodb/outputs.tf @@ -0,0 +1,4 @@ +output "db_endpoint" { + description = "The endpoint of the RDS database" + value = aws_db_instance.db.endpoint +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/mongodb/variable.tf b/terraform/greenfield/bootstrap/modules/mongodb/variable.tf new file mode 100644 index 0000000000..52985f1189 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/mongodb/variable.tf @@ -0,0 +1,6 @@ + +variable "tags" { + description = "tags to add " + type = map(any) + default = {} +} diff --git a/terraform/greenfield/bootstrap/modules/oidc/github/main.tf b/terraform/greenfield/bootstrap/modules/oidc/github/main.tf new file mode 100644 index 0000000000..83701ff696 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/oidc/github/main.tf @@ -0,0 +1,48 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_openid_connect_provider" "github_actions" { + client_id_list = var.audience_list + thumbprint_list = var.thumbprint_list + url = "https://token.actions.githubusercontent.com" +} + +data "aws_iam_policy_document" "github_actions_assume_role" { + statement { + sid = "RoleForGitHubActions" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = var.audience_list + } + + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = var.subject_claim_filters + } + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github_actions.arn] + } + } +} + +resource "aws_iam_role" "github_actions_oidc_role" { + for_each = {for role in var.github_actions_roles: role.role_name => role} + name = lookup(each.value, "role_name") + description = lookup(each.value, "description") + assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json + path = "/delegatedadmin/developer/" # Please remove if permission boundary is not needed + permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/cms-cloud-admin/developer-boundary-policy" # Please remove if permission boundary policy is not needed +} + +resource "aws_iam_role_policy" "github_actions_permissions" { + for_each = {for role in var.github_actions_roles: role.role_name => role} + name = lookup(each.value, "policy_name") + role = aws_iam_role.github_actions_oidc_role[each.value.role_name].id + policy = file("${path.root}/${lookup(each.value, "role_permissions_policy_json_path")}") +} diff --git a/terraform/greenfield/bootstrap/modules/oidc/github/outputs.tf b/terraform/greenfield/bootstrap/modules/oidc/github/outputs.tf new file mode 100644 index 0000000000..2dd55db375 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/oidc/github/outputs.tf @@ -0,0 +1,11 @@ +output "github_actions_oidc_iam_provider_arn"{ + description = "The ARN of the AWS OIDC identity provider" + value = aws_iam_openid_connect_provider.github_actions.arn +} + +output "role_arn"{ + value = { + for k, role in aws_iam_role.github_actions_oidc_role : k => role.arn + } +} + diff --git a/terraform/greenfield/bootstrap/modules/oidc/github/variables.tf b/terraform/greenfield/bootstrap/modules/oidc/github/variables.tf new file mode 100644 index 0000000000..8147d94495 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/oidc/github/variables.tf @@ -0,0 +1,29 @@ +variable "subject_claim_filters" { + description = "A list of valid subject claim filters" + type = list(string) +} + +variable "audience_list" { + description = "A list of allowed audiences (AKA client IDs) for the AWS identity provider" + type = list(string) + default = ["sts.amazonaws.com"] # the default audience for the GitHub OIDC provider, see https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-the-identity-provider-to-aws +} + +variable "thumbprint_list" { + description = " A list of thumbprints for the OIDC identity provider's server certificate" + type = list(string) + default = ["6938fd4d98bab03faadb97b34396831e3780aea1"] # see https://github.blog/changelog/2022-01-13-github-actions-update-on-oidc-based-deployments-to-aws/ and https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html +} + +variable github_actions_roles { + description = "list of roles" + type = list(object({ + + role_name: string + description: string + policy_name: string + role_permissions_policy_json_path: string + + })) + default = [] +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/rds/main.tf b/terraform/greenfield/bootstrap/modules/rds/main.tf new file mode 100644 index 0000000000..68514dd07e --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/rds/main.tf @@ -0,0 +1,75 @@ +#Generate a random password +resource "random_password" "db_password" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +#Push the random password to Aws Secret Manager +resource "aws_secretsmanager_secret" "db_credential_secret" { + name = var.aws_secretsmanager_secret_name + recovery_window_in_days = var.recovery_window_in_days +} + +resource "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret.db_credential_secret.id + secret_string = random_password.db_password.result + +} + +module "rds-sgs" { + source = "../security-groups" + vpc_id = var.vpc_id + tags = var.security_group_tags + ingresses = var.ingresses + egresses = var.egresses + security_group_name = var.security_group_name + security_group_description = var.security_group_description +} + +resource "aws_db_instance" "pg-db" { + + identifier = var.identifier + + engine = var.engine + engine_version = var.engine_version + instance_class = var.instance_class + allocated_storage = var.allocated_storage + license_model = var.license_model + + + username = var.username + password = random_password.db_password.result + port = var.port + + + availability_zone = var.availability_zone + multi_az = var.multi_az + vpc_security_group_ids = concat([ module.rds-sgs.security_group_id], var.additional_security_groups) + db_subnet_group_name = var.db_subnet_group_name + parameter_group_name = var.parameter_group_name + publicly_accessible = var.publicly_accessible + + + skip_final_snapshot = var.skip_final_snapshot + final_snapshot_identifier = var.final_snapshot_identifier + deletion_protection = var.deletion_protection + enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + storage_encrypted = true + + tags = var.tags + +} + +# Cloudwatch Log group +resource "aws_cloudwatch_log_group" "db-log-group" { + for_each = toset([for log in var.enabled_cloudwatch_logs_exports : log if var.create_cloudwatch_log_group ]) + + name = "/aws/rds/instance/${var.identifier}/${each.value}" + retention_in_days = var.cloudwatch_log_group_retention_in_days + kms_key_id = var.cloudwatch_log_group_kms_key_id + + tags = var.tags +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/rds/outputs.tf b/terraform/greenfield/bootstrap/modules/rds/outputs.tf new file mode 100644 index 0000000000..a2b964ccbf --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/rds/outputs.tf @@ -0,0 +1,4 @@ +output "db_endpoint" { + description = "The endpoint of the RDS database" + value = aws_db_instance.pg-db.endpoint +} diff --git a/terraform/greenfield/bootstrap/modules/rds/variable.tf b/terraform/greenfield/bootstrap/modules/rds/variable.tf new file mode 100644 index 0000000000..99ab22f738 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/rds/variable.tf @@ -0,0 +1,232 @@ + +variable "tags" { + description = "tags to add " + type = map(any) + default = {} +} + +variable "identifier" { + description = "The name of the RDS instance" + type = string +} + +variable "additional_security_groups" { + description = "List of additional security group ids to attach to the DB" + type = list(string) + default = [] +} + +variable "allocated_storage" { + description = "The allocated storage in gigabytes" + type = string + default = null +} + + +variable "license_model" { + description = "License model information for this DB instance. Optional, but required for some DB s, i.e. Oracle SE1" + type = string + default = null +} + + +variable "engine" { + description = "The database engine to use" + type = string + default = null +} + +variable "engine_version" { + description = "The engine version to use" + type = string + default = null +} + +variable "instance_class" { + description = "The instance type of the RDS instance" + type = string + default = null +} + +variable "db_name" { + description = "The DB name to create. If omitted, no database is created initially" + type = string + default = null +} + +variable "username" { + description = "Username for the master DB user" + type = string + default = null +} + +variable "password" { + description = "Password for the master DB user. Note that this may show up in logs, and it will be stored in the state file" + type = string + default = null +} + +variable "port" { + description = "The port on which the DB accepts connections" + type = string + default = null +} + + +variable "skip_final_snapshot" { + description = "Determines whether a final DB snapshot is created before the DB instance is deleted. If true is specified, no DBSnapshot is created. If false is specified, a DB snapshot is created before the DB instance is deleted" + type = bool + #default = true +} + +variable "final_snapshot_identifier" { + description = "The name to the final snapshot on db destroy" + type = string + default = "" +} + +variable "db_subnet_group_name" { + description = "Name of DB subnet group. DB instance will be created in the VPC associated with the DB subnet group. If unspecified, will be created in the default VPC" + type = string + default = null +} + +variable "parameter_group_name" { + description = "Name of the DB parameter group to associate" + type = string + default = null +} + +variable "deletion_protection" { + description = "The database can't be deleted when this value is set to true." + type = bool + default = false +} + + +variable "prevent_destroy" { + description = "prevent terraform destroying db if value is set to true." + type = bool + default = true +} + +variable "publicly_accessible" { + description = "Bool to control if instance is publicly accessible" + type = bool + default = false +} + +variable "availability_zone" { + description = "The Availability Zone of the RDS instance" + type = string + default = null +} + +variable "multi_az" { + description = "Specifies if the RDS instance is multi-AZ" + type = bool + default = false +} + +variable "aws_secretsmanager_secret_name" { + description = "Name of aws secretsmanager secret" + type = string + default = null +} + +variable "recovery_window_in_days" { + description = "recovery window in days" + type = number + default = 0 +} + +# Cloudwatch Log group +variable "create_cloudwatch_log_group" { + description = "Determines whether a CloudWatch log group is created for each `enabled_cloudwatch_logs_exports`" + type = bool + default = false +} + +variable "cloudwatch_log_group_retention_in_days" { + description = "The number of days to retain CloudWatch logs for the DB instance" + type = number + default = 7 +} + +variable "cloudwatch_log_group_kms_key_id" { + description = "The ARN of the KMS Key to use when encrypting log data" + type = string + default = null +} + +variable "enabled_cloudwatch_logs_exports" { + description = "List of log types to enable for exporting to CloudWatch logs. If omitted, no logs will be exported. Valid values (depending on engine): alert, audit, error, general, listener, slowquery, trace, postgresql (PostgreSQL), upgrade (PostgreSQL)." + type = list(string) + default = [] +} + +variable "backup_retention_period" { + description = "The days to retain backups for" + type = number + default = 7 +} + +variable "backup_window" { + description = "The daily time range (in UTC) during which automated backups are created if they are enabled." + type = string + default = "03:00-06:00" +} + +# Security group +variable "security_group_name" { + description = "security group name" + type = string + default = "" +} + +variable "security_group_description" { + description = "security group description" + type = string + default = "" +} + +variable "vpc_id" { + description = "vpc ID" + type = string + default = "" +} + +variable "security_group_tags" { + description = "tags to add " + type = map(any) + default = null +} + +variable "ingresses" { + description = "List of ingress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + +variable "egresses" { + description = "List of egress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + diff --git a/terraform/greenfield/bootstrap/modules/security-groups/main.tf b/terraform/greenfield/bootstrap/modules/security-groups/main.tf new file mode 100644 index 0000000000..879c5aa8b0 --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/security-groups/main.tf @@ -0,0 +1,32 @@ +resource "aws_security_group" "sg" { + name = var.security_group_name + description = var.security_group_description + vpc_id = var.vpc_id + tags = var.tags + + dynamic "ingress" { + for_each = var.ingresses + content { + from_port = ingress.value.from_port + to_port = ingress.value.to_port + protocol = ingress.value.protocol + description = ingress.value.description + cidr_blocks = ingress.value.cidr_blocks + security_groups = ingress.value.security_groups + self = ingress.value.self + } + } + + dynamic "egress" { + for_each = var.egresses + content { + from_port = egress.value.from_port + to_port = egress.value.to_port + protocol = egress.value.protocol + description = egress.value.description + cidr_blocks = egress.value.cidr_blocks + security_groups = egress.value.security_groups + self = egress.value.self + } + } +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/security-groups/outputs.tf b/terraform/greenfield/bootstrap/modules/security-groups/outputs.tf new file mode 100644 index 0000000000..4108d47a4f --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/security-groups/outputs.tf @@ -0,0 +1,4 @@ +output "security_group_id" { + description = "security group ID" + value = aws_security_group.sg.id +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/modules/security-groups/variables.tf b/terraform/greenfield/bootstrap/modules/security-groups/variables.tf new file mode 100644 index 0000000000..a215c63fcc --- /dev/null +++ b/terraform/greenfield/bootstrap/modules/security-groups/variables.tf @@ -0,0 +1,51 @@ +variable "security_group_name" { + description = "security group name" + type = string + default = "" +} + +variable "security_group_description" { + description = "security group description" + type = string + default = "" +} + +variable "vpc_id" { + description = "vpc ID" + type = string + default = "" +} + +variable "tags" { + description = "tags to add " + type = map(any) + default = null +} + +variable "ingresses" { + description = "List of ingress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + +variable "egresses" { + description = "List of egress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/outputs.tf b/terraform/greenfield/bootstrap/outputs.tf new file mode 100644 index 0000000000..41fa92c064 --- /dev/null +++ b/terraform/greenfield/bootstrap/outputs.tf @@ -0,0 +1,27 @@ +# github oidc +output "oidc_iam_role_arns"{ + description = "OIDC role arn" + value = module.actions-aws-oidc-provider.role_arn +} + +# ecr repository for web +output "web_ecr_repository" { + description = "web ecr repository url" + value = aws_ecr_repository.web-ecr.repository_url +} + +# ecr repository for api +output "api_ecr_repository" { + description = "api ecr repository url" + value = aws_ecr_repository.api-ecr.repository_url +} + +output "db_endpoint" { + description = "The endpoint of the RDS database" + value = module.aws-rds.db_endpoint +} + +output "task_secret_policy_arn" { + description = "policy arn of task secrets policy " + value = aws_iam_policy.task_secrets.arn +} \ No newline at end of file diff --git a/terraform/greenfield/bootstrap/providers.tf b/terraform/greenfield/bootstrap/providers.tf new file mode 100644 index 0000000000..9f0a7ffe28 --- /dev/null +++ b/terraform/greenfield/bootstrap/providers.tf @@ -0,0 +1,21 @@ +provider "aws" { + region = "us-east-1" +} + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + } + + required_version = ">= 0.14.6" + + backend "s3" { + dynamodb_table = "terraform-state-lock" + region = "us-east-1" + encrypt = "true" + } +} + diff --git a/terraform/greenfield/bootstrap/variable.tf b/terraform/greenfield/bootstrap/variable.tf new file mode 100644 index 0000000000..19526f0a40 --- /dev/null +++ b/terraform/greenfield/bootstrap/variable.tf @@ -0,0 +1,421 @@ +variable "subject_claim_filters" { + description = "A list of valid subject claim filters" + type = list(string) +} + +# General +variable "region" { + default = "us-east-1" + description = "AWS region" +} + +variable "environment" { + description = "Environment name" +} + +variable "project" { + default = "eapd" + description = "Project name" +} + +variable "team" { + default = "eapd" + description = "Team name" +} + +# Github OIDC +variable "audience_list" { + description = "A list of allowed audiences (AKA client IDs) for the AWS identity provider" + type = list(string) + default = ["sts.amazonaws.com"] # the default audience for the GitHub OIDC provider, see https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-the-identity-provider-to-aws +} + +variable "thumbprint_list" { + description = " A list of thumbprints for the OIDC identity provider's server certificate" + type = list(string) + default = ["6938fd4d98bab03faadb97b34396831e3780aea1"] # see https://github.blog/changelog/2022-01-13-github-actions-update-on-oidc-based-deployments-to-aws/ and https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html +} + +variable github_actions_roles { + description = "list of roles" + type = list(object({ + + role_name: string + description: string + policy_name: string + role_permissions_policy_json_path: string + + })) + default = [] +} + +#RDS + +variable "tags" { + description = "tags to add " + type = map(any) + default = {} +} + +variable "identifier" { + description = "The name of the RDS instance" + type = string +} + +variable "vpc_security_group_ids" { + description = "List of security group ids to attach to the DB" + type = list(string) + default = [] +} + +variable "allocated_storage" { + description = "The allocated storage in gigabytes" + type = string + default = null +} + + +variable "license_model" { + description = "License model information for this DB instance. Optional, but required for some DB s, i.e. Oracle SE1" + type = string + default = null +} + + +variable "engine" { + description = "The database engine to use" + type = string + default = null +} + +variable "engine_version" { + description = "The engine version to use" + type = string + default = null +} + +variable "instance_class" { + description = "The instance type of the RDS instance" + type = string + default = null +} + +variable "db_name" { + description = "The DB name to create. If omitted, no database is created initially" + type = string + default = null +} + +variable "username" { + description = "Username for the master DB user" + type = string + default = null +} + +variable "password" { + description = "Password for the master DB user. Note that this may show up in logs, and it will be stored in the state file" + type = string + default = null +} + +variable "port" { + description = "The port on which the DB accepts connections" + type = string + default = null +} + + +variable "skip_final_snapshot" { + description = "Determines whether a final DB snapshot is created before the DB instance is deleted. If true is specified, no DBSnapshot is created. If false is specified, a DB snapshot is created before the DB instance is deleted" + type = bool + default = true +} + +variable "final_snapshot_identifier" { + description = "The name to the final snapshot on db destroy" + type = string + default = "" +} + +variable "db_subnet_group_name" { + description = "Name of DB subnet group. DB instance will be created in the VPC associated with the DB subnet group. If unspecified, will be created in the default VPC" + type = string + default = null +} + +variable "parameter_group_name" { + description = "Name of the DB parameter group to associate" + type = string + default = null +} + +variable "deletion_protection" { + description = "The database can't be deleted when this value is set to true." + type = bool + default = false +} + + +variable "prevent_destroy" { + description = "prevent terraform destroying db if value is set to true." + type = bool + default = true +} + + +variable "publicly_accessible" { + description = "Bool to control if instance is publicly accessible" + type = bool + default = false +} + +variable "availability_zone" { + description = "The Availability Zone of the RDS instance" + type = string + default = null +} + +variable "multi_az" { + description = "Specifies if the RDS instance is multi-AZ" + type = bool + default = false +} + +variable "aws_secretsmanager_secret_name" { + description = "Name of aws secretsmanager secret" + type = string + default = null +} + +variable "recovery_window_in_days" { + description = "recovery window in days" + type = number + default = 0 +} + +variable "create_cloudwatch_log_group" { + description = "Determines whether a CloudWatch log group is created for each `enabled_cloudwatch_logs_exports`" + type = bool + default = true +} + +variable "cloudwatch_log_group_retention_in_days" { + description = "The number of days to retain CloudWatch logs for the DB instance" + type = number + default = 7 +} + +variable "cloudwatch_log_group_kms_key_id" { + description = "The ARN of the KMS Key to use when encrypting log data" + type = string + default = null +} + +variable "enabled_cloudwatch_logs_exports" { + description = "List of log types to enable for exporting to CloudWatch logs. If omitted, no logs will be exported. Valid values (depending on engine): alert, audit, error, general, listener, slowquery, trace, postgresql (PostgreSQL), upgrade (PostgreSQL)." + type = list(string) + default = [] +} + +variable "subnet_ids" { + description = "A list of VPC subnet IDs" + type = list(string) + default = [] +} + +variable "subnet_group_name" { + description = "The name of the DB subnet group" + type = string + default = "" +} + +variable "family" { + description = "The family of the DB parameter group" + type = string + default = null +} + +variable "parameters" { + description = "A list of DB parameter maps to apply" + type = list(map(string)) + default = [] +} + +variable "backup_retention_period" { + description = "The days to retain backups for" + type = number + default = 7 +} + +variable "backup_window" { + description = "The daily time range (in UTC) during which automated backups are created if they are enabled." + type = string + default = "03:00-06:00" +} + +# rds Security group +variable "rds_security_group_name" { + description = "security group name" + type = string + default = "" +} + +variable "rds_security_group_description" { + description = "security group description" + type = string + default = "" +} + +variable "rds_security_group_tags" { + description = "tags to add " + type = map(any) + default = null +} + +variable "rds_ingresses" { + description = "List of ingress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + +variable "rds_egresses" { + description = "List of egress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + +# EC2 +variable "ami" { + description = "ID of AMI to use for the instance" + type = string + default = null +} + +variable "instance_type" { + description = "The type of instance to start" + type = string + default = "t3.micro" +} + +variable "subnet_id" { + description = "The VPC Subnet ID to launch in" + type = string + default = null +} + +variable "key_name" { + description = "Key name of the Key Pair to use for the instance; which can be managed using the `aws_key_pair` resource" + type = string + default = null +} + +variable "monitoring" { + description = "If true, the launched EC2 instance will have detailed monitoring enabled" + type = bool + default = true +} + +variable "additional_security_groups" { + description = "A list of additional security group IDs to associate with" + type = list(string) + default = null +} + +variable "root_block_device" { + description = "Customize details about the root block device of the instance. See Block Devices below for details" + type = list(any) + default = [] +} + +variable "ebs_block_device" { + description = "Additional EBS block devices to attach to the instance" + type = list(map(string)) + default = [] +} + +variable "volume_tags" { + description = "A mapping of tags to assign to the devices created by the instance at launch time" + type = map(string) + default = {} +} + +variable "sg_tags" { + description = "A map of tags to add to the ec2 instance created" + type = map(string) + default = {} +} + + +# Security group +variable "security_group_name" { + description = "security group name" + type = string + default = "" +} + +variable "security_group_description" { + description = "security group description" + type = string + default = "" +} + +variable "vpc_id" { + description = "vpc ID" + type = string + default = "" +} + +variable "security_group_tags" { + description = "tags to add " + type = map(any) + default = null +} + +variable "ingresses" { + description = "List of ingress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + + variable "egresses" { + description = "List of egress rules to create by name" + type = list(object({ + from_port = number + to_port = number + protocol = string + description = string + cidr_blocks = list(string) + security_groups = list(string) + self = bool + })) + default = [] + } + +# S3 Buckets + + +# Databases diff --git a/terraform/greenfield/ecs/README.md b/terraform/greenfield/ecs/README.md new file mode 100644 index 0000000000..b371bee227 --- /dev/null +++ b/terraform/greenfield/ecs/README.md @@ -0,0 +1 @@ +# Terraform ecs cluster diff --git a/terraform/greenfield/ecs/envs/dev/backend.tfvars b/terraform/greenfield/ecs/envs/dev/backend.tfvars new file mode 100644 index 0000000000..a50767ec1b --- /dev/null +++ b/terraform/greenfield/ecs/envs/dev/backend.tfvars @@ -0,0 +1,3 @@ +region = "us-east-1" +bucket = "eapd-tf-" +key = "ecs/dev/terraform.tfstate" diff --git a/terraform/greenfield/ecs/envs/dev/inputs.tfvars b/terraform/greenfield/ecs/envs/dev/inputs.tfvars new file mode 100644 index 0000000000..56ef6c0a03 --- /dev/null +++ b/terraform/greenfield/ecs/envs/dev/inputs.tfvars @@ -0,0 +1,7 @@ +environment = "dev" +project = "eapd" +vpc_azs = ["us-east-1a", "us-east-1b"] +vpc_id = "" +task_secrets_policy_name = "eapd-dev-task-secrets-policy20230321185946729200000002" +public_subnets = ["", "", ""] +private_subnets = ["", "", ""] \ No newline at end of file diff --git a/terraform/greenfield/ecs/main.tf b/terraform/greenfield/ecs/main.tf new file mode 100644 index 0000000000..1699ddb4ec --- /dev/null +++ b/terraform/greenfield/ecs/main.tf @@ -0,0 +1,127 @@ +locals { + name_prefix = "${var.project}-${var.environment}" +} + +data "aws_caller_identity" "current" {} + +#Generate a secret token +resource "random_password" "secret_token" { + length = 12 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +#Push the random password to Aws Secret Manager +resource "aws_secretsmanager_secret" "secret_token" { + name = "/eapd/ecs/${var.environment}/JWT_SECRET" + recovery_window_in_days = var.recovery_window_in_days +} + +resource "aws_secretsmanager_secret_version" "secret_token" { + secret_id = aws_secretsmanager_secret.secret_token.id + secret_string = random_password.secret_token.result + +} + +module "ecs-fargate-service" { + source = "./modules/fargate" + name_prefix = local.name_prefix + project = var.project + team = var.team + region = var.region + environment = var.environment + aws_account = var.aws_account + task_secrets_policy_name = var.task_secrets_policy_name + + vpc_id = var.vpc_id + vpc_public_subnets = var.public_subnets + vpc_private_subnets = var.private_subnets + + web_image = var.web_image + api_image = var.api_image + + web_definitions = { + "TEALIUM_ENV" = "dev" + } + api_definitions = { + "API_PORT" = "8001", + "API_URL" = "/api", + "PORT" = "8001" + } + web_secrets = [ + { + name: "OKTA_CLIENT_ID", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "OKTA_API_KEY", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "LD_API_KEY", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "LD_CLIENT_ID", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "TEALIUM_TAG", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "SNYK_API_KEY", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "OKTA_SERVER_ID", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "OKTA_DOMAIN", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/OKTA_DOMAIN-fGZrNh" + } + + ] + api_secrets = [ + { + name: "API_DATABASE_URL", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "LD_API_KEY", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "LD_CLIENT_ID", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "MONGO_URL", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "OKTA_API_KEY", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "OKTA_CLIENT_ID", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "OKTA_SERVER_ID", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/apI_definition-DhHcRi" + }, + { + name: "JWT_SECRET", + valueFrom: "${aws_secretsmanager_secret.secret_token.arn}" + }, + { + name: "OKTA_DOMAIN", + valueFrom: "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/eapd/ecs/dev/OKTA_DOMAIN-fGZrNh" + } + ] + + web_health_check_path = "/" + api_health_check_path = "/api/heartbeat" +} diff --git a/terraform/greenfield/ecs/modules/fargate/alb.tf b/terraform/greenfield/ecs/modules/fargate/alb.tf new file mode 100644 index 0000000000..2a58be60f2 --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/alb.tf @@ -0,0 +1,126 @@ +resource "aws_lb" "lb" { + name = "${substr(var.name_prefix, 0, 29)}-lb" + load_balancer_type = "application" + internal = false + subnets = var.vpc_public_subnets + + security_groups = [aws_security_group.allow-external1.id] +} + +resource "aws_security_group" "allow-external1" { + description = "Allows external https traffic" + + egress { + cidr_blocks = ["0.0.0.0/0"] + from_port = "0" + protocol = "-1" + self = "false" + to_port = "0" + } + + ingress { + cidr_blocks = ["0.0.0.0/0"] + from_port = "443" + protocol = "tcp" + self = "false" + to_port = "443" + } + + ingress { + cidr_blocks = ["0.0.0.0/0"] + from_port = "80" + protocol = "tcp" + self = "false" + to_port = "80" + } + + name = "${var.name_prefix}_allow-external" + vpc_id = var.vpc_id +} + +resource "aws_lb_listener" "lb_listener" { + load_balancer_arn = aws_lb.lb.arn + port = "443" + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-2016-08" + certificate_arn = "arn:aws:acm:us-east-1:894719201277:certificate/11b646b4-3b7c-444e-8f78-84b75e7e82de" + + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.web.arn + } +} + +resource "aws_lb_listener_rule" "lb_listner_rule" { + listener_arn = aws_lb_listener.lb_listener.arn + priority = 100 + + action { + type = "forward" + target_group_arn = aws_lb_target_group.api.arn + } + + condition { + path_pattern { + values = ["/api/*"] + } + } + +} + +resource "aws_lb_target_group" "web" { + deregistration_delay = "30" + + health_check { + enabled = "true" + healthy_threshold = "2" + interval = "10" + matcher = "200" + path = var.web_health_check_path + port = "traffic-port" + protocol = "HTTP" + timeout = "8" + unhealthy_threshold = "10" + } + + load_balancing_algorithm_type = "round_robin" + name = "${substr(var.name_prefix, 0, 29)}-web-tg" + port = var.web_port + protocol = "HTTP" + slow_start = "120" + target_type = "ip" + vpc_id = var.vpc_id + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_lb_target_group" "api" { + deregistration_delay = "30" + + health_check { + enabled = "true" + healthy_threshold = "2" + interval = "10" + matcher = "204" + path = var.api_health_check_path + port = "traffic-port" + protocol = "HTTP" + timeout = "8" + unhealthy_threshold = "10" + } + + load_balancing_algorithm_type = "round_robin" + name = "${substr(var.name_prefix, 0, 29)}-api-tg" + port = var.api_port + protocol = "HTTP" + slow_start = "120" + target_type = "ip" + vpc_id = var.vpc_id + + lifecycle { + create_before_destroy = true + } +} \ No newline at end of file diff --git a/terraform/greenfield/ecs/modules/fargate/ecs.tf b/terraform/greenfield/ecs/modules/fargate/ecs.tf new file mode 100644 index 0000000000..3aa9d31bd3 --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/ecs.tf @@ -0,0 +1,200 @@ +resource "aws_ecs_cluster" "cluster" { + name = var.name_prefix + setting { + name = "containerInsights" + value = "enabled" + } +} + +resource "aws_ecs_cluster_capacity_providers" "cluster" { + cluster_name = aws_ecs_cluster.cluster.name + capacity_providers = ["FARGATE"] +} + +resource "aws_ecs_service" "web_service" { + name = "${var.name_prefix}-web" + cluster = aws_ecs_cluster.cluster.id + desired_count = var.web_replicas + wait_for_steady_state = true + deployment_maximum_percent = var.deployment_maximum_percent + deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent + enable_ecs_managed_tags = "false" + health_check_grace_period_seconds = var.health_check_grace_period_seconds + launch_type = "FARGATE" + platform_version = var.platform_version + scheduling_strategy = "REPLICA" + task_definition = aws_ecs_task_definition.web.arn + enable_execute_command = true + + deployment_controller { + type = "ECS" + } + + load_balancer { + container_name = "${var.name_prefix}-web" + container_port = var.web_port + target_group_arn = aws_lb_target_group.web.arn + } + + network_configuration { + assign_public_ip = "true" + security_groups = [aws_security_group.allow-external.id] + subnets = var.vpc_public_subnets + } + + depends_on = [ + aws_ecs_task_definition.web, + aws_lb.lb + ] +} + +resource "aws_ecs_service" "api_service" { + name = "${var.name_prefix}-api" + cluster = aws_ecs_cluster.cluster.id + desired_count = var.api_replicas + wait_for_steady_state = true + deployment_maximum_percent = var.deployment_maximum_percent + deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent + enable_ecs_managed_tags = "false" + health_check_grace_period_seconds = var.health_check_grace_period_seconds + launch_type = "FARGATE" + platform_version = var.platform_version + scheduling_strategy = "REPLICA" + task_definition = aws_ecs_task_definition.api.arn + enable_execute_command = true + + deployment_controller { + type = "ECS" + } + + load_balancer { + container_name = "${var.name_prefix}-api" + container_port = var.api_port + target_group_arn = aws_lb_target_group.api.arn + } + + network_configuration { + assign_public_ip = "true" + security_groups = [aws_security_group.allow-external.id] + subnets = var.vpc_private_subnets + } + + depends_on = [ + aws_ecs_task_definition.api, + aws_lb.lb + ] +} + +resource "aws_cloudwatch_log_group" "ecs_web_logs" { + name = "${var.name_prefix}-web-logs" + + tags = { + Project = var.project + Environment = var.environment + Team = var.team + } +} + +resource "aws_cloudwatch_log_group" "ecs_api_logs" { + name = "${var.name_prefix}-api-logs" + + tags = { + Project = var.project + Environment = var.environment + Team = var.team + } +} + +resource "aws_ecs_task_definition" "web" { + execution_role_arn = aws_iam_role.task.arn + family = "${var.name_prefix}-web" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + task_role_arn = aws_iam_role.task.arn + cpu = var.web_cpu + memory = var.web_memory + container_definitions = jsonencode([ + { + name = "${var.name_prefix}-web" + image = var.web_image + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = "${var.name_prefix}-web-logs", + awslogs-region = "us-east-1", + awslogs-stream-prefix = var.project, + awslogs-create-group = "true" + } + } + portMappings = [ + { + containerPort = var.web_port + hostPort = var.web_port + protocol = "tcp" + } + ] + essential = true + environment = local.web_definitions + secrets = var.web_secrets + } + ] + ) +} + +resource "aws_ecs_task_definition" "api" { + execution_role_arn = aws_iam_role.task.arn + family = "${var.name_prefix}-api" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + task_role_arn = aws_iam_role.task.arn + cpu = var.api_cpu + memory = var.api_memory + container_definitions = jsonencode([ + { + name = "${var.name_prefix}-api" + image = var.api_image + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = "${var.name_prefix}-api-logs", + awslogs-region = "us-east-1", + awslogs-stream-prefix = var.project, + awslogs-create-group = "true" + } + } + portMappings = [ + { + containerPort = var.api_port + hostPort = var.api_port + protocol = "tcp" + } + ] + essential = true + environment = local.api_definitions + secrets = var.api_secrets + } + ] + ) +} + +locals { + web_definitions = flatten([ + for k, v in var.web_definitions : [ + { + name = k + value = v + } + ] + ]) +} + +locals { + api_definitions = flatten([ + for k, v in var.api_definitions : [ + { + name = k + value = v + } + ] + ]) +} diff --git a/terraform/greenfield/ecs/modules/fargate/iam.tf b/terraform/greenfield/ecs/modules/fargate/iam.tf new file mode 100644 index 0000000000..dfd190cb16 --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/iam.tf @@ -0,0 +1,32 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "task" { + name = "${var.name_prefix}-task-role" + path = "/delegatedadmin/developer/" # please remove if policy boundary is not needed + assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume_role.json + permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/cms-cloud-admin/developer-boundary-policy" # please remove if permission boundary is not required +} + +data "aws_iam_policy_document" "ecs_tasks_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role_policy_attachment" "task" { + role = aws_iam_role.task.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role_policy_attachment" "task-secrets" { + role = aws_iam_role.task.name + policy_arn = "arn:aws:iam::${var.aws_account}:policy/delegatedadmin/developer/${var.task_secrets_policy_name}" # update path if policy boundary is not needed +} + + + diff --git a/terraform/greenfield/ecs/modules/fargate/outputs.tf b/terraform/greenfield/ecs/modules/fargate/outputs.tf new file mode 100644 index 0000000000..8ed2201443 --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/outputs.tf @@ -0,0 +1,4 @@ +output "load_balancer_endpoint" { + description = "endpoint for loadbalancer" + value = aws_lb.lb.dns_name +} \ No newline at end of file diff --git a/terraform/greenfield/ecs/modules/fargate/providers.tf b/terraform/greenfield/ecs/modules/fargate/providers.tf new file mode 100644 index 0000000000..a61fb73fa6 --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.15.0" + } + } +} diff --git a/terraform/greenfield/ecs/modules/fargate/sg.tf b/terraform/greenfield/ecs/modules/fargate/sg.tf new file mode 100644 index 0000000000..8e9169874c --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/sg.tf @@ -0,0 +1,53 @@ +resource "aws_security_group" "allow-external" { + description = "Allows external traffic" + + egress { + cidr_blocks = ["0.0.0.0/0"] + from_port = "0" + protocol = "-1" + self = "false" + to_port = "0" + } + ingress { + cidr_blocks = ["10.0.0.0/8"] + from_port = "443" + protocol = "tcp" + self = "false" + to_port = "443" + } + + ingress { + cidr_blocks = ["10.0.0.0/8"] + from_port = "8080" + protocol = "tcp" + self = "false" + to_port = "8080" + } + + ingress { + cidr_blocks = ["10.0.0.0/8"] + from_port = "8000" + protocol = "tcp" + self = "false" + to_port = "8000" + } + + ingress { + cidr_blocks = ["10.0.0.0/8"] + from_port = "8001" + protocol = "tcp" + self = "false" + to_port = "8001" + } + + ingress { + cidr_blocks = ["10.0.0.0/8"] + from_port = "80" + protocol = "tcp" + self = "false" + to_port = "80" + } + + name = "${var.name_prefix}_allow-external-ecs" + vpc_id = var.vpc_id +} \ No newline at end of file diff --git a/terraform/greenfield/ecs/modules/fargate/variables.tf b/terraform/greenfield/ecs/modules/fargate/variables.tf new file mode 100644 index 0000000000..71e2b531f7 --- /dev/null +++ b/terraform/greenfield/ecs/modules/fargate/variables.tf @@ -0,0 +1,189 @@ +# General +variable "name_prefix" { + type = string + description = "Prefix to apply to resources" +} + +variable "project" { + default = "" + description = "Project, aka the application name" +} + +variable "environment" { + default = "" + description = "Environment for deployment" +} + +variable "team" { + default = "" + description = "Team name" +} + +variable "region" { + default = "" + description = "AWS region for resources" +} + + +# Networking Info +variable "vpc_id" { + description = "VPC ID to use for the resources" +} + +variable "vpc_public_subnets" { + type = list(any) + description = "Public Subnets for VPC" +} + +variable "vpc_private_subnets" { + type = list(any) + description = "Private Subnets for VPC" +} + + + +# ECS Info +variable "ecs_cluster" { + default = null + description = "ECS Cluster name to deploy the service" +} + +variable "platform_version" { + default = "1.4.0" + description = "Platform version on which to run your service" +} + +variable "web_cpu" { + default = 512 + type = number + description = "Number of cpu units used by the task" +} + +variable "web_memory" { + default = 1024 + type = number + description = "Amount (in MiB) of memory used by the task" +} + +variable "api_cpu" { + default = 512 + type = number + description = "Number of cpu units used by the task" +} + +variable "api_memory" { + default = 1024 + type = number + description = "Amount (in MiB) of memory used by the task" +} + +variable "web_replicas" { + default = "1" + description = "Number of containers (instances) to run for web" +} + +variable "api_replicas" { + default = "1" + description = "Number of containers (instances) to run for api" +} + +variable "deployment_maximum_percent" { + default = "200" + description = "Max percentage of the service's desired count during deployment" +} + +variable "deployment_minimum_healthy_percent" { + default = "100" + description = "Min percentage of the service's desired count during deployment" +} + + +# Healthcheck +variable "web_health_check_path" { + default = "/" + description = "Service health endpoint to check" +} + +variable "api_health_check_path" { + default = "/api" + description = "API service health endpoint to check" +} + +variable "web_path" { + default = "/*" + description = "Path to configure the alb listener" +} + +variable "api_path" { + default = "/api/*" + description = "Path to configure the alb listener" +} + +variable "web_port" { + default = 80 + type = number + description = "Port the application runs on" +} + +variable "api_port" { + default = 8001 + type = number + description = "API port the application runs on" +} + + +# Permissions +variable "task_iam_policy" { + default = "" + description = "Policy document for ecs task" +} + +variable "web_definitions" { + description = "Map of environment variables for the application" +} + +variable "api_definitions" { + description = "Map of environment variables for the api" +} + +variable "web_secrets" { + type = list(any) + default = [] + sensitive = true + description = "List of sensative data to inject into the container definitions" +} + +variable "api_secrets" { + type = list(any) + default = [] + sensitive = true + description = "List of sensative data to inject into the container definitions" +} + +variable "health_check_grace_period_seconds" { + default = "120" + description = "Seconds to ignore failing load balancer health checks on new tasks" +} + + +# Docker Images +variable "web_image" { + type = string + description = "Docker image name with tag for web" +} + +variable "api_image" { + type = string + description = "Docker image name with tag for api" +} + +variable "aws_account" { + description = "AWS account number" + default = "" +} + +variable "task_secrets_policy_name" { + default = "" + description = "task secrets policy name" + type = string +} \ No newline at end of file diff --git a/terraform/greenfield/ecs/outputs.tf b/terraform/greenfield/ecs/outputs.tf new file mode 100644 index 0000000000..bc6ffff160 --- /dev/null +++ b/terraform/greenfield/ecs/outputs.tf @@ -0,0 +1,4 @@ +output "application_endpoint" { + description = "Endpoint for application" + value = module.ecs-fargate-service.load_balancer_endpoint +} \ No newline at end of file diff --git a/terraform/greenfield/ecs/providers.tf b/terraform/greenfield/ecs/providers.tf new file mode 100644 index 0000000000..91f783650c --- /dev/null +++ b/terraform/greenfield/ecs/providers.tf @@ -0,0 +1,26 @@ +terraform { + backend "s3" {} + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.15.0" + } + + random = { + source = "hashicorp/random" + } + } +} + +provider "aws" { + region = var.region + default_tags { + tags = { + Environment = var.environment + Team = var.team + Application = var.project + Platform = var.platform + Terraform = "true" + } + } +} \ No newline at end of file diff --git a/terraform/greenfield/ecs/variables.tf b/terraform/greenfield/ecs/variables.tf new file mode 100644 index 0000000000..3b0098d2f1 --- /dev/null +++ b/terraform/greenfield/ecs/variables.tf @@ -0,0 +1,93 @@ +# General +variable "region" { + default = "us-east-1" + description = "AWS region" +} + +variable "environment" { + default = "stage" + description = "Environment name" +} + +variable "project" { + default = "eapd" + description = "Project name" +} + +variable "team" { + default = "eapd" + description = "Team name" +} + +variable "platform" { + description = "Platform name used for infrastructure deployment" + default = "ecs" +} + +# VPC Info +variable "vpc_id" { + description = "VPC ID" + type = string +} + +variable "vpc_cidr" { + description = "VPC CIDR" + default = "" +} + +variable "vpc_azs" { + description = "VPC availability zones" + type = list(any) +} + +variable "public_subnets" { + description = "List of public subnets for ALB deployment" + type = list(any) +} + +variable "private_subnets" { + description = "List of private subnets for ECS cluster deployment" + type = list(any) +} + +# Docker Images +variable "web_image" { + default = "894719201277.dkr.ecr.us-east-1.amazonaws.com/eapd-dev-web:c7e0d11" + description = "Docker image name with tag for web" + type = string +} + +variable "api_image" { + default = "894719201277.dkr.ecr.us-east-1.amazonaws.com/eapd-dev-api:c7e0d11" + description = "Docker image name with tag for api" + type = string +} + +variable "web_port" { + default = 8000 + type = number + description = "Port the application runs on" +} + +variable "api_port" { + default = 8001 + type = number + description = "API port the application runs on" +} + +variable "aws_account" { + description = "AWS account number" + default = "" +} + +variable "task_secrets_policy_name" { + default = "" + description = "task secrets policy name" + type = string +} + +variable "recovery_window_in_days" { + description = "recovery window in days" + type = number + default = 0 +} \ No newline at end of file diff --git a/web/DockerfileECS b/web/DockerfileECS new file mode 100644 index 0000000000..5336c44b85 --- /dev/null +++ b/web/DockerfileECS @@ -0,0 +1,31 @@ +ARG OKTA_DOMAIN +ARG TEALIUM_ENV +ARG LD_CLIENT_ID +ARG TEALIUM_TAG + +FROM node:16.19.1-bullseye-slim as builder +RUN apt-get update; \ + apt-get install -y python3; \ + apt-get install -y python2 make; \ + apt-get install -y g++; \ + ln -s /usr/bin/python2.7 /usr/bin/python; + +RUN mkdir /app +WORKDIR /app + +COPY package.json yarn.lock ./ +COPY web/package.json ./web/package.json +COPY common ./common +COPY web/src/static ./web/src/static + +RUN yarn install --pure-lockfile + +COPY web ./web + +WORKDIR /app/web +RUN LD_CLIENT_ID=${LD_CLIENT_ID} TEALIUM_ENV=${TEALIUM_ENV} TEALIUM_TAG=${TEALIUM_TAG} OKTA_DOMAIN=${OKTA_DOMAIN} yarn build + +# --- + +FROM nginx:1.23.3 +COPY --from=builder /app/web/dist /usr/share/nginx/html \ No newline at end of file