diff --git a/.gitignore b/.gitignore index f6fc7b26..ff57159d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ crash.log # control as they are data points which are potentially sensitive and subject # to change depending on the environment. # -*.tfvars +#*.tfvars # Ignore override files as they are usually used to override resources locally and so # are not checked in diff --git a/application-code/ecsdemo-cicd/appspec.json b/application-code/ecsdemo-cicd/appspec.json new file mode 100644 index 00000000..f94b2492 --- /dev/null +++ b/application-code/ecsdemo-cicd/appspec.json @@ -0,0 +1,29 @@ +{ + "version": 0.0, + "Resources": [ + { + "TargetService": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": "", + "LoadBalancerInfo": { + "ContainerName": "fargate_task_container", + "ContainerPort": 80 + }, + "PlatformVersion": "LATEST", + "NetworkConfiguration": { + "awsvpcConfiguration": { + "subnets": [ + "" + ], + "securityGroups": [ + "" + ], + "assignPublicIp": "DISABLED" + } + } + } + } + } + ] + } diff --git a/application-code/ecsdemo-cicd/buildspec-cicd.yml b/application-code/ecsdemo-cicd/buildspec-cicd.yml new file mode 100644 index 00000000..1024f174 --- /dev/null +++ b/application-code/ecsdemo-cicd/buildspec-cicd.yml @@ -0,0 +1,30 @@ +version: 0.2 + +phases: + pre_build: + commands: + - echo $REPO_URL + - REPOSITORY=${REPO_URL%/*} + - echo Logging in to Amazon ECR... + - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPOSITORY + - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) + - IMAGE_TAG=${COMMIT_HASH:=latest} + build: + commands: + - echo Build started on `date` + - echo Building the Docker image... + - docker build -t $REPO_URL . + post_build: + commands: + - echo Building Task Definition + - python3 create-configs.py $REPO_URL:$IMAGE_TAG core-infra-external-state ecsdemo-frontend development us-west-2 + - echo Pushing the Docker image... + - docker tag $REPO_URL $REPO_URL:$IMAGE_TAG + - docker push $REPO_URL:$IMAGE_TAG + - echo Preparing spec files in new folder + - mkdir artifacts + - printf '[{"name":"%s","imageUri":"%s"}]' "$CONTAINER_NAME" "$REPO_URL:$IMAGE_TAG" > artifacts/imagedefinitions.json + - cat artifacts/imagedefinitions.json +artifacts: + files: + - '**/*' diff --git a/application-code/ecsdemo-cicd/create-configs.py b/application-code/ecsdemo-cicd/create-configs.py new file mode 100644 index 00000000..73fd199d --- /dev/null +++ b/application-code/ecsdemo-cicd/create-configs.py @@ -0,0 +1,114 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 + +#Example Command +# python3 create-configs.py public.ecr.aws/docker/library/httpd:latest core-infra ecsdemo-frontend development us-west-2 + +import boto3 +import json +import sys +import re + +#input new ecs image +image = sys.argv[1] + +#input ecs cluster name +cluster_name = sys.argv[2] + +#input ecs service name +service_name = sys.argv[3] + +#input app environment +app_environment = sys.argv[4] + +#input region +region = sys.argv[5] + +################################################################################ +# AppSpec +################################################################################ + +# AWS ECS client +ecs_client = boto3.client('ecs') + +service_information = ecs_client.describe_services( + cluster=cluster_name, + services=[ + service_name, + ], +) + +subnets=service_information['services'][0]['taskSets'][0]['networkConfiguration']['awsvpcConfiguration']['subnets'] +security_groups=service_information['services'][0]['taskSets'][0]['networkConfiguration']['awsvpcConfiguration']['securityGroups'] +container_name=service_information['services'][0]['taskSets'][0]['loadBalancers'][0]['containerName'] +task_definition=service_information['services'][0]['taskDefinition'] + +# Removing the last colon and the following numbers +updated_arn = re.sub(r':\d+$', '', task_definition) + +# Extracting task revision numbers +rev_number = re.findall(r'\d+$', task_definition) + +if rev_number: + # Incrementing task revision numbers + updated_numbers = int(rev_number[0]) + 1 + + # Constructing the updated ARN with the incremented numbers + new_task_definition_arn = f'{updated_arn}:{updated_numbers}' + +with open('appspec.json', 'r') as file: + app_spec_original_json = json.load(file) + +app_spec_original_json['Resources'][0]['TargetService']['Properties']['TaskDefinition'] = new_task_definition_arn +app_spec_original_json['Resources'][0]['TargetService']['Properties']['LoadBalancerInfo']['ContainerName'] = container_name +app_spec_original_json['Resources'][0]['TargetService']['Properties']['NetworkConfiguration']['awsvpcConfiguration']['subnets'] = subnets +app_spec_original_json['Resources'][0]['TargetService']['Properties']['NetworkConfiguration']['awsvpcConfiguration']['securityGroups'] = security_groups + +# Save the modified JSON to a new file +app_spec_file_name = f'{app_environment}-appspec.json.json' + +# Save the modified JSON to a new file +with open(app_spec_file_name, 'w') as file: + json.dump(app_spec_original_json, file, indent=2) + +################################################################################ +# Task Def +################################################################################ + +# Get task definition details using AWS SDK +response = ecs_client.describe_task_definition(taskDefinition=task_definition) + +task_definition_details = response['taskDefinition'] + +# Extracting required values +execution_role_arn = task_definition_details['executionRoleArn'] +task_role_arn = task_definition_details['taskRoleArn'] +cpu = task_definition_details['cpu'] +memory = task_definition_details['memory'] +family = task_definition_details['family'] +name = task_definition_details['containerDefinitions'][0]['name'] +log_group = task_definition_details['containerDefinitions'][0]['logConfiguration']['options']['awslogs-group'] +log_region = task_definition_details['containerDefinitions'][0]['logConfiguration']['options']['awslogs-region'] +log_prefix = task_definition_details['containerDefinitions'][0]['logConfiguration']['options']['awslogs-stream-prefix'] + +# Load the original JSON file +with open('task-definition.json', 'r') as file: + task_def_original_json = json.load(file) + +# Replace placeholders with extracted values +task_def_original_json['executionRoleArn'] = execution_role_arn +task_def_original_json['taskRoleArn'] = task_role_arn +task_def_original_json['cpu'] = cpu +task_def_original_json['memory'] = memory +task_def_original_json['family'] = family +task_def_original_json['containerDefinitions'][0]['name'] = name +task_def_original_json['containerDefinitions'][0]['image'] = image +task_def_original_json['containerDefinitions'][0]['logConfiguration']['options']['awslogs-group'] = log_group +task_def_original_json['containerDefinitions'][0]['logConfiguration']['options']['awslogs-region'] = log_region +task_def_original_json['containerDefinitions'][0]['logConfiguration']['options']['awslogs-stream-prefix'] = log_prefix + +# Save the modified JSON to a new file +task_def_file_name = f'{app_environment}-task-definition.json' + +# Save the modified JSON to a new file +with open(task_def_file_name, 'w') as file: + json.dump(task_def_original_json, file, indent=2) diff --git a/application-code/ecsdemo-cicd/service-definition.json b/application-code/ecsdemo-cicd/service-definition.json new file mode 100644 index 00000000..a9640e00 --- /dev/null +++ b/application-code/ecsdemo-cicd/service-definition.json @@ -0,0 +1,28 @@ +{ + "taskDefinition": "", + "cluster": "default", + "loadBalancers": [ + { + "targetGroupArn": "", + "containerName": "", + "containerPort": 80 + } + ], + "desiredCount": 3, + "launchType": "FARGATE", + "schedulingStrategy": "REPLICA", + "deploymentController": { + "type": "CODE_DEPLOY" + }, + "networkConfiguration": { + "awsvpcConfiguration": { + "subnets": [ + "" + ], + "securityGroups": [ + "" + ], + "assignPublicIp": "DISABLED" + } + } +} diff --git a/application-code/ecsdemo-cicd/task-definition.json b/application-code/ecsdemo-cicd/task-definition.json new file mode 100644 index 00000000..5216c052 --- /dev/null +++ b/application-code/ecsdemo-cicd/task-definition.json @@ -0,0 +1,42 @@ +{ + "family": "", + "containerDefinitions": [ + { + "portMappings": [ + { + "hostPort": 80, + "protocol": "tcp", + "containerPort": 80 + } + ], + "image": "", + "essential": true, + "name": "", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "", + "awslogs-region": "", + "awslogs-stream-prefix": "" + } + }, + "privileged": false, + "linuxParameters": { + "capabilities": { + "drop": [ + "SYS_ADMIN", + "NET_ADMIN" + ] + } + } + } + ], + "cpu": "", + "memory": "", + "taskRoleArn": "", + "executionRoleArn": "", + "requiresCompatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc" +} diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 00000000..ff57159d --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,79 @@ +build/ +plan.out +plan.out.json + +# Local .terraform directories +.terraform/ + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +# +#*.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* + +# Ignore CLI configuration files +.terraformrc +terraform.rc +.terraform.lock.hcl + +go.mod +go.sum + +# Build files +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.envrc + +# CDK directories +cdk.out/ +cdk.context.json + +# Python +__pycache__/ + +# Customized ENV files +.env* +.venv/ +output.json diff --git a/terraform/cicd-examples/README.md b/terraform/cicd-examples/README.md new file mode 100644 index 00000000..174e7ea4 --- /dev/null +++ b/terraform/cicd-examples/README.md @@ -0,0 +1,124 @@ +# ECS + AWS CodePipeline Deployment Example + +This repository serves as an example for educational purposes only. It is not intended for production use. Make sure to review and customize the scripts and configurations according to your specific requirements and security best practices. + +## Overview + +This directory contains an examples of Terraform code to orchestrate the deployment of infrastructure components and the deployment of new containers via green / blue. + +State for the terraform is stored external in a S3 bucket. + +It containers two CodePipeline examples one which will deploy the infrastructure as code (IAC) and another which will deliver new containers to the deployed infrastructure. Often the application teams are not the teams responsible for the deployment in infrastructure. These two pipelines simulate that separation of duty. + +By default these examples will deploy in us-west-2. + +## Prerequisites + +Before deploying this example, ensure you have the following: + +- An AWS account with the necessary permissions. +- AWS CLI configured with the appropriate credentials. +- JQ installed (https://jqlang.github.io/jq/download/) + +## Deployment Steps + +1. Clone this repository to your local machine. + + ```bash + git clone https://github.com/aws-ia/ecs-blueprints + cd ecs-blueprints + ``` + +2. Copy Git ignore to sub dir for future push. + ```bash + cp .gitignore ./terraform/.gitignore + +2. Deploy S3 bucket used for external state. + + ```bash + cd terraform/cicd-examples/external-state-bucket + terraform init + terraform apply + ``` + +3. Set local environment variable to reference deployed state bucket. + + ``` + STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket --region us-west-2 | jq -r '.Parameters[0].Value') + ``` + +4. Deploy the CodePipeline which will deploy the required infrastructure. + ```bash + cd ../iac-pipeline + terraform init + terraform apply -var="s3_bucket=$STATE_BUCKET" + ``` + +5. Commit Terraform code to IAC repository. This will deploy the VPC, ECS Cluster, Load Balancer, and ECS Service. This make take several minutes. + + ``` + #Get IAC code commit repo + YOUR_CODE_COMMIT_IAC_REPO=$(aws codecommit get-repository --repository-name iac_sample_repo --query 'repositoryMetadata.cloneUrlHttp' --region us-west-2 | jq -r .) + + #CD to terraform folder + cd ../../../terraform + + git init + git remote add origin $YOUR_CODE_COMMIT_IAC_REPO + git add . + git commit -m "initial commit" + git push origin main + ``` +6. Deploy the CodePipeline which will build and deploy new containers. + + ```bash + cd cicd-examples/lb-service-container-pipeline + terraform init + terraform apply + ``` + +7. Once the Pipeline has fully deployed the environment we can begin to CI/CD new containers. + + ``` + # CD to sample app director + cd ../../../application-code/ecsdemo-cicd/ + + #Get Application code commit repo + YOUR_CODE_COMMIT_APP_REPO=$(aws codecommit get-repository --repository-name ecs_service_repo --query 'repositoryMetadata.cloneUrlHttp' --region us-west-2 | jq -r .) + + git init + git add . + git remote add origin $YOUR_CODE_COMMIT_APP_REPO + git commit -m "initial application deployment" + git push origin main + ``` + +## Clean up + +Starting from the ECS-Blueprints/Terraform folder + +``` +cd cicd-examples/lb-service-container-pipeline/ +terraform destroy -var="s3_bucket=$STATE_BUCKET" +``` + + +``` +cd ../iac-pipeline +terraform destroy -var="s3_bucket=$STATE_BUCKET" +``` + + +``` +cd ../lb-service-external-state +terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=lb-service-dev.tfstate" -backend-config="region=us-west-2" -reconfigure +terraform destroy -var-file=../dev.tfvars +``` + +``` +cd ../core-infra-external-state +terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=core-infra-dev.tfstate" -backend-config="region=us-west-2" -reconfigure +terraform destroy -var-file=../dev.tfvars +``` + +For detailed information on AWS CodePipeline and ECS, refer to the [AWS documentation](https://docs.aws.amazon.com/). diff --git a/terraform/cicd-examples/core-infra-external-state/README.md b/terraform/cicd-examples/core-infra-external-state/README.md new file mode 100644 index 00000000..554cf6db --- /dev/null +++ b/terraform/cicd-examples/core-infra-external-state/README.md @@ -0,0 +1,91 @@ +# Core Infrastructure +This folder contains the Terraform code to deploy the core infratructure for an ECS Fargate workload. The AWS resources created by the script are: +* Networking + * VPC + * 3 public subnets, 1 per AZ. If a region has less than 3 AZs it will create same number of public subnets as AZs. + * 3 private subnets, 1 per AZ. If a region has less than 3 AZs it will create same number of private subnets as AZs. + * 1 NAT Gateway + * 1 Internet Gateway + * Associated Route Tables +* 1 ECS Cluster with AWS CloudWatch Container Insights enabled. +* Task execution IAM role +* CloudWatch log groups +* CloudMap service discovery namespace `default` + +## Getting Started +Make sure you have all the [prerequisites](../../../README.md) for your laptop. + +## Usage +* Clone the forked repository from your account (not the one from the aws-ia organization) and change the directory to the appropriate one as shown below: +```bash +cd ecs-blueprints/terraform/fargate-examples/core-infra/ +``` +* Run Terraform init to download the providers and install the modules +```shell +terraform init +``` +* Review the terraform plan output, take a look at the changes that terraform will execute, and then apply them: +```shell +terraform plan +terraform apply --auto-approve +``` +## Outputs +After the execution of the Terraform code you will get an output with needed IDs and values needed as input for the nexts Terraform applies. You can use this infrastructure to run other example blueprints, all you need is the `cluster_name`. + +## Cleanup +Run the following command if you want to delete all the resources created before. If you have created other blueprints and they use these infrastructure then destroy those blueprint resources first. +```shell +terraform destroy +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ecs](#module\_ecs) | terraform-aws-modules/ecs/aws | ~> 5.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_service_discovery_private_dns_namespace.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_private_dns_namespace) | resource | +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [environment](#input\_environment) | What environment this is associate with. | `string` | `"development"` | no | +| [region](#input\_region) | AWS region you want to deploy to. | `string` | `"us-west-2"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [ecs\_cluster\_id](#output\_ecs\_cluster\_id) | The ID of the ECS cluster | +| [ecs\_cluster\_name](#output\_ecs\_cluster\_name) | The name of the ECS cluster and the name of the core stack | +| [ecs\_task\_execution\_role\_arn](#output\_ecs\_task\_execution\_role\_arn) | The ARN of the task execution role | +| [ecs\_task\_execution\_role\_name](#output\_ecs\_task\_execution\_role\_name) | The ARN of the task execution role | +| [private\_subnets](#output\_private\_subnets) | A list of private subnets for the client app | +| [private\_subnets\_cidr\_blocks](#output\_private\_subnets\_cidr\_blocks) | A list of private subnets CIDRs | +| [public\_subnets](#output\_public\_subnets) | A list of public subnets | +| [service\_discovery\_namespaces](#output\_service\_discovery\_namespaces) | Service discovery namespaces already available | +| [vpc\_id](#output\_vpc\_id) | The ID of the VPC | + diff --git a/terraform/cicd-examples/core-infra-external-state/main.tf b/terraform/cicd-examples/core-infra-external-state/main.tf new file mode 100644 index 00000000..ee166131 --- /dev/null +++ b/terraform/cicd-examples/core-infra-external-state/main.tf @@ -0,0 +1,105 @@ + +# STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') + +# terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=core-infra-dev.tfstate" -backend-config="region=us-west-2" +# terraform apply -var-file=../dev.tfvars +# terraform destroy -var-file=../dev.tfvars + +# terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=core-infra-qa.tfstate" -backend-config="region=us-west-2" +# terraform apply -var-file=../qa.tfvars +# terraform destroy -var-file=../qa.tfvars + +provider "aws" { + region = var.region +} + +# Terraform backend configuration to store state in S3 +terraform { + backend "s3" {} +} + +data "aws_availability_zones" "available" {} +data "aws_caller_identity" "current" {} + +locals { + name = basename(path.cwd) + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + + tags = { + Blueprint = local.name + GithubRepo = "github.com/aws-ia/ecs-blueprints" + Environment = var.environment + } +} + +################################################################################ +# ECS Blueprint +################################################################################ + +module "ecs" { + source = "terraform-aws-modules/ecs/aws" + version = "~> 5.0" + + cluster_name = local.name + + cluster_service_connect_defaults = { + namespace = aws_service_discovery_private_dns_namespace.this.arn + } + + fargate_capacity_providers = { + FARGATE = {} + FARGATE_SPOT = {} + } + + # Shared task execution role + create_task_exec_iam_role = false + # Allow read access to all SSM params in current account for demo + task_exec_ssm_param_arns = ["arn:aws:ssm:${var.region}:${data.aws_caller_identity.current.account_id}:parameter/*"] + # Allow read access to all secrets in current account for demo + task_exec_secret_arns = ["arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.current.account_id}:secret:*"] + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = local.name + cidr = local.vpc_cidr + + azs = local.azs + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)] + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 10)] + + enable_nat_gateway = true + single_nat_gateway = true + enable_dns_hostnames = true + + # Manage so we can name + manage_default_network_acl = true + default_network_acl_tags = { Name = "${local.name}-default" } + manage_default_route_table = true + default_route_table_tags = { Name = "${local.name}-default" } + manage_default_security_group = true + default_security_group_tags = { Name = "${local.name}-default" } + + tags = local.tags +} + +################################################################################ +# Service discovery namespaces +################################################################################ + +resource "aws_service_discovery_private_dns_namespace" "this" { + name = "default.${local.name}.local" + description = "Service discovery namespace.clustername.local" + vpc = module.vpc.vpc_id + + tags = local.tags +} diff --git a/terraform/cicd-examples/core-infra-external-state/outputs.tf b/terraform/cicd-examples/core-infra-external-state/outputs.tf new file mode 100644 index 00000000..705ab513 --- /dev/null +++ b/terraform/cicd-examples/core-infra-external-state/outputs.tf @@ -0,0 +1,44 @@ +output "vpc_id" { + description = "The ID of the VPC" + value = module.vpc.vpc_id +} + +output "public_subnets" { + description = "A list of public subnets" + value = module.vpc.public_subnets +} + +output "private_subnets" { + description = "A list of private subnets for the client app" + value = module.vpc.private_subnets +} + +output "private_subnets_cidr_blocks" { + description = "A list of private subnets CIDRs" + value = module.vpc.private_subnets_cidr_blocks +} + +output "ecs_cluster_name" { + description = "The name of the ECS cluster and the name of the core stack" + value = module.ecs.cluster_name +} + +output "ecs_cluster_id" { + description = "The ID of the ECS cluster" + value = module.ecs.cluster_id +} + +output "ecs_task_execution_role_name" { + description = "The ARN of the task execution role" + value = module.ecs.task_exec_iam_role_name +} + +output "ecs_task_execution_role_arn" { + description = "The ARN of the task execution role" + value = module.ecs.task_exec_iam_role_arn +} + +output "service_discovery_namespaces" { + description = "Service discovery namespaces already available" + value = aws_service_discovery_private_dns_namespace.this +} diff --git a/terraform/cicd-examples/core-infra-external-state/variables.tf b/terraform/cicd-examples/core-infra-external-state/variables.tf new file mode 100644 index 00000000..19c9f0e9 --- /dev/null +++ b/terraform/cicd-examples/core-infra-external-state/variables.tf @@ -0,0 +1,11 @@ +variable "region" { + type = string + default = "us-west-2" + description = "AWS region you want to deploy to." +} + +variable "environment" { + type = string + default = "development" + description = "What environment this is associate with." +} diff --git a/terraform/cicd-examples/core-infra-external-state/versions.tf b/terraform/cicd-examples/core-infra-external-state/versions.tf new file mode 100644 index 00000000..ddfcb0e0 --- /dev/null +++ b/terraform/cicd-examples/core-infra-external-state/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/terraform/cicd-examples/dev.tfvars b/terraform/cicd-examples/dev.tfvars new file mode 100644 index 00000000..950363d2 --- /dev/null +++ b/terraform/cicd-examples/dev.tfvars @@ -0,0 +1,2 @@ +region = "us-west-2" +environment = "development" diff --git a/terraform/cicd-examples/external-state-bucket/main.tf b/terraform/cicd-examples/external-state-bucket/main.tf new file mode 100644 index 00000000..2cb3ebc6 --- /dev/null +++ b/terraform/cicd-examples/external-state-bucket/main.tf @@ -0,0 +1,22 @@ +provider "aws" { + region = var.region +} + +resource "aws_s3_bucket" "terraform_state_bucket" { + tags = { + Name = "TerraformStateBucket" + } +} + +resource "aws_s3_bucket_versioning" "terraform_state_bucket" { + bucket = aws_s3_bucket.terraform_state_bucket.id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_ssm_parameter" "state_bucket" { + name = "terraform_state_bucket" + type = "String" + value = aws_s3_bucket.terraform_state_bucket.bucket +} diff --git a/terraform/cicd-examples/external-state-bucket/variables.tf b/terraform/cicd-examples/external-state-bucket/variables.tf new file mode 100644 index 00000000..236eadae --- /dev/null +++ b/terraform/cicd-examples/external-state-bucket/variables.tf @@ -0,0 +1,5 @@ +variable "region" { + type = string + default = "us-west-2" + description = "AWS region you want to deploy to." +} diff --git a/terraform/cicd-examples/external-state-bucket/versions.tf b/terraform/cicd-examples/external-state-bucket/versions.tf new file mode 100644 index 00000000..ddfcb0e0 --- /dev/null +++ b/terraform/cicd-examples/external-state-bucket/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/terraform/cicd-examples/iac-pipeline/dev-core-infra-deploy-buildspec.yml b/terraform/cicd-examples/iac-pipeline/dev-core-infra-deploy-buildspec.yml new file mode 100644 index 00000000..83c773b4 --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/dev-core-infra-deploy-buildspec.yml @@ -0,0 +1,21 @@ +version: 0.2 +env: + variables: + MAJOR: "1" + MINOR: "1" +phases: + install: + commands: + - wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + - sudo apt update && sudo apt install terraform + pre_build: + commands: + - echo "pre_build step" + build: + commands: + - echo "build command" + - STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') + - cd cicd-examples/core-infra-external-state + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=core-infra-dev.tfstate" -backend-config="region=us-west-2" + - terraform apply -var-file=../dev.tfvars -auto-approve diff --git a/terraform/cicd-examples/iac-pipeline/dev-lb-service-deploy-buildspec.yml b/terraform/cicd-examples/iac-pipeline/dev-lb-service-deploy-buildspec.yml new file mode 100644 index 00000000..754f4fec --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/dev-lb-service-deploy-buildspec.yml @@ -0,0 +1,22 @@ +version: 0.2 +env: + variables: + MAJOR: "1" + MINOR: "1" +phases: + install: + commands: + - ls + - wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + - sudo apt update && sudo apt install terraform + pre_build: + commands: + - echo "pre_build step" + build: + commands: + - echo "build command" + - STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') + - cd cicd-examples/lb-service-external-state + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=lb-service-dev.tfstate" -backend-config="region=us-west-2" + - terraform apply -var-file=../dev.tfvars -auto-approve diff --git a/terraform/cicd-examples/iac-pipeline/main.tf b/terraform/cicd-examples/iac-pipeline/main.tf new file mode 100644 index 00000000..a1546e93 --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/main.tf @@ -0,0 +1,213 @@ +# STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') +# terraform apply -var="s3_bucket=$STATE_BUCKET" + +provider "aws" { + region = "us-west-2" +} + +data "aws_s3_bucket" "example" { + bucket = var.s3_bucket +} + +# CodeCommit repository +resource "aws_codecommit_repository" "example_repo" { + repository_name = "iac_sample_repo" +} + +################################################################################ +# CodeBuild Modules to Deploy Terraform IAC +################################################################################ + +module "deploy_dev_core_infra" { + source = "../../modules/codebuild-iac" + iam_role_name = "deploy_dev_core_infra" + buildspec_path = "./dev-core-infra-deploy-buildspec.yml" + name = "deploy_dev_core_infra" +} + +module "deploy_dev_lb_service" { + source = "../../modules/codebuild-iac" + iam_role_name = "deploy_dev_lb_service" + buildspec_path = "./dev-lb-service-deploy-buildspec.yml" + + name = "deploy_dev_lb_service" +} + +/* +module "deploy_qa_core_infra" { + source = "../../modules/codebuild-iac" + iam_role_name = "deploy_qa_core_infra" + buildspec_path = "./qa-core-infra-deploy-buildspec.yml" + s3_bucket_name = data.aws_s3_bucket.example.id + name = "deploy_qa_core_infra" +} + +module "deploy_qa_lb_service" { + source = "../../modules/codebuild-iac" + iam_role_name = "deploy_qa_lb_service" + buildspec_path = "./qa-lb-service-deploy-buildspec.yml" + s3_bucket_name = data.aws_s3_bucket.example.id + name = "deploy_qa_lb_service" +} +*/ + +################################################################################ +# CodePipeline Permissions +################################################################################ + +resource "aws_iam_role" "codepipeline_role" { + name = "codepipeline-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "codepipeline.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodePipeline_FullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "codecommit_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeCommitFullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "s3_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "codebuild_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "cloudwatch_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +################################################################################ +# CodePipeline +################################################################################ + +resource "aws_codepipeline" "example_pipeline" { + name = "example-iac-pipeline" + role_arn = aws_iam_role.codepipeline_role.arn + + artifact_store { + location = data.aws_s3_bucket.example.bucket + type = "S3" + } + + stage { + name = "Source" + + action { + name = "SourceAction" + category = "Source" + owner = "AWS" + provider = "CodeCommit" + version = "1" + output_artifacts = ["SourceArtifact"] + + configuration = { + RepositoryName = aws_codecommit_repository.example_repo.repository_name + BranchName = "main" # Replace with your branch name + } + } + } + + stage { + name = "core-infra-dev" + + action { + name = "BuildAction" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + input_artifacts = ["SourceArtifact"] + + configuration = { + ProjectName = module.deploy_dev_core_infra.project_id + } + } + } + + stage { + name = "lb-service-dev" + + action { + name = "BuildAction" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + input_artifacts = ["SourceArtifact"] + + configuration = { + ProjectName = module.deploy_dev_lb_service.project_id + } + } + } + + /* + stage { + name = "ManualApprovalToQA" + + action { + name = "ManualApprovalToQAAction" + category = "Approval" + owner = "AWS" + provider = "Manual" + version = "1" + } + } + + stage { + name = "core-infra-qa" + + action { + name = "BuildAction" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + input_artifacts = ["SourceArtifact"] + + configuration = { + ProjectName = module.deploy_qa_core_infra.project_id + } + } + } + + stage { + name = "lb-service-qa" + + action { + name = "BuildAction" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + input_artifacts = ["SourceArtifact"] + + configuration = { + ProjectName = module.deploy_qa_lb_service.project_id + } + } + } + # You can add more stages for deployment or testing as needed + */ +} diff --git a/terraform/cicd-examples/iac-pipeline/qa-core-infra-deploy-buildspec.yml b/terraform/cicd-examples/iac-pipeline/qa-core-infra-deploy-buildspec.yml new file mode 100644 index 00000000..08cf6ff4 --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/qa-core-infra-deploy-buildspec.yml @@ -0,0 +1,21 @@ +version: 0.2 +env: + variables: + MAJOR: "1" + MINOR: "1" +phases: + install: + commands: + - wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + - sudo apt update && sudo apt install terraform + pre_build: + commands: + - echo "pre_build step" + build: + commands: + - echo "build command" + - STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') + - cd cicd-examples/core-infra-external-state + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=core-infra-qa.tfstate" -backend-config="region=us-west-2" + - terraform apply -var-file=../qa.tfvars -auto-approve diff --git a/terraform/cicd-examples/iac-pipeline/qa-lb-service-deploy-buildspec.yml b/terraform/cicd-examples/iac-pipeline/qa-lb-service-deploy-buildspec.yml new file mode 100644 index 00000000..3d2731e5 --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/qa-lb-service-deploy-buildspec.yml @@ -0,0 +1,21 @@ +version: 0.2 +env: + variables: + MAJOR: "1" + MINOR: "1" +phases: + install: + commands: + - wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + - sudo apt update && sudo apt install terraform + pre_build: + commands: + - echo "pre_build step" + build: + commands: + - echo "build command" + - STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') + - cd cicd-examples/lb-service-external-state + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=lb-service-qa.tfstate" -backend-config="region=us-west-2" + - terraform apply -var-file=../qa.tfvars -auto-approve diff --git a/terraform/cicd-examples/iac-pipeline/variable.tf b/terraform/cicd-examples/iac-pipeline/variable.tf new file mode 100644 index 00000000..2e20b2e9 --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/variable.tf @@ -0,0 +1,5 @@ +variable "s3_bucket" { + type = string + default = "" + description = "s3 bucket" +} diff --git a/terraform/cicd-examples/iac-pipeline/versions.tf b/terraform/cicd-examples/iac-pipeline/versions.tf new file mode 100644 index 00000000..ddfcb0e0 --- /dev/null +++ b/terraform/cicd-examples/iac-pipeline/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/terraform/cicd-examples/lb-service-container-pipeline/README.md b/terraform/cicd-examples/lb-service-container-pipeline/README.md new file mode 100644 index 00000000..e69de29b diff --git a/terraform/cicd-examples/lb-service-container-pipeline/main.tf b/terraform/cicd-examples/lb-service-container-pipeline/main.tf new file mode 100644 index 00000000..6d6de4b9 --- /dev/null +++ b/terraform/cicd-examples/lb-service-container-pipeline/main.tf @@ -0,0 +1,295 @@ +provider "aws" { + region = var.region +} + +################################################################################ +# Parameter Store +################################################################################ + +# # CodeDeploy Application Parameter +# data "aws_ssm_parameter" "codedeploy_app" { +# name = "/codedeploy/app/deploy_development_ecsdemo-frontend" +# } + +# # CodeDeploy Deployment Group Parameter +# data "aws_ssm_parameter" "deployment_group" { +# name = "/codedeploy/deployment-group/deploy_development_ecsdemo-frontend" +# } + +################################################################################ +# ECR and Git Repositories +################################################################################ + +# CodeCommit repository +resource "aws_codecommit_repository" "example_repo" { + repository_name = "ecs_service_repo" +} + +resource "aws_ecr_repository" "example_repo" { + name = "ecs_service_repo" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } +} + +################################################################################ +# CodePipleine IAM role +################################################################################ + +resource "aws_iam_role" "codepipeline_role" { + name = "codepipeline-app-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "codepipeline.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodePipeline_FullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "codecommit_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeCommitFullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "s3_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "codebuild_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "cloudwatch_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +resource "aws_iam_role_policy_attachment" "codedeploy_ecs_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + + +resource "aws_iam_role_policy_attachment" "codedeploy_codepipeline_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployDeployerAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codepipeline_role.name +} + +################################################################################ +# CodeBuild Container Build Permissions +################################################################################ + +resource "aws_iam_role" "codebuild_role" { + name = "codebuild-to-ecr-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "codebuild.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "codebuild_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codebuild_role.name +} + +resource "aws_iam_role_policy_attachment" "ecs_codebuild_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codebuild_role.name +} + +resource "aws_iam_role_policy_attachment" "ecr_codebuild_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess" # Attach a policy that provides necessary permissions + role = aws_iam_role.codebuild_role.name +} + +resource "aws_iam_policy" "codebuild_all_permissions" { + description = "IAM policy for AWS CodeBuild with all necessary permissions" + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:PutParameter", + "ssm:DescribeParameters", + "ssm:ListTagsForResource", + "ssm:DeleteParameter", + "logs:*", + "ec2:*", + "ecs:*", + "iam:PassRole", + "servicediscovery:*", + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "codebuild:*", + "codedeploy:*", + "s3:ListAllMyBuckets", + "s3:GetBucketLocation", + ], + Resource = "*", + }, + ], + }) +} + +resource "aws_iam_role_policy_attachment" "codebuild_all_permissions_attachment" { + policy_arn = aws_iam_policy.codebuild_all_permissions.arn + role = aws_iam_role.codebuild_role.name +} + +################################################################################ +# CodeBuild Module +################################################################################ + +resource "random_id" "this" { + byte_length = 8 +} + +module "codepipeline_s3_bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + version = "~> 3.15" + + bucket_prefix = "codepipeline-${var.region}-" + # acl = "private" + + # For example only - please re-evaluate for your environment + force_destroy = true + + attach_deny_insecure_transport_policy = true + attach_require_latest_tls_policy = true + + server_side_encryption_configuration = { + rule = { + apply_server_side_encryption_by_default = { + sse_algorithm = "AES256" + } + } + } +} + +module "build_container" { + source = "../../modules/codebuild" + + name = "dev-lb-service-codebuild" + service_role = aws_iam_role.codebuild_role.arn + buildspec_path = "./buildspec-cicd.yml" + s3_bucket = module.codepipeline_s3_bucket + + environment = { + image = "aws/codebuild/standard:5.0" + privileged_mode = true + environment_variables = [ + { + name = "REPO_URL" + value = aws_ecr_repository.example_repo.repository_url + } + ] + } + + create_iam_role = true + iam_role_name = "dev-codebuild-${random_id.this.hex}" + ecr_repository = aws_ecr_repository.example_repo.arn +} + +################################################################################ +# CodePipeline +################################################################################ + +resource "aws_codepipeline" "example_applicaiton_pipeline" { + name = "example-applicaiton-pipeline" + role_arn = aws_iam_role.codepipeline_role.arn + + artifact_store { + location = module.codepipeline_s3_bucket.s3_bucket_id + type = "S3" + } + + stage { + name = "Source" + + action { + name = "SourceAction" + category = "Source" + owner = "AWS" + provider = "CodeCommit" + version = "1" + output_artifacts = ["SourceArtifact"] + + configuration = { + RepositoryName = aws_codecommit_repository.example_repo.repository_name + BranchName = "main" # Replace with your branch name + } + } + } + + stage { + name = "build-lb-service" + + action { + name = "BuildAction" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + input_artifacts = ["SourceArtifact"] + output_artifacts = ["BuildArtifact"] + + configuration = { + ProjectName = module.build_container.project_id + } + } + } + + stage { + name = "deploy-lb-service" + + action { + name = "deploy-action" + category = "Deploy" + owner = "AWS" + provider = "CodeDeployToECS" + version = "1" + input_artifacts = ["BuildArtifact"] + + configuration = { + ApplicationName = "deploy_development_ecsdemo-frontend" + DeploymentGroupName = "deployment-group-deploy_development_ecsdemo-frontend" + TaskDefinitionTemplateArtifact = "BuildArtifact" + TaskDefinitionTemplatePath = "development-task-definition.json" + AppSpecTemplateArtifact = "BuildArtifact" + AppSpecTemplatePath = "development-appspec.json.json" + } + } + } + + # You can add more stages for deployment or testing as needed +} diff --git a/terraform/cicd-examples/lb-service-container-pipeline/variable.tf b/terraform/cicd-examples/lb-service-container-pipeline/variable.tf new file mode 100644 index 00000000..236eadae --- /dev/null +++ b/terraform/cicd-examples/lb-service-container-pipeline/variable.tf @@ -0,0 +1,5 @@ +variable "region" { + type = string + default = "us-west-2" + description = "AWS region you want to deploy to." +} diff --git a/terraform/cicd-examples/lb-service-container-pipeline/versions.tf b/terraform/cicd-examples/lb-service-container-pipeline/versions.tf new file mode 100644 index 00000000..6a614f8f --- /dev/null +++ b/terraform/cicd-examples/lb-service-container-pipeline/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + } +} diff --git a/terraform/cicd-examples/lb-service-external-state/README.md b/terraform/cicd-examples/lb-service-external-state/README.md new file mode 100644 index 00000000..c5cff412 --- /dev/null +++ b/terraform/cicd-examples/lb-service-external-state/README.md @@ -0,0 +1,31 @@ +# ECS load-balanced service + +This solution blueprint creates a web-facing load balanced ECS service. There are two steps to deploying this service: + +* Deploy the [core-infra](../core-infra/README.md). Note if you have already deployed the `core-infra` then you can reuse it. +* Deploy this blueprint using the below commands +```shell +terraform init +terraform plan +terraform apply -auto-approve +``` + +

+ +

+ +The solution has following key components: + +* ALB: We are using Application Load Balancer for this service. Note the following key attributes for ALB: + * ALB security group - allows ingress from any IP address to port 80 and allows all egress + * ALB subnet - ALB is created in a public subnet + * Listener - listens on port 80 for protocol HTTP + * Target group - Since we are using Fargate launch type, the targe type is IP since each task in Fargate gets its own ENI and IP address. The target group has container port (3000) and protocol (HTTP) where the application container will serve requests. The ALB runs health check against all registered targets. In this example, ALB send HTTP GET request to path "/" to container port 3000. We are using target group default health check settings. You can tune these settings to adjust the time interval and frequency of health checks. It impacts how fast tasks become available to serve traffic. (See [ALB target health check documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html) to learn more.) +* ECR registery for the container image. We are using only one container image for the task in this example. +* ECS service definition: + * Task security group: allows ingress for TCP from the ALB security group to the container service port (3000 for this example). And allows all egress. + * Service discovery: You can register the service to AWS Cloud Map registry. You just need to provide the `namespace` but make sure the namespace is created in the `core-infra` step. + * Tasks for this service will be deployed in private subnet + * Service definition takes the load balancer target group created above as input. + * Task definition consisting of task vCPU size, task memory, and container information including the above created ECR repository URL. + * Task definition also takes the task execution role ARN which is used by ECS agent to fetch ECR images and send logs to AWS CloudWatch on behalf of the task. diff --git a/terraform/cicd-examples/lb-service-external-state/main.tf b/terraform/cicd-examples/lb-service-external-state/main.tf new file mode 100644 index 00000000..4891fdee --- /dev/null +++ b/terraform/cicd-examples/lb-service-external-state/main.tf @@ -0,0 +1,306 @@ +# STATE_BUCKET=$(aws ssm get-parameters --names terraform_state_bucket | jq -r '.Parameters[0].Value') + +# terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=lb-service-dev.tfstate" -backend-config="region=us-west-2" +# terraform apply -var-file=../dev.tfvars +# terraform destroy -var-file=../dev.tfvars + +# terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="key=lb-service-qa.tfstate" -backend-config="region=us-west-2" +# terraform apply -var-file=../qa.tfvars +# terraform destroy -var-file=../qa.tfvars + +provider "aws" { + region = var.region +} + +# Terraform backend configuration to store state in S3 +terraform { + backend "s3" {} +} + +locals { + name = "ecsdemo-frontend" + + tags = { + Blueprint = local.name + GithubRepo = "github.com/aws-ia/ecs-blueprints" + Environment = var.environment + } +} + +################################################################################ +# ECS Blueprint +################################################################################ + +module "service_alb" { + source = "terraform-aws-modules/alb/aws" + version = "~> 8.3" + + name = "${local.name}-alb" + + load_balancer_type = "application" + + vpc_id = data.aws_vpc.vpc.id + subnets = data.aws_subnets.public.ids + + security_group_rules = { + ingress_all_http = { + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + description = "HTTP web traffic" + cidr_blocks = ["0.0.0.0/0"] + } + egress_all = { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [for s in data.aws_subnet.private_cidr : s.cidr_block] + } + } + + http_tcp_listeners = [ + { + port = "80" + protocol = "HTTP" + target_group_index = 0 + }, + { + port = "8080" + protocol = "HTTP" + target_group_index = 1 + }, + ] + + target_groups = [ + { + + name = "${local.name}-green-tg" + backend_protocol = "HTTP" + backend_port = var.container_port + target_type = "ip" + health_check = { + path = "/" + port = var.container_port + matcher = "200-299" + } + }, + { + name = "${local.name}-blue-tg" + backend_protocol = "HTTP" + backend_port = var.container_port + target_type = "ip" + health_check = { + path = "/" + port = var.container_port + matcher = "200-299" + } + }, + ] + + tags = local.tags +} + +resource "aws_service_discovery_service" "this" { + name = local.name + + dns_config { + namespace_id = data.aws_service_discovery_dns_namespace.this.id + + dns_records { + ttl = 10 + type = "A" + } + + routing_policy = "MULTIVALUE" + } + + health_check_custom_config { + failure_threshold = 1 + } +} + +module "ecs_service_definition" { + source = "terraform-aws-modules/ecs/aws//modules/service" + version = "~> 5.0" + + name = local.name + desired_count = 3 + cluster_arn = data.aws_ecs_cluster.core_infra.arn + enable_autoscaling = false + + subnet_ids = data.aws_subnets.private.ids + security_group_rules = { + ingress_alb_service = { + type = "ingress" + from_port = var.container_port + to_port = var.container_port + protocol = "tcp" + description = "Service port" + source_security_group_id = module.service_alb.security_group_id + } + egress_all = { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + } + + deployment_controller = { + type = "CODE_DEPLOY" + } + + load_balancer = [{ + container_name = var.container_name + container_port = var.container_port + target_group_arn = element(module.service_alb.target_group_arns, 0) + }] + + service_registries = { + registry_arn = aws_service_discovery_service.this.arn + } + + # service_connect_configuration = { + # enabled = false + # } + + # Task Definition + create_iam_role = false + create_task_exec_iam_role = true + #task_exec_iam_role_arn = one(data.aws_iam_roles.ecs_core_infra_exec_role.arns) + enable_execute_command = true + + container_definitions = { + main_container = { + name = var.container_name + image = var.container_image + readonly_root_filesystem = false + + port_mappings = [{ + protocol : "tcp", + containerPort : var.container_port + hostPort : var.container_port + }] + } + } + + ignore_task_definition_changes = false + + tags = local.tags +} + +################################################################################ +# Code Deploy +################################################################################ + +resource "aws_sns_topic" "deployment_notificaitons" { + name = "${var.environment}_${local.name}_deployment_topic" +} + +module "deploy_dev_service" { + source = "../../modules/codedeploy" + name = "deploy_${var.environment}_${local.name}" + ecs_cluster = data.aws_ecs_cluster.core_infra.cluster_name + ecs_service = local.name + sns_topic_arn = aws_sns_topic.deployment_notificaitons.arn + iam_role_name = "deploy_${var.environment}_${local.name}" + create_iam_role = true + service_role = aws_iam_role.codedeploy_service_role.arn + alb_listener = module.service_alb.http_tcp_listener_arns[0] + tg_blue = "${local.name}-green-tg" + tg_green = "${local.name}-blue-tg" +} + +resource "aws_iam_role" "codedeploy_service_role" { + name = "codedeploy-service-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "codedeploy.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs_codedeploy_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS" + role = aws_iam_role.codedeploy_service_role.name +} + +resource "aws_iam_role_policy_attachment" "codedeploy_policy_attachment" { + policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployFullAccess" + role = aws_iam_role.codedeploy_service_role.name +} + +################################################################################ +# Supporting Resources +################################################################################ + +data "aws_vpc" "vpc" { + filter { + name = "tag:Name" + values = ["core-infra-external-state"] + } + + filter { + name = "tag:Environment" + values = [var.environment] + } + +} + +data "aws_subnets" "public" { + filter { + name = "tag:Name" + values = ["core-infra-external-state-public-*"] + } + filter { + name = "tag:Environment" + values = [var.environment] + } +} + +data "aws_subnets" "private" { + filter { + name = "tag:Name" + values = ["core-infra-external-state-private-*"] + } + tags = { + Environment = var.environment + } +} + +data "aws_subnet" "private_cidr" { + for_each = toset(data.aws_subnets.private.ids) + id = each.value + + tags = { + Environment = var.environment + } +} + +data "aws_ecs_cluster" "core_infra" { + tags = { + Environment = var.environment + } + cluster_name = "core-infra-external-state" + +} + +data "aws_service_discovery_dns_namespace" "this" { + name = "default.${data.aws_ecs_cluster.core_infra.cluster_name}.local" + type = "DNS_PRIVATE" + + tags = { + Environment = var.environment + } +} diff --git a/terraform/cicd-examples/lb-service-external-state/outputs.tf b/terraform/cicd-examples/lb-service-external-state/outputs.tf new file mode 100644 index 00000000..8f691494 --- /dev/null +++ b/terraform/cicd-examples/lb-service-external-state/outputs.tf @@ -0,0 +1,4 @@ +output "application_url" { + value = "http://${module.service_alb.lb_dns_name}" + description = "Copy this value in your browser in order to access the deployed app" +} diff --git a/terraform/cicd-examples/lb-service-external-state/variables.tf b/terraform/cicd-examples/lb-service-external-state/variables.tf new file mode 100644 index 00000000..f67281aa --- /dev/null +++ b/terraform/cicd-examples/lb-service-external-state/variables.tf @@ -0,0 +1,29 @@ +variable "region" { + type = string + default = "us-west-2" + description = "AWS region you want to deploy to." +} + +variable "environment" { + type = string + default = "development" + description = "What environment this is associate with." +} + +variable "container_image" { + type = string + default = "public.ecr.aws/docker/library/httpd:latest" + description = "ref to container image" +} + +variable "container_port" { + type = number + default = 80 + description = "container port" +} + +variable "container_name" { + type = string + default = "ecsdemo-frontend" + description = "container name" +} diff --git a/terraform/cicd-examples/lb-service-external-state/versions.tf b/terraform/cicd-examples/lb-service-external-state/versions.tf new file mode 100644 index 00000000..ddfcb0e0 --- /dev/null +++ b/terraform/cicd-examples/lb-service-external-state/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/terraform/cicd-examples/qa.tfvars b/terraform/cicd-examples/qa.tfvars new file mode 100644 index 00000000..37df545c --- /dev/null +++ b/terraform/cicd-examples/qa.tfvars @@ -0,0 +1,2 @@ +region = "us-east-1" +environment = "qa" diff --git a/terraform/modules/codebuild-iac/main.tf b/terraform/modules/codebuild-iac/main.tf new file mode 100644 index 00000000..2a4d49bc --- /dev/null +++ b/terraform/modules/codebuild-iac/main.tf @@ -0,0 +1,104 @@ + + +# IAM role for role for code build +resource "aws_iam_role" "this" { + name = var.iam_role_name + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "codebuild.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_policy" "codebuild_all_permissions" { + description = "IAM policy for AWS CodeBuild with all necessary permissions" + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "cloudwatch:PutMetricData", + "codebuild:*", + "codedeploy:*", + "codepipeline:*", + "ec2:*", + "ecs:*", + "elasticloadbalancing:*", + "iam:AttachRolePolicy", + "iam:CreatePolicy", + "iam:CreateRole", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListGroupPolicies", + "iam:ListGroups", + "iam:ListInstanceProfiles", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoles", + "iam:PassRole", + "iam:PutRolePolicy", + "iam:TagRole", + "iam:TagPolicy", + "logs:*", + "route53:*", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListAllMyBuckets", + "s3:ListBucket", + "s3:PutObject", + "servicediscovery:*", + "sns:*", + "ssm:DeleteParameter", + "ssm:DescribeParameters", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:ListTagsForResource", + "ssm:PutParameter" + ], + Resource = "*", + }, + ], + }) +} + +resource "aws_iam_role_policy_attachment" "codebuild_all_permissions_attachment" { + policy_arn = aws_iam_policy.codebuild_all_permissions.arn + role = aws_iam_role.this.name +} + +# CodeBuild project +resource "aws_codebuild_project" "this" { + name = var.name + service_role = aws_iam_role.this.arn + + artifacts { + type = "CODEPIPELINE" + } + + environment { + compute_type = "BUILD_GENERAL1_SMALL" + image = "aws/codebuild/standard:5.0" + type = "LINUX_CONTAINER" + } + + source { + type = "CODEPIPELINE" + buildspec = file(var.buildspec_path) + } + +} diff --git a/terraform/modules/codebuild-iac/outputs.tf b/terraform/modules/codebuild-iac/outputs.tf new file mode 100644 index 00000000..af74ac3b --- /dev/null +++ b/terraform/modules/codebuild-iac/outputs.tf @@ -0,0 +1,4 @@ +output "project_id" { + description = "value" + value = aws_codebuild_project.this.id +} diff --git a/terraform/modules/codebuild-iac/variables.tf b/terraform/modules/codebuild-iac/variables.tf new file mode 100644 index 00000000..e5652172 --- /dev/null +++ b/terraform/modules/codebuild-iac/variables.tf @@ -0,0 +1,17 @@ +variable "iam_role_name" { + default = "" + description = "value" + type = string +} + +variable "buildspec_path" { + default = "" + description = "value" + type = string +} + +variable "name" { + default = "" + description = "value" + type = string +} diff --git a/terraform/modules/codebuild-iac/versions.tf b/terraform/modules/codebuild-iac/versions.tf new file mode 100644 index 00000000..bbe958c7 --- /dev/null +++ b/terraform/modules/codebuild-iac/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.72.0" + } + } +}