├── .terraform-version ├── backend.tf ├── eip.tf ├── provider.tf ├── versions.tf ├── ecr.tf ├── clouwdwatch.tf ├── files ├── appspec.yml ├── buildspec.yml └── task_definition.json ├── .github └── workflows │ ├── fmt.yml │ └── tfsec.yml ├── rds_parameter_group.tf ├── iam_role_ecs_task.tf ├── iam_role_rds.tf ├── iam_role_codedeploy.tf ├── kms.tf ├── event_bridge.tf ├── .gitignore ├── acm.tf ├── ssm.tf ├── iam_role_ecr_event.tf ├── LICENSE ├── .terraform.lock.hcl ├── iam_role_ecs.tf ├── s3_codepipeline.tf ├── waf.tf ├── subnet.tf ├── codedeploy.tf ├── codebuild.tf ├── rds.tf ├── locales.tf ├── alb.tf ├── iam_role_codepipeline.tf ├── vpc.tf ├── route53.tf ├── s3_build.tf ├── codepipeline.tf ├── ecs.tf ├── iam_user_actions.tf ├── cloudfront.tf ├── route_table.tf ├── s3_logs.tf ├── iam_role_codebuild.tf ├── security_group.tf ├── README.ja.md └── README.md /.terraform-version: -------------------------------------------------------------------------------- 1 | 0.14.9 2 | -------------------------------------------------------------------------------- /backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "local" {} 3 | } 4 | -------------------------------------------------------------------------------- /eip.tf: -------------------------------------------------------------------------------- 1 | resource "aws_eip" "nat" { 2 | for_each = local.availability_zones 3 | 4 | vpc = true 5 | } 6 | -------------------------------------------------------------------------------- /provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "ap-northeast-1" 3 | } 4 | 5 | provider "aws" { 6 | alias = "useast1" 7 | region = "us-east-1" 8 | } 9 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | version = "= 3.35.0" 5 | source = "hashicorp/aws" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "rails" { 2 | name = "${local.name}-rails" 3 | 4 | image_scanning_configuration { 5 | scan_on_push = true 6 | } 7 | 8 | encryption_configuration { 9 | encryption_type = "AES256" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /clouwdwatch.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "app" { 2 | name = "/aws/ecs/${local.name}-app" 3 | retention_in_days = 1 4 | } 5 | 6 | resource "aws_cloudwatch_log_group" "codebuild" { 7 | name = "/aws/codebuild/${local.name}-build" 8 | retention_in_days = 1 9 | } 10 | -------------------------------------------------------------------------------- /files/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | Resources: 3 | - TargetService: 4 | Type: AWS::ECS::Service 5 | Properties: 6 | TaskDefinition: 7 | LoadBalancerInfo: 8 | ContainerName: "web" 9 | ContainerPort: 3000 10 | PlatformVersion: "LATEST" 11 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | name: fmt 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | fmt: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: hashicorp/setup-terraform@v1 19 | with: 20 | terraform_version: 0.14.9 21 | - run: terraform fmt -check 22 | -------------------------------------------------------------------------------- /rds_parameter_group.tf: -------------------------------------------------------------------------------- 1 | resource "aws_rds_cluster_parameter_group" "main" { 2 | name = "${local.name}-cluster-main" 3 | description = "Cluster Parameter Group" 4 | family = "aurora-postgresql12" 5 | 6 | # TODO: configure yourself 7 | } 8 | 9 | resource "aws_db_parameter_group" "main" { 10 | name = "${local.name}-main" 11 | description = "Instance Parameter Group" 12 | family = "aurora-postgresql12" 13 | 14 | # TODO: configure yourself 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/tfsec.yml: -------------------------------------------------------------------------------- 1 | name: tfsec 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | tfsec: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Terraform security scan 19 | uses: triat/terraform-security-scan@v2.2.1 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /iam_role_ecs_task.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "ecs_task" { 2 | name = "${local.name}-ecs-task" 3 | assume_role_policy = data.aws_iam_policy_document.ecs_task_assume.json 4 | } 5 | 6 | data "aws_iam_policy_document" "ecs_task_assume" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["ecs-tasks.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | # NOTE: Now, Policy is empty. 17 | # Add a policy to this role to access S3 from rails apps. 18 | -------------------------------------------------------------------------------- /iam_role_rds.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "rds" { 2 | name = "${local.name}-rds" 3 | assume_role_policy = data.aws_iam_policy_document.rds_assume.json 4 | } 5 | 6 | data "aws_iam_policy_document" "rds_assume" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["monitoring.rds.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | resource "aws_iam_role_policy_attachment" "rds_monitoring" { 17 | role = aws_iam_role.rds.name 18 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" 19 | } 20 | -------------------------------------------------------------------------------- /iam_role_codedeploy.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "codedeploy" { 2 | name = "${local.name}-codedeploy" 3 | assume_role_policy = data.aws_iam_policy_document.codedeploy_assume.json 4 | } 5 | 6 | data "aws_iam_policy_document" "codedeploy_assume" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["codedeploy.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | resource "aws_iam_role_policy_attachment" "codedeploy" { 17 | policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS" 18 | role = aws_iam_role.codedeploy.name 19 | } 20 | -------------------------------------------------------------------------------- /kms.tf: -------------------------------------------------------------------------------- 1 | resource "aws_kms_key" "terraform" { 2 | enable_key_rotation = true 3 | } 4 | 5 | resource "aws_kms_alias" "terraform" { 6 | name = "alias/terraform" 7 | target_key_id = aws_kms_key.terraform.key_id 8 | } 9 | 10 | data "aws_kms_secrets" "secrets" { 11 | dynamic "secret" { 12 | for_each = local.secrets 13 | 14 | content { 15 | name = secret.key 16 | payload = secret.value 17 | } 18 | } 19 | } 20 | 21 | resource "aws_kms_key" "rds" { 22 | enable_key_rotation = true 23 | } 24 | 25 | resource "aws_kms_alias" "rds" { 26 | name = "alias/rds" 27 | target_key_id = aws_kms_key.rds.key_id 28 | } 29 | -------------------------------------------------------------------------------- /event_bridge.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_event_rule" "ecr" { 2 | name = "${local.name}-ecr-push" 3 | description = "Push ECR to start CodePipeline." 4 | 5 | event_pattern = jsonencode({ 6 | source = ["aws.ecr"] 7 | detail-type = ["ECR Image Action"] 8 | detail = { 9 | action-type = ["PUSH"] 10 | result = ["SUCCESS"] 11 | repository-name = [aws_ecr_repository.rails.name] 12 | image-tag = ["latest"] 13 | } 14 | }) 15 | } 16 | 17 | resource "aws_cloudwatch_event_target" "ecr_codepipeline" { 18 | rule = aws_cloudwatch_event_rule.ecr.id 19 | arn = aws_codepipeline.deploy.arn 20 | role_arn = aws_iam_role.ecr_event.arn 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | -------------------------------------------------------------------------------- /files/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | parameter-store: 5 | RAILS_MASTER_KEY: ${ssm_rails_master_key_name} 6 | DATABASE_URL: ${ssm_database_url_name} 7 | 8 | phases: 9 | pre_build: 10 | commands: 11 | - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin ${account_id}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com 12 | build: 13 | commands: 14 | - IMAGE=$(cat imageDetail.json | jq -r '.ImageURI') 15 | 16 | # NOTE: execute db:migrate 17 | - docker run --rm -e RAILS_MASTER_KEY -e DATABASE_URL $IMAGE rails db:migrate 18 | 19 | - aws s3 cp s3://${bucket}/${task_definition_key} ./task_definition.json 20 | - aws s3 cp s3://${bucket}/${appspec_key} ./appspec.yml 21 | 22 | artifacts: 23 | files: 24 | - task_definition.json 25 | - appspec.yml 26 | -------------------------------------------------------------------------------- /acm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "main" { 2 | domain_name = local.domain 3 | subject_alternative_names = ["*.${local.domain}"] 4 | validation_method = "DNS" 5 | } 6 | 7 | resource "aws_acm_certificate_validation" "main" { 8 | certificate_arn = aws_acm_certificate.main.arn 9 | validation_record_fqdns = [for record in aws_route53_record.acm : record.fqdn] 10 | } 11 | 12 | resource "aws_acm_certificate" "cloudfront" { 13 | provider = aws.useast1 14 | 15 | domain_name = local.domain 16 | subject_alternative_names = ["*.${local.domain}"] 17 | validation_method = "DNS" 18 | } 19 | 20 | resource "aws_acm_certificate_validation" "cloudfront" { 21 | provider = aws.useast1 22 | 23 | certificate_arn = aws_acm_certificate.cloudfront.arn 24 | validation_record_fqdns = [for record in aws_route53_record.acm : record.fqdn] 25 | } 26 | -------------------------------------------------------------------------------- /ssm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "rails_master_key" { 2 | name = "/${local.name}/rails_master_key" 3 | description = "Rails Master Key." 4 | type = "SecureString" 5 | value = data.aws_kms_secrets.secrets.plaintext["rails_master_key"] 6 | } 7 | 8 | resource "aws_ssm_parameter" "database_url" { 9 | name = "/${local.name}/database_url" 10 | description = "DATABASE_URL" 11 | type = "SecureString" 12 | value = "postgres://myuser:${data.aws_kms_secrets.secrets.plaintext["db_password"]}@${aws_rds_cluster.main.endpoint}:5432/mydb" 13 | } 14 | 15 | resource "aws_ssm_parameter" "reader_database_url" { 16 | name = "/${local.name}/reader_database_url" 17 | description = "READER_DATABASE_URL" 18 | type = "SecureString" 19 | value = "postgres://myuser:${data.aws_kms_secrets.secrets.plaintext["db_password"]}@${aws_rds_cluster.main.reader_endpoint}:5432/mydb" 20 | } 21 | -------------------------------------------------------------------------------- /iam_role_ecr_event.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "ecr_event" { 2 | name = "${local.name}-ecr-event" 3 | assume_role_policy = data.aws_iam_policy_document.ecr_event_assume.json 4 | } 5 | 6 | data "aws_iam_policy_document" "ecr_event_assume" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["events.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | resource "aws_iam_policy" "ecr_event" { 17 | name = "${local.name}-ecr-event" 18 | description = "For ECS Execution Task policy." 19 | policy = data.aws_iam_policy_document.ecr_event.json 20 | } 21 | 22 | data "aws_iam_policy_document" "ecr_event" { 23 | statement { 24 | actions = [ 25 | "codepipeline:StartPipelineExecution", 26 | ] 27 | resources = [ 28 | aws_codepipeline.deploy.arn, 29 | ] 30 | } 31 | } 32 | 33 | resource "aws_iam_role_policy_attachment" "ecr_event" { 34 | role = aws_iam_role.ecr_event.name 35 | policy_arn = aws_iam_policy.ecr_event.arn 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 reireias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "3.35.0" 6 | constraints = "3.35.0" 7 | hashes = [ 8 | "h1:btK0kEAhczdkGY1qWGSEs5840TlyefTEekQJB9ctwJ8=", 9 | "zh:0b7cf15369fe940190f2e3fd77300119a16a9b821a7b15e049a6e349126b833d", 10 | "zh:65680b35a45df6dc9ebe4439aa28dbe5767f8745443d0807656759d81ed23f5d", 11 | "zh:75a71517d40b842308bc7a13a8dadcade99de3344292318b28a3ad95f09994e7", 12 | "zh:865e618aa41f4a3842e818f6389f4aac89a985b409d1bb108ac753ea6292215d", 13 | "zh:893658f93e57ea6c2bb458cdcf7d51e617147f53a9b2ed7741689bec3cfca4f9", 14 | "zh:89d63eab0ad7fe3a891e6567052dd3227520ae48c212465d3a2b0bed326b319d", 15 | "zh:94a408adcc52ce1758ecc2aad8394c35254029d18f358392b86602705a688b3d", 16 | "zh:a82d09d03cb5480b3b4f318d1db3a64d80a701a429f3a84520e4a26f66b9a178", 17 | "zh:aee92e745c43de2cc30913bddd8e625a8c6511f900f8e4104a0ccb746f276428", 18 | "zh:b1178f4c2db30d7e49c3af441f79ac932bb5b8a8f05b0d770515b732ad4ac388", 19 | "zh:b2684d0124ed6c394c83005def7387a6f8c9ac5f459dd7fc2fe56ea1aa97b20c", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /iam_role_ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "ecs" { 2 | name = "${local.name}-ecs" 3 | assume_role_policy = data.aws_iam_policy_document.ecs_assume.json 4 | } 5 | 6 | data "aws_iam_policy_document" "ecs_assume" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["ecs-tasks.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | resource "aws_iam_policy" "ecs" { 17 | name = "${local.name}-ecs" 18 | description = "For ECS Execution Task policy." 19 | policy = data.aws_iam_policy_document.ecs.json 20 | } 21 | 22 | data "aws_iam_policy_document" "ecs" { 23 | # NOTE: For get System Manager Prameter Store 24 | statement { 25 | actions = [ 26 | "ssm:GetParameters", 27 | ] 28 | resources = [ 29 | aws_ssm_parameter.rails_master_key.arn, 30 | aws_ssm_parameter.database_url.arn, 31 | aws_ssm_parameter.reader_database_url.arn, 32 | ] 33 | } 34 | } 35 | 36 | resource "aws_iam_role_policy_attachment" "ecs" { 37 | role = aws_iam_role.ecs.name 38 | policy_arn = aws_iam_policy.ecs.arn 39 | } 40 | 41 | resource "aws_iam_role_policy_attachment" "ecs_basic" { 42 | role = aws_iam_role.ecs.name 43 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 44 | } 45 | -------------------------------------------------------------------------------- /s3_codepipeline.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "codepipeline" { 2 | bucket = "${local.name}.codepipeline" 3 | 4 | server_side_encryption_configuration { 5 | rule { 6 | apply_server_side_encryption_by_default { 7 | sse_algorithm = "AES256" 8 | } 9 | } 10 | } 11 | 12 | versioning { 13 | enabled = true 14 | } 15 | 16 | logging { 17 | target_bucket = aws_s3_bucket.logs.id 18 | target_prefix = "s3/${local.name}.codepipeline/" 19 | } 20 | 21 | force_destroy = true 22 | } 23 | 24 | resource "aws_s3_bucket_policy" "codepipeline" { 25 | bucket = aws_s3_bucket.codepipeline.id 26 | policy = data.aws_iam_policy_document.codepipeline_bucket.json 27 | } 28 | 29 | data "aws_iam_policy_document" "codepipeline_bucket" { 30 | # see: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-5 31 | statement { 32 | sid = "AllowSSLRequestsOnly" 33 | effect = "Deny" 34 | principals { 35 | type = "*" 36 | identifiers = ["*"] 37 | } 38 | actions = ["s3:*"] 39 | resources = [ 40 | aws_s3_bucket.codepipeline.arn, 41 | "${aws_s3_bucket.codepipeline.arn}/*", 42 | ] 43 | condition { 44 | test = "Bool" 45 | variable = "aws:SecureTransport" 46 | values = ["false"] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /waf.tf: -------------------------------------------------------------------------------- 1 | resource "aws_wafv2_web_acl" "cloudfront" { 2 | provider = aws.useast1 3 | 4 | name = "${local.name}-cloudfront" 5 | description = "WAF for CloudFront." 6 | scope = "CLOUDFRONT" 7 | 8 | default_action { 9 | allow {} 10 | } 11 | 12 | rule { 13 | name = "crs" 14 | priority = 1 15 | 16 | override_action { 17 | none {} 18 | } 19 | 20 | statement { 21 | managed_rule_group_statement { 22 | name = "AWSManagedRulesCommonRuleSet" 23 | vendor_name = "AWS" 24 | } 25 | } 26 | 27 | visibility_config { 28 | cloudwatch_metrics_enabled = true 29 | metric_name = "crs" 30 | sampled_requests_enabled = true 31 | } 32 | } 33 | 34 | rule { 35 | name = "sqli" 36 | priority = 2 37 | 38 | override_action { 39 | none {} 40 | } 41 | 42 | statement { 43 | managed_rule_group_statement { 44 | name = "AWSManagedRulesSQLiRuleSet" 45 | vendor_name = "AWS" 46 | } 47 | } 48 | 49 | visibility_config { 50 | cloudwatch_metrics_enabled = true 51 | metric_name = "sqli" 52 | sampled_requests_enabled = true 53 | } 54 | } 55 | 56 | visibility_config { 57 | cloudwatch_metrics_enabled = true 58 | metric_name = "cloudfront" 59 | sampled_requests_enabled = true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /files/task_definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "${family}", 3 | "taskRoleArn": "${task_role_arn}", 4 | "executionRoleArn": "${execution_role_arn}", 5 | "networkMode": "awsvpc", 6 | "requiresCompatibilities": ["FARGATE"], 7 | "cpu": "256", 8 | "memory": "512", 9 | "containerDefinitions": [ 10 | { 11 | "name": "web", 12 | "image": "", 13 | "portMappings": [ 14 | { 15 | "hostPort": 3000, 16 | "containerPort": 3000 17 | } 18 | ], 19 | "logConfiguration": { 20 | "logDriver": "awslogs", 21 | "options": { 22 | "awslogs-group": "${log_group}", 23 | "awslogs-region": "${region}", 24 | "awslogs-stream-prefix": "web" 25 | } 26 | }, 27 | "environment": [ 28 | { 29 | "name": "RAILS_LOG_TO_STDOUT", 30 | "value": "1" 31 | }, 32 | { 33 | "name": "RAILS_SERVE_STATIC_FILES", 34 | "value": "1" 35 | } 36 | ], 37 | "secrets": [ 38 | { 39 | "name": "RAILS_MASTER_KEY", 40 | "valueFrom": "${ssm_rails_master_key_name}" 41 | }, 42 | { 43 | "name": "DATABASE_URL", 44 | "valueFrom": "${ssm_database_url_name}" 45 | }, 46 | { 47 | "name": "READER_DATABASE_URL", 48 | "valueFrom": "${ssm_reader_database_url_name}" 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /subnet.tf: -------------------------------------------------------------------------------- 1 | resource "aws_subnet" "public" { 2 | for_each = local.availability_zones 3 | 4 | vpc_id = aws_vpc.main.id 5 | availability_zone = each.key 6 | cidr_block = cidrsubnet(local.vpc_cidr, 8, local.az_conf[each.key].index) 7 | 8 | tags = { 9 | Name = "${local.name}-public-${local.az_conf[each.key].short_name}" 10 | } 11 | } 12 | 13 | resource "aws_subnet" "ecs" { 14 | for_each = local.availability_zones 15 | 16 | vpc_id = aws_vpc.main.id 17 | availability_zone = each.key 18 | cidr_block = cidrsubnet(local.vpc_cidr, 8, 10 + local.az_conf[each.key].index) 19 | 20 | tags = { 21 | Name = "${local.name}-ecs-${local.az_conf[each.key].short_name}" 22 | } 23 | } 24 | 25 | resource "aws_subnet" "rds" { 26 | for_each = local.availability_zones 27 | 28 | vpc_id = aws_vpc.main.id 29 | availability_zone = each.key 30 | cidr_block = cidrsubnet(local.vpc_cidr, 8, 20 + local.az_conf[each.key].index) 31 | 32 | tags = { 33 | Name = "${local.name}-rds-${local.az_conf[each.key].short_name}" 34 | } 35 | } 36 | 37 | resource "aws_subnet" "codebuild" { 38 | for_each = local.availability_zones 39 | 40 | vpc_id = aws_vpc.main.id 41 | availability_zone = each.key 42 | cidr_block = cidrsubnet(local.vpc_cidr, 8, 40 + local.az_conf[each.key].index) 43 | 44 | tags = { 45 | Name = "${local.name}-codebuild-${local.az_conf[each.key].short_name}" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /codedeploy.tf: -------------------------------------------------------------------------------- 1 | resource "aws_codedeploy_app" "app" { 2 | compute_platform = "ECS" 3 | name = "${local.name}-app" 4 | } 5 | 6 | resource "aws_codedeploy_deployment_group" "app" { 7 | app_name = aws_codedeploy_app.app.name 8 | deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" 9 | deployment_group_name = "${local.name}-app" 10 | service_role_arn = aws_iam_role.codedeploy.arn 11 | 12 | auto_rollback_configuration { 13 | enabled = true 14 | events = ["DEPLOYMENT_FAILURE"] 15 | } 16 | 17 | blue_green_deployment_config { 18 | deployment_ready_option { 19 | action_on_timeout = "CONTINUE_DEPLOYMENT" 20 | } 21 | 22 | terminate_blue_instances_on_deployment_success { 23 | action = "TERMINATE" 24 | termination_wait_time_in_minutes = 1 25 | } 26 | } 27 | 28 | deployment_style { 29 | deployment_option = "WITH_TRAFFIC_CONTROL" 30 | deployment_type = "BLUE_GREEN" 31 | } 32 | 33 | ecs_service { 34 | cluster_name = aws_ecs_cluster.main.name 35 | service_name = aws_ecs_service.app.name 36 | } 37 | 38 | load_balancer_info { 39 | target_group_pair_info { 40 | prod_traffic_route { 41 | listener_arns = [aws_lb_listener.app.arn] 42 | } 43 | 44 | target_group { 45 | name = aws_lb_target_group.app["blue"].name 46 | } 47 | 48 | target_group { 49 | name = aws_lb_target_group.app["green"].name 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /codebuild.tf: -------------------------------------------------------------------------------- 1 | resource "aws_codebuild_project" "build" { 2 | name = "${local.name}-build" 3 | description = "Create TaskDefinition and run db:migrate" 4 | build_timeout = 10 5 | service_role = aws_iam_role.codebuild.arn 6 | 7 | artifacts { 8 | type = "CODEPIPELINE" 9 | } 10 | 11 | cache { 12 | type = "LOCAL" 13 | modes = ["LOCAL_DOCKER_LAYER_CACHE", "LOCAL_SOURCE_CACHE"] 14 | } 15 | 16 | environment { 17 | compute_type = "BUILD_GENERAL1_MEDIUM" 18 | image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0" 19 | type = "LINUX_CONTAINER" 20 | privileged_mode = true 21 | } 22 | 23 | logs_config { 24 | cloudwatch_logs { 25 | group_name = aws_cloudwatch_log_group.codebuild.name 26 | } 27 | } 28 | 29 | source { 30 | type = "CODEPIPELINE" 31 | buildspec = templatefile("files/buildspec.yml", { 32 | ssm_rails_master_key_name = aws_ssm_parameter.rails_master_key.name 33 | ssm_database_url_name = aws_ssm_parameter.database_url.name 34 | account_id = local.account_id 35 | bucket = aws_s3_bucket.build.bucket 36 | task_definition_key = aws_s3_bucket_object.task_definition.key 37 | appspec_key = aws_s3_bucket_object.appspec.key 38 | }) 39 | } 40 | 41 | vpc_config { 42 | vpc_id = aws_vpc.main.id 43 | security_group_ids = [aws_security_group.codebuild.id] 44 | subnets = values(aws_subnet.codebuild)[*].id 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rds.tf: -------------------------------------------------------------------------------- 1 | resource "aws_rds_cluster" "main" { 2 | cluster_identifier = "${local.name}-cluster" 3 | engine = "aurora-postgresql" 4 | engine_version = "12.4" 5 | availability_zones = local.availability_zones 6 | database_name = "mydb" 7 | master_username = "myuser" 8 | master_password = data.aws_kms_secrets.secrets.plaintext["db_password"] 9 | backup_retention_period = 14 10 | preferred_backup_window = "18:00-19:00" 11 | storage_encrypted = true 12 | kms_key_id = aws_kms_key.rds.arn 13 | db_subnet_group_name = aws_db_subnet_group.main.id 14 | vpc_security_group_ids = [aws_security_group.rds.id] 15 | deletion_protection = true 16 | skip_final_snapshot = false 17 | final_snapshot_identifier = "${local.name}-cluster-final" 18 | 19 | db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main.id 20 | } 21 | 22 | resource "aws_rds_cluster_instance" "main" { 23 | count = 2 24 | 25 | identifier = "${local.name}-${count.index}" 26 | cluster_identifier = aws_rds_cluster.main.id 27 | instance_class = "db.t3.medium" 28 | engine = aws_rds_cluster.main.engine 29 | engine_version = aws_rds_cluster.main.engine_version 30 | db_subnet_group_name = aws_db_subnet_group.main.id 31 | db_parameter_group_name = aws_db_parameter_group.main.id 32 | monitoring_role_arn = aws_iam_role.rds.arn 33 | monitoring_interval = 60 34 | } 35 | 36 | resource "aws_db_subnet_group" "main" { 37 | name = "${local.name}-main" 38 | subnet_ids = values(aws_subnet.rds)[*].id 39 | } 40 | -------------------------------------------------------------------------------- /locales.tf: -------------------------------------------------------------------------------- 1 | data "aws_availability_zones" "available" { 2 | state = "available" 3 | } 4 | 5 | data "aws_caller_identity" "current" {} 6 | data "aws_region" "current" {} 7 | data "aws_elb_service_account" "main" {} 8 | data "aws_canonical_user_id" "current" {} 9 | 10 | locals { 11 | account_id = data.aws_caller_identity.current.account_id 12 | region = data.aws_region.current.name 13 | 14 | name = "reireias2021" 15 | domain = "reireias.link" 16 | 17 | availability_zones = toset(data.aws_availability_zones.available.names) 18 | 19 | az_conf = { 20 | "ap-northeast-1a" = { 21 | index = 1 22 | short_name = "1a" 23 | } 24 | "ap-northeast-1c" = { 25 | index = 2 26 | short_name = "1c" 27 | } 28 | "ap-northeast-1d" = { 29 | index = 3 30 | short_name = "1d" 31 | } 32 | } 33 | 34 | vpc_cidr = "10.0.0.0/16" 35 | 36 | secrets = { 37 | cloudfront_shared_key = "AQICAHhxcOHhVzL2EwWj90RRTdMZ0MYnfo0ER2g2xA6XxvsuMQFbfQt7T9DKrNezw9nVZrkYAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMDGyuL/ERm3fGHkXHAgEQgDvYMEXLBcD0MiZdANgk4a7pv2DUsG/J0dK8yfr20C/thZktjvrmtVnWj9dGh75/0mvbdVwh+lBhg/7zYQ==" 38 | rails_master_key = "AQICAHhxcOHhVzL2EwWj90RRTdMZ0MYnfo0ER2g2xA6XxvsuMQG7TKaXX+Cl/gDQaU1KN1tJAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMXCj237k7rHqYDLqTAgEQgDvttnqzO/W7uotcXvenQpTsPDyqNExxRKvvdnCozhyhwlo+dfdfsY18PJSqabsdBvZR+llfrKJq/avWzQ==" 39 | db_password = "AQICAHhxcOHhVzL2EwWj90RRTdMZ0MYnfo0ER2g2xA6XxvsuMQFB0W6mUq7CGTRpuJRU1JtdAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMtgf9ZvOZseWDcZlFAgEQgDtFlv+fobkyI4t3Jm4cqfvgpKumuCcT3Ep/UcjdJmUOIvhUKxILJA7Uwg9Z3MDY2HoGmrQuLaM18n4uAQ==" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /alb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "app" { 2 | name = "${local.name}-app" 3 | # NOTE: allow from internet 4 | # tfsec:ignore:AWS005 5 | internal = false 6 | subnets = values(aws_subnet.public)[*].id 7 | security_groups = [aws_security_group.alb.id] 8 | 9 | enable_deletion_protection = false 10 | 11 | access_logs { 12 | bucket = aws_s3_bucket.logs.bucket 13 | enabled = true 14 | } 15 | } 16 | 17 | resource "aws_lb_listener" "app" { 18 | load_balancer_arn = aws_lb.app.arn 19 | port = "443" 20 | protocol = "HTTPS" 21 | ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" 22 | certificate_arn = aws_acm_certificate.main.arn 23 | 24 | # NOTE: Return 404 to all not CloudFront requests. 25 | default_action { 26 | type = "fixed-response" 27 | 28 | fixed_response { 29 | content_type = "text/plain" 30 | status_code = "404" 31 | } 32 | } 33 | } 34 | 35 | resource "aws_lb_listener_rule" "app_from_cloudfront" { 36 | listener_arn = aws_lb_listener.app.arn 37 | priority = 100 38 | 39 | action { 40 | type = "forward" 41 | target_group_arn = aws_lb_target_group.app["blue"].arn 42 | } 43 | 44 | condition { 45 | http_header { 46 | http_header_name = "x-pre-shared-key" 47 | values = [data.aws_kms_secrets.secrets.plaintext["cloudfront_shared_key"]] 48 | } 49 | } 50 | 51 | # NOTE: Ignore target group switch 52 | lifecycle { 53 | ignore_changes = [action] 54 | } 55 | } 56 | 57 | resource "aws_lb_target_group" "app" { 58 | for_each = toset(["blue", "green"]) 59 | 60 | name = "${local.name}-app-${each.key}" 61 | port = 3000 62 | protocol = "HTTP" 63 | target_type = "ip" 64 | vpc_id = aws_vpc.main.id 65 | 66 | health_check { 67 | path = "/health_checks" 68 | matcher = "200-299" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /iam_role_codepipeline.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "codepipeline" { 2 | name = "${local.name}-codepipeline" 3 | assume_role_policy = data.aws_iam_policy_document.codepipeline_assume.json 4 | } 5 | 6 | data "aws_iam_policy_document" "codepipeline_assume" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["codepipeline.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | resource "aws_iam_policy" "codepipeline" { 17 | name = "${local.name}-codepipeline" 18 | description = "For CodeBuild policy." 19 | policy = data.aws_iam_policy_document.codepipeline.json 20 | } 21 | 22 | data "aws_iam_policy_document" "codepipeline" { 23 | statement { 24 | # NOTE: I want to narrow down the permissions, but the build doesn't succeed. 25 | actions = [ 26 | "s3:*", 27 | ] 28 | resources = [ 29 | aws_s3_bucket.codepipeline.arn, 30 | "${aws_s3_bucket.codepipeline.arn}/*", 31 | ] 32 | } 33 | 34 | statement { 35 | actions = [ 36 | "ecr:DescribeImages" 37 | ] 38 | resources = ["*"] 39 | } 40 | 41 | statement { 42 | actions = [ 43 | "codebuild:BatchGetBuilds", 44 | "codebuild:StartBuild", 45 | ] 46 | resources = ["*"] 47 | } 48 | 49 | # NOTE: For CodeDeployToECS 50 | statement { 51 | actions = [ 52 | "codedeploy:CreateDeployment", 53 | "codedeploy:GetDeployment", 54 | "codedeploy:GetApplication", 55 | "codedeploy:GetApplicationRevision", 56 | "codedeploy:RegisterApplicationRevision", 57 | "codedeploy:GetDeploymentConfig", 58 | "ecs:RegisterTaskDefinition", 59 | "iam:PassRole", 60 | ] 61 | resources = ["*"] 62 | } 63 | } 64 | 65 | resource "aws_iam_role_policy_attachment" "codepipeline" { 66 | role = aws_iam_role.codepipeline.name 67 | policy_arn = aws_iam_policy.codepipeline.arn 68 | } 69 | -------------------------------------------------------------------------------- /vpc.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "main" { 2 | cidr_block = local.vpc_cidr 3 | 4 | tags = { 5 | Name = "${local.name}-main" 6 | } 7 | } 8 | 9 | resource "aws_flow_log" "main" { 10 | log_destination = aws_s3_bucket.logs.arn 11 | log_destination_type = "s3" 12 | traffic_type = "ALL" 13 | vpc_id = aws_vpc.main.id 14 | 15 | # NOTE: Wait creating a policy so that logs can be written. 16 | depends_on = [aws_s3_bucket_policy.logs] 17 | } 18 | 19 | # NOTE: region default VPC 20 | resource "aws_default_vpc" "default" {} 21 | 22 | resource "aws_internet_gateway" "igw" { 23 | vpc_id = aws_vpc.main.id 24 | 25 | tags = { 26 | Name = "${local.name}-igw" 27 | } 28 | } 29 | 30 | resource "aws_nat_gateway" "nat" { 31 | for_each = local.availability_zones 32 | 33 | allocation_id = aws_eip.nat[each.key].id 34 | subnet_id = aws_subnet.public[each.key].id 35 | 36 | tags = { 37 | Name = "${local.name}-nat-${local.az_conf[each.key].short_name}" 38 | } 39 | 40 | depends_on = [aws_internet_gateway.igw] 41 | } 42 | 43 | resource "aws_vpc_endpoint" "s3" { 44 | vpc_id = aws_vpc.main.id 45 | service_name = data.aws_vpc_endpoint_service.s3.service_name 46 | vpc_endpoint_type = "Gateway" 47 | 48 | tags = { 49 | Name = "${local.name}-s3" 50 | } 51 | } 52 | 53 | data "aws_vpc_endpoint_service" "s3" { 54 | service = "s3" 55 | service_type = "Gateway" 56 | } 57 | 58 | resource "aws_vpc_endpoint" "dkr" { 59 | vpc_id = aws_vpc.main.id 60 | service_name = data.aws_vpc_endpoint_service.dkr.service_name 61 | vpc_endpoint_type = "Interface" 62 | security_group_ids = [aws_security_group.vpce_dkr.id] 63 | subnet_ids = concat(values(aws_subnet.ecs)[*].id, values(aws_subnet.codebuild)[*].id) 64 | 65 | tags = { 66 | Name = "${local.name}-dkr" 67 | } 68 | } 69 | 70 | data "aws_vpc_endpoint_service" "dkr" { 71 | service = "ecr.dkr" 72 | } 73 | -------------------------------------------------------------------------------- /route53.tf: -------------------------------------------------------------------------------- 1 | data "aws_route53_zone" "main" { 2 | name = local.domain 3 | private_zone = false 4 | } 5 | 6 | resource "aws_route53_record" "acm" { 7 | for_each = { 8 | for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => { 9 | name = dvo.resource_record_name 10 | record = dvo.resource_record_value 11 | type = dvo.resource_record_type 12 | } 13 | } 14 | 15 | allow_overwrite = true 16 | name = each.value.name 17 | records = [each.value.record] 18 | ttl = 60 19 | type = each.value.type 20 | zone_id = data.aws_route53_zone.main.zone_id 21 | } 22 | 23 | resource "aws_route53_record" "acm_cloudfront" { 24 | for_each = { 25 | for dvo in aws_acm_certificate.cloudfront.domain_validation_options : dvo.domain_name => { 26 | name = dvo.resource_record_name 27 | record = dvo.resource_record_value 28 | type = dvo.resource_record_type 29 | } 30 | } 31 | 32 | allow_overwrite = true 33 | name = each.value.name 34 | records = [each.value.record] 35 | ttl = 60 36 | type = each.value.type 37 | zone_id = data.aws_route53_zone.main.zone_id 38 | } 39 | 40 | resource "aws_route53_record" "cloudfront" { 41 | zone_id = data.aws_route53_zone.main.zone_id 42 | name = local.domain 43 | type = "A" 44 | 45 | alias { 46 | name = aws_cloudfront_distribution.main.domain_name 47 | zone_id = aws_cloudfront_distribution.main.hosted_zone_id 48 | evaluate_target_health = true 49 | } 50 | } 51 | 52 | resource "aws_route53_record" "alb" { 53 | zone_id = data.aws_route53_zone.main.zone_id 54 | name = "alb.${local.domain}" 55 | type = "A" 56 | 57 | alias { 58 | name = aws_lb.app.dns_name 59 | zone_id = aws_lb.app.zone_id 60 | evaluate_target_health = true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /s3_build.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "build" { 2 | bucket = "${local.name}.build" 3 | 4 | server_side_encryption_configuration { 5 | rule { 6 | apply_server_side_encryption_by_default { 7 | sse_algorithm = "AES256" 8 | } 9 | } 10 | } 11 | 12 | versioning { 13 | enabled = true 14 | } 15 | 16 | logging { 17 | target_bucket = aws_s3_bucket.logs.id 18 | target_prefix = "s3/${local.name}.build/" 19 | } 20 | 21 | force_destroy = true 22 | } 23 | 24 | resource "aws_s3_bucket_policy" "build" { 25 | bucket = aws_s3_bucket.build.id 26 | policy = data.aws_iam_policy_document.build_bucket.json 27 | } 28 | 29 | data "aws_iam_policy_document" "build_bucket" { 30 | # see: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-5 31 | statement { 32 | sid = "AllowSSLRequestsOnly" 33 | effect = "Deny" 34 | principals { 35 | type = "*" 36 | identifiers = ["*"] 37 | } 38 | actions = ["s3:*"] 39 | resources = [ 40 | aws_s3_bucket.build.arn, 41 | "${aws_s3_bucket.build.arn}/*", 42 | ] 43 | condition { 44 | test = "Bool" 45 | variable = "aws:SecureTransport" 46 | values = ["false"] 47 | } 48 | } 49 | } 50 | 51 | resource "aws_s3_bucket_object" "task_definition" { 52 | bucket = aws_s3_bucket.build.id 53 | key = "task_definition.json" 54 | content = templatefile("files/task_definition.json", { 55 | family = aws_ecs_task_definition.app.family 56 | task_role_arn = aws_iam_role.ecs_task.arn 57 | execution_role_arn = aws_iam_role.ecs.arn 58 | ssm_rails_master_key_name = aws_ssm_parameter.rails_master_key.name 59 | ssm_database_url_name = aws_ssm_parameter.database_url.name 60 | ssm_reader_database_url_name = aws_ssm_parameter.reader_database_url.name 61 | log_group = aws_cloudwatch_log_group.app.name 62 | region = local.region 63 | }) 64 | } 65 | 66 | resource "aws_s3_bucket_object" "appspec" { 67 | bucket = aws_s3_bucket.build.id 68 | key = "appspec.yml" 69 | content = file("files/appspec.yml") 70 | } 71 | -------------------------------------------------------------------------------- /codepipeline.tf: -------------------------------------------------------------------------------- 1 | resource "aws_codepipeline" "deploy" { 2 | name = "${local.name}-deploy" 3 | role_arn = aws_iam_role.codepipeline.arn 4 | 5 | artifact_store { 6 | location = aws_s3_bucket.codepipeline.bucket 7 | type = "S3" 8 | } 9 | 10 | stage { 11 | name = "Source" 12 | 13 | action { 14 | name = "Source" 15 | category = "Source" 16 | owner = "AWS" 17 | provider = "ECR" 18 | version = "1" 19 | output_artifacts = ["source_output"] 20 | 21 | configuration = { 22 | RepositoryName = aws_ecr_repository.rails.name 23 | ImageTag = "latest" 24 | } 25 | } 26 | } 27 | 28 | stage { 29 | name = "Build" 30 | 31 | action { 32 | name = "BuildRails" 33 | category = "Build" 34 | owner = "AWS" 35 | provider = "CodeBuild" 36 | input_artifacts = ["source_output"] 37 | output_artifacts = ["build_output"] 38 | version = "1" 39 | 40 | configuration = { 41 | ProjectName = aws_codebuild_project.build.name 42 | } 43 | } 44 | } 45 | 46 | stage { 47 | name = "Deploy" 48 | 49 | action { 50 | name = "Deploy" 51 | category = "Deploy" 52 | owner = "AWS" 53 | provider = "CodeDeployToECS" 54 | version = "1" 55 | input_artifacts = ["source_output", "build_output"] 56 | 57 | configuration = { 58 | ApplicationName = aws_codedeploy_app.app.name 59 | DeploymentGroupName = aws_codedeploy_deployment_group.app.deployment_group_name 60 | TaskDefinitionTemplateArtifact = "build_output" 61 | TaskDefinitionTemplatePath = "task_definition.json" 62 | AppSpecTemplateArtifact = "build_output" 63 | AppSpecTemplatePath = "appspec.yml" 64 | # NOTE: using image in imageDetail.json from source_output. 65 | # see: https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-ECR.html 66 | Image1ArtifactName = "source_output" 67 | Image1ContainerName = "IMAGE1_NAME" 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "main" { 2 | name = "${local.name}-main" 3 | } 4 | 5 | resource "aws_ecs_task_definition" "app" { 6 | family = "${local.name}-app" 7 | execution_role_arn = aws_iam_role.ecs.arn 8 | task_role_arn = aws_iam_role.ecs_task.arn 9 | network_mode = "awsvpc" 10 | cpu = 256 11 | memory = 512 12 | requires_compatibilities = ["FARGATE"] 13 | 14 | # NOTE: Dummy containers for initial. 15 | container_definitions = < ALB`. 22 | resource "aws_security_group_rule" "alb_from_all_https" { 23 | description = "Allow HTTPS from ALL." 24 | type = "ingress" 25 | from_port = 443 26 | to_port = 443 27 | protocol = "-1" 28 | # NOTE: Allow for CloudFront via Internet. 29 | # tfsec:ignore:AWS006 30 | cidr_blocks = ["0.0.0.0/0"] 31 | security_group_id = aws_security_group.alb.id 32 | } 33 | 34 | resource "aws_security_group_rule" "alb_egress" { 35 | description = "Allow all to outbound." 36 | type = "egress" 37 | from_port = 0 38 | to_port = 0 39 | protocol = "-1" 40 | # NOTE: Allow egress full open for build 41 | # tfsec:ignore:AWS007 42 | cidr_blocks = ["0.0.0.0/0"] 43 | security_group_id = aws_security_group.alb.id 44 | } 45 | 46 | resource "aws_security_group" "ecs" { 47 | name = "${local.name}-ecs" 48 | description = "For ECS." 49 | vpc_id = aws_vpc.main.id 50 | 51 | tags = { 52 | Name = "${local.name}-ecs" 53 | } 54 | } 55 | 56 | resource "aws_security_group_rule" "ecs_from_alb" { 57 | description = "Allow 3000 from Security Group for ALB." 58 | type = "ingress" 59 | from_port = 3000 60 | to_port = 3000 61 | protocol = "tcp" 62 | source_security_group_id = aws_security_group.alb.id 63 | security_group_id = aws_security_group.ecs.id 64 | } 65 | 66 | resource "aws_security_group_rule" "ecs_egress" { 67 | description = "Allow all to outbound." 68 | type = "egress" 69 | from_port = 0 70 | to_port = 0 71 | protocol = "-1" 72 | # NOTE: Allow egress full open for build 73 | # tfsec:ignore:AWS007 74 | cidr_blocks = ["0.0.0.0/0"] 75 | security_group_id = aws_security_group.ecs.id 76 | } 77 | 78 | resource "aws_security_group" "rds" { 79 | name = "${local.name}-rds" 80 | description = "For RDS." 81 | vpc_id = aws_vpc.main.id 82 | 83 | tags = { 84 | Name = "${local.name}-rds" 85 | } 86 | } 87 | 88 | resource "aws_security_group_rule" "rds_from_ecs" { 89 | description = "Allow 5432 from Security Group for ECS." 90 | type = "ingress" 91 | from_port = 5432 92 | to_port = 5432 93 | protocol = "tcp" 94 | source_security_group_id = aws_security_group.ecs.id 95 | security_group_id = aws_security_group.rds.id 96 | } 97 | 98 | resource "aws_security_group_rule" "rds_from_codebuild" { 99 | description = "Allow 5432 from Security Group for CodeBuild." 100 | type = "ingress" 101 | from_port = 5432 102 | to_port = 5432 103 | protocol = "tcp" 104 | source_security_group_id = aws_security_group.codebuild.id 105 | security_group_id = aws_security_group.rds.id 106 | } 107 | 108 | resource "aws_security_group" "vpce_dkr" { 109 | name = "${local.name}-vpce-dkr" 110 | description = "For VPC Endpoint ecr.dkr." 111 | vpc_id = aws_vpc.main.id 112 | 113 | tags = { 114 | Name = "${local.name}-vpce-dkr" 115 | } 116 | } 117 | 118 | resource "aws_security_group" "codebuild" { 119 | name = "${local.name}-codebuild" 120 | description = "For CodeBuild." 121 | vpc_id = aws_vpc.main.id 122 | 123 | tags = { 124 | Name = "${local.name}-codebuild" 125 | } 126 | } 127 | 128 | resource "aws_security_group_rule" "codebuild_egress" { 129 | description = "Allow all to outbound." 130 | type = "egress" 131 | from_port = 0 132 | to_port = 0 133 | protocol = "-1" 134 | # NOTE: Allow egress full open for build 135 | # tfsec:ignore:AWS007 136 | cidr_blocks = ["0.0.0.0/0"] 137 | security_group_id = aws_security_group.codebuild.id 138 | } 139 | 140 | resource "aws_security_group_rule" "vpce_dkr_from_ecs" { 141 | description = "Allow HTTPS from Security Group for ECS." 142 | type = "ingress" 143 | from_port = 443 144 | to_port = 443 145 | protocol = "tcp" 146 | source_security_group_id = aws_security_group.ecs.id 147 | security_group_id = aws_security_group.vpce_dkr.id 148 | } 149 | 150 | resource "aws_security_group_rule" "vpce_dkr_from_codebuild" { 151 | description = "Allow HTTPS from Security Group for CodeBuild." 152 | type = "ingress" 153 | from_port = 443 154 | to_port = 443 155 | protocol = "tcp" 156 | source_security_group_id = aws_security_group.codebuild.id 157 | security_group_id = aws_security_group.vpce_dkr.id 158 | } 159 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/static/v1?label=terraform&message=0.14.9&color=blue) ![](https://img.shields.io/static/v1?label=aws-provider&message=3.35.0&color=blue) [![fmt](https://github.com/reireias/rails-on-ecs-terraform/workflows/fmt/badge.svg)](https://github.com/reireias/rails-on-ecs-terraform/actions) [![tfsec](https://github.com/reireias/rails-on-ecs-terraform/workflows/tfsec/badge.svg)](https://github.com/reireias/rails-on-ecs-terraform/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 2 | 3 | # rails-on-ecs-terraform 4 | Rails on AWS ECS を構築する Terraform の実装です。 5 | 6 | **シンプル** かつ **セキュア** なアーキテクチャに仕上げています。 7 | 8 | Rails リポジトリ: [reireias/rails-on-ecs](https://github.com/reireias/rails-on-ecs) 9 | 10 | ## 構成 11 | 12 | ### 全体概要 13 | ![rails-on-ecs-01](https://user-images.githubusercontent.com/24800246/114057770-d7225080-98cd-11eb-8f93-507f6080ccd3.png) 14 | 15 | CloudFront, ALB, ECS, Auroraを利用したシンプルなアーキテクチャです。 16 | 17 | CloudFrontにはWAFを設定しXSSやSQLi等の攻撃をブロックします。 18 | 19 | CloudFront, ALBのドメインはRoute53で管理しています。 20 | 21 | いずれもMulti-AZレベルの可用性をもつように設計しているため、任意のAZで障害が発生してもサービスは継続できる構成です。 22 | 23 | ### ネットワーク 24 | ![rails-on-ecs-02](https://user-images.githubusercontent.com/24800246/114059568-7562e600-98cf-11eb-9b85-b5e288dc0b93.png) 25 | 26 | VPC内のSubnetは上記のように用途ごとに細かく分けています。 27 | 28 | ### Security Group 29 | ![rails-on-ecs-03](https://user-images.githubusercontent.com/24800246/114063905-0fc52880-98d4-11eb-9159-d9239eb9724e.png) 30 | 31 | Security Groupも用途ごとに分けた設計になっています。 32 | 33 | Security Group間のインバウンドルールのソースに別Security Groupを指定することで、最低限の通信しか許可していません。 34 | 35 | また、VPC Endpointを設定することでインターネットを経由せずにS3やECRと通信できるように設定しています。 36 | 37 | ### CDパイプライン 38 | ![rails-on-ecs-04](https://user-images.githubusercontent.com/24800246/114064398-91b55180-98d4-11eb-995e-33c424d51688.png) 39 | 40 | CDパイプラインは上記のようにECRへのイメージプッシュをトリガーに、CodePipelineで実行されます。 41 | 42 | ECRへのプッシュをトリガーにすることで、Railsアプリケーション開発者とインフラエンジニアの自然な責任境界を実現できます。 43 | 44 | イメージビルドはGitHub Actions側で行います。 45 | 46 | `rails db:migrate` をCodeBuildで実行します。 47 | 48 | デプロイはCodeDeployを採用しており、全トラフィックを一括で新バージョンに切り替えることで、デプロイ時にassets参照が404エラーにならないように配慮しています。 49 | 50 | ## Tips 51 | 52 | ### セキュリティ対策 53 | Well-Architected FrameworkやSecurity Hubによるベストプラクティスなどに従い、下記のセキュリティ対策を実施しています。 54 | 55 | - CloudFrontを前段に配置することによるDDoS対策 56 | - WAFのマネージドルールによるXSSやSQLi等の攻撃のブロック 57 | - RDSの暗号化設定 58 | - SSMパラメータストアを利用した秘匿情報の管理 59 | - ECRによる脆弱性スキャン 60 | - [tfsec](https://tfsec.dev/)によるTerraformコード静的解析によるセキュリティ指摘 61 | 62 | ### ロギング 63 | 以下のログ設定を行っています。 64 | 65 | 可能な限りログは取得するべきです。(このリポジトリではWAFのログは取得していませんが) 66 | 67 | - [CloudFront](cloudfront.tf) 68 | - [ALB](alb.tf) 69 | - [VPC Flow Log](vpc.tf) 70 | - [S3 Access Log](s3_codepipeline.tf) 71 | 72 | ### 証明書とDNSレコード 73 | このリポジトリでは、[route53.tf](route53.tf) の実装のようにRoute53で取得したドメインを利用しています。 74 | 75 | もし、ドメインを別の方法で取得している場合でも、Route53から設定できるように移譲するのが良いでしょう。 76 | 77 | Route53でドメインを設定できると、[acm.tf](acm.tf) のようにACMの検証などもスムーズに実装できます。 78 | 79 | ACM証明書とその検証DNSレコードの実装: 80 | ```terraform 81 | resource "aws_acm_certificate" "main" { 82 | domain_name = local.domain 83 | subject_alternative_names = ["*.${local.domain}"] 84 | validation_method = "DNS" 85 | } 86 | 87 | resource "aws_acm_certificate_validation" "main" { 88 | certificate_arn = aws_acm_certificate.main.arn 89 | validation_record_fqdns = [for record in aws_route53_record.acm : record.fqdn] 90 | } 91 | ``` 92 | 93 | ### カスタムヘッダーによるALBへのリクエストをCloudFront限定にする 94 | CloudFrontで以下のカスタムヘッダーを設定します。 95 | 96 | ```terraform 97 | resource "aws_cloudfront_distribution" "main" { 98 | 99 | origin { 100 | custom_header { 101 | name = "x-pre-shared-key" 102 | value = data.aws_kms_secrets.secrets.plaintext["cloudfront_shared_key"] 103 | } 104 | # ... 105 | } 106 | # ... 107 | } 108 | ``` 109 | 110 | ALBのリスナールールで、上記ヘッダーが付与されたリクエストのみ、ECSへ流すように設定します。 111 | 112 | ```terraform 113 | resource "aws_lb_listener_rule" "app_from_cloudfront" { 114 | listener_arn = aws_lb_listener.app.arn 115 | priority = 100 116 | 117 | action { 118 | type = "forward" 119 | target_group_arn = aws_lb_target_group.app["blue"].arn 120 | } 121 | 122 | condition { 123 | http_header { 124 | http_header_name = "x-pre-shared-key" 125 | values = [data.aws_kms_secrets.secrets.plaintext["cloudfront_shared_key"]] 126 | } 127 | } 128 | 129 | # NOTE: Ignore target group switch 130 | lifecycle { 131 | ignore_changes = [action] 132 | } 133 | } 134 | ``` 135 | 136 | このように設定することで、CloudFrontを経由せずに直接ALBへリクエストする経路を塞ぐことができます。 137 | 138 | ### CodeDeployによりローリングアップデートを回避する 139 | 複数台のサーバーにRailsをローリングアップデートでデプロイすると、一部のjsやcss等のassets系ファイルの参照が404エラーになる瞬間が発生します。 140 | 141 | これは、新サーバーからhtmlを取得し、その中のjsを旧サーバーへ取得しにいくと発生します。 142 | 143 | この問題を回避する方法は様々ありますが、ここではシンプルにCodeDeployを利用しローリングアップデートを実施しないことで回避しています。 144 | 145 | `deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"` を指定することで、全リクエストを同時に新バージョンへ切り替えることができます。 146 | 147 | ### 初期構築時のダミー用ECSタスク定義 148 | プロダクトの構築初期段階など、まだRailsアプリが用意できていない場合に最低限ヘルスチェックにだけ合格する軽量イメージとそれを利用したECSタスク定義が欲しくなります。 149 | 150 | [medpeer/health_check](https://hub.docker.com/r/medpeer/health_check) イメージを使うことで指定したパスのヘルスチェックに合格するだけのECSタスク定義が作成できます。 151 | 152 | ```terraform 153 | resource "aws_ecs_task_definition" "app" { 154 | # ... 155 | 156 | # NOTE: Dummy containers for initial. 157 | container_definitions = <