├── provider.tf ├── modules ├── gitlab-repository-webhook │ ├── outputs.tf │ ├── main.tf │ ├── variables.tf │ └── README.md └── github-repository-webhook │ ├── outputs.tf │ ├── main.tf │ ├── variables.tf │ └── README.md ├── backend.tf ├── var_global.tf ├── outputs.tf ├── terraform.tfvars ├── iam.tf ├── variables.tf ├── README.md └── main.tf /provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "ap-northeast-2" 3 | 4 | } 5 | -------------------------------------------------------------------------------- /modules/gitlab-repository-webhook/outputs.tf: -------------------------------------------------------------------------------- 1 | output "this_repository_webhook_urls" { 2 | description = "Webhook URL" 3 | value = gitlab_project_hook.this.*.url 4 | } 5 | 6 | output "this_repository_webhook_secret" { 7 | description = "Webhook secret" 8 | value = var.webhook_secret 9 | } 10 | -------------------------------------------------------------------------------- /modules/github-repository-webhook/outputs.tf: -------------------------------------------------------------------------------- 1 | output "this_repository_webhook_urls" { 2 | description = "Webhook URL" 3 | value = github_repository_webhook.this.*.url 4 | } 5 | 6 | output "this_repository_webhook_secret" { 7 | description = "Webhook secret" 8 | value = var.webhook_secret 9 | } 10 | -------------------------------------------------------------------------------- /backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.0" 3 | 4 | backend "s3" { 5 | bucket = "alias-apnortheast2-tfstate" 6 | key = "provisioning/terraform/platform/atlantis/terraform.tfstate" 7 | region = "ap-northeast-2" 8 | encrypt = true 9 | dynamodb_table = "terraform-lock" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/gitlab-repository-webhook/main.tf: -------------------------------------------------------------------------------- 1 | provider "gitlab" { 2 | token = var.gitlab_token 3 | base_url = var.gitlab_base_url 4 | } 5 | 6 | resource "gitlab_project_hook" "this" { 7 | count = var.create_gitlab_repository_webhook ? length(var.atlantis_allowed_repo_names) : 0 8 | 9 | project = var.atlantis_allowed_repo_names[count.index] 10 | url = var.webhook_url 11 | token = var.webhook_secret 12 | enable_ssl_verification = false 13 | 14 | merge_requests_events = true 15 | push_events = true 16 | note_events = true 17 | } 18 | -------------------------------------------------------------------------------- /modules/github-repository-webhook/main.tf: -------------------------------------------------------------------------------- 1 | provider "github" { 2 | token = var.github_token 3 | organization = var.github_organization 4 | } 5 | 6 | resource "github_repository_webhook" "this" { 7 | count = var.create_github_repository_webhook ? length(var.atlantis_allowed_repo_names) : 0 8 | 9 | repository = var.atlantis_allowed_repo_names[count.index] 10 | 11 | configuration { 12 | url = var.webhook_url 13 | content_type = "application/json" 14 | insecure_ssl = false 15 | secret = var.webhook_secret 16 | } 17 | 18 | events = [ 19 | "issue_comment", 20 | "pull_request", 21 | "pull_request_review", 22 | "pull_request_review_comment", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /var_global.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | default = "ap-northeast-2" 3 | } 4 | 5 | variable "assume_role_arn" { 6 | description = "The role to assume when accessing the AWS API." 7 | default = "" 8 | } 9 | 10 | # Atlantis user 11 | variable "atlantis_user" { 12 | description = "The username that will be triggering atlantis commands. This will be used to name the session when assuming a role. More information - https://github.com/runatlantis/atlantis#assume-role-session-names" 13 | default = "atlantis_user" 14 | } 15 | 16 | # Account IDs 17 | # Add all account ID to here 18 | variable "account_id" { 19 | default = { 20 | id = "" 21 | } 22 | } 23 | 24 | # Remote State that will be used when creating other resources 25 | # You can add any resource here, if you want to refer from others 26 | variable "remote_state" { 27 | default = { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/github-repository-webhook/variables.tf: -------------------------------------------------------------------------------- 1 | variable "create_github_repository_webhook" { 2 | description = "Whether to create Github repository webhook for Atlantis" 3 | type = bool 4 | default = true 5 | } 6 | 7 | variable "github_token" { 8 | description = "Github token to use when creating webhook" 9 | type = string 10 | default = "" 11 | } 12 | 13 | variable "github_organization" { 14 | description = "Github organization to use when creating webhook" 15 | type = string 16 | default = "" 17 | } 18 | 19 | variable "atlantis_allowed_repo_names" { 20 | description = "List of names of repositories which belong to the organization specified in `github_organization`" 21 | type = list(string) 22 | } 23 | 24 | variable "webhook_url" { 25 | description = "Webhook URL" 26 | type = string 27 | default = "" 28 | } 29 | 30 | variable "webhook_secret" { 31 | description = "Webhook secret" 32 | type = string 33 | default = "" 34 | } 35 | -------------------------------------------------------------------------------- /modules/gitlab-repository-webhook/variables.tf: -------------------------------------------------------------------------------- 1 | variable "create_gitlab_repository_webhook" { 2 | description = "Whether to create Gitlab repository webhook for Atlantis" 3 | type = bool 4 | default = true 5 | } 6 | 7 | variable "gitlab_base_url" { 8 | description = "Gitlab base_url use" 9 | type = string 10 | default = "" 11 | } 12 | 13 | variable "gitlab_token" { 14 | description = "Gitlab token to use when creating webhook" 15 | type = string 16 | default = "" 17 | } 18 | 19 | variable "gitlab_organization" { 20 | description = "Gitlab organization to use when creating webhook" 21 | type = string 22 | default = "" 23 | } 24 | 25 | variable "atlantis_allowed_repo_names" { 26 | description = "List of names of repositories which belong to the organization specified in `gitlab_organization`" 27 | type = list(string) 28 | } 29 | 30 | variable "webhook_url" { 31 | description = "Webhook URL" 32 | type = string 33 | default = "" 34 | } 35 | 36 | variable "webhook_secret" { 37 | description = "Webhook secret" 38 | type = string 39 | default = "" 40 | } 41 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "atlantis_url" { 2 | description = "URL of Atlantis" 3 | value = local.atlantis_url 4 | } 5 | 6 | output "atlantis_url_events" { 7 | description = "Webhook events URL of Atlantis" 8 | value = local.atlantis_url_events 9 | } 10 | 11 | output "atlantis_allowed_repo_names" { 12 | description = "Github repositories where webhook should be created" 13 | value = var.atlantis_allowed_repo_names 14 | } 15 | 16 | output "task_role_arn" { 17 | description = "The Atlantis ECS task role arn" 18 | value = aws_iam_role.ecs_task_execution.arn 19 | } 20 | 21 | output "vpc_id" { 22 | description = "ID of the VPC that was created or passed in" 23 | value = local.vpc_id 24 | } 25 | 26 | output "webhook_secret" { 27 | description = "Webhook secret" 28 | value = element(concat(random_id.webhook.*.hex, [""]), 0) 29 | } 30 | 31 | output "alb_dns_name" { 32 | description = "Dns name of alb" 33 | value = module.alb.dns_name 34 | } 35 | 36 | output "ecs_task_definition" { 37 | description = "Task definition for ECS service (used for external triggers)" 38 | value = aws_ecs_service.atlantis.task_definition 39 | } 40 | -------------------------------------------------------------------------------- /modules/github-repository-webhook/README.md: -------------------------------------------------------------------------------- 1 | # Github repository webhook for Atlantis 2 | 3 | 4 | ## Providers 5 | 6 | | Name | Version | 7 | |------|---------| 8 | | github | n/a | 9 | 10 | ## Inputs 11 | 12 | | Name | Description | Type | Default | Required | 13 | |------|-------------|------|---------|:-----:| 14 | | atlantis\_allowed\_repo\_names | List of names of repositories which belong to the organization specified in `github_organization` | `list(string)` | n/a | yes | 15 | | create\_github\_repository\_webhook | Whether to create Github repository webhook for Atlantis | `bool` | `true` | no | 16 | | github\_organization | Github organization to use when creating webhook | `string` | `""` | no | 17 | | github\_token | Github token to use when creating webhook | `string` | `""` | no | 18 | | webhook\_secret | Webhook secret | `string` | `""` | no | 19 | | webhook\_url | Webhook URL | `string` | `""` | no | 20 | 21 | ## Outputs 22 | 23 | | Name | Description | 24 | |------|-------------| 25 | | this\_repository\_webhook\_secret | Webhook secret | 26 | | this\_repository\_webhook\_urls | Webhook URL | 27 | 28 | 29 | -------------------------------------------------------------------------------- /modules/gitlab-repository-webhook/README.md: -------------------------------------------------------------------------------- 1 | # Gitlab repository webhook for Atlantis 2 | 3 | 4 | ## Providers 5 | 6 | | Name | Version | 7 | |------|---------| 8 | | gitlab | n/a | 9 | 10 | ## Inputs 11 | 12 | | Name | Description | Type | Default | Required | 13 | |------|-------------|------|---------|:-----:| 14 | | atlantis\_allowed\_repo\_names | List of names of repositories which belong to the organization specified in `gitlab_organization` | `list(string)` | n/a | yes | 15 | | create\_gitlab\_repository\_webhook | Whether to create Gitlab repository webhook for Atlantis | `bool` | `true` | no | 16 | | gitlab\_base\_url | Gitlab base\_url use | `string` | `""` | no | 17 | | gitlab\_organization | Gitlab organization to use when creating webhook | `string` | `""` | no | 18 | | gitlab\_token | Gitlab token to use when creating webhook | `string` | `""` | no | 19 | | webhook\_secret | Webhook secret | `string` | `""` | no | 20 | | webhook\_url | Webhook URL | `string` | `""` | no | 21 | 22 | ## Outputs 23 | 24 | | Name | Description | 25 | |------|-------------| 26 | | this\_repository\_webhook\_secret | Webhook secret | 27 | | this\_repository\_webhook\_urls | Webhook URL | 28 | 29 | 30 | -------------------------------------------------------------------------------- /terraform.tfvars: -------------------------------------------------------------------------------- 1 | # VPC - use these parameters to create new VPC resources 2 | # 10.0.0.0 - 10.255.255.255 (10/8 접두사) 3 | # 172.16.0.0 - 172.31.255.255 (172.16/12 접두사) 4 | # 192.168.0.0 - 192.168.255.255 (192.168/16 접두사) 5 | cidr = "192.168.0.0/16" 6 | 7 | azs = ["ap-northeast-2a", "ap-northeast-2c"] 8 | 9 | private_subnets = ["192.168.1.0/24", "192.168.2.0/24"] 10 | 11 | public_subnets = ["192.168.11.0/24", "192.168.12.0/24"] 12 | 13 | # VPC - use these parameters to use existing VPC resources 14 | # vpc_id = "vpc-1651acf1" 15 | # private_subnet_ids = ["subnet-1fe3d837", "subnet-129d66ab"] 16 | # public_subnet_ids = ["subnet-1211eef5", "subnet-163466ab"] 17 | 18 | # DNS 19 | route53_zone_name = "" 20 | route53_zone_id = "" 21 | 22 | # ACM (SSL certificate) 23 | # Specify ARN of an existing certificate or new one will be created and validated using Route53 DNS: 24 | certificate_arn = "" 25 | 26 | # ECS Service and Task 27 | ecs_service_assign_public_ip = true 28 | 29 | # Atlantis 30 | atlantis_allowed_repo_names = [""] 31 | atlantis_repo_whitelist = ["github.com//*"] 32 | 33 | # Specify one of the following block. 34 | # For Github 35 | atlantis_github_user = "" 36 | 37 | # For Gitlab 38 | atlantis_gitlab_user = "" 39 | atlantis_gitlab_user_token = "" 40 | 41 | # For Bitbucket 42 | atlantis_bitbucket_user = "" 43 | atlantis_bitbucket_user_token = "" 44 | 45 | # For Bitbucket on prem (Stash) 46 | # atlantis_bitbucket_base_url = "" 47 | 48 | # Tags 49 | tags = { 50 | Name = "atlantis" 51 | } 52 | -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | # 2 | # alias administrator 3 | # 4 | resource "aws_iam_role" "atlantis_alias_admin" { 5 | name = "atlantis-alias-admin" 6 | path = "/" 7 | 8 | assume_role_policy = jsonencode({ 9 | "Version" : "2012-10-17", 10 | "Statement" : [ 11 | { 12 | "Sid" : "", 13 | "Effect" : "Allow", 14 | "Principal" : { 15 | "AWS" : [ 16 | "arn:aws:iam::${var.account_id.id}:role/atlantis-ecs_task_execution" 17 | ] 18 | }, 19 | "Action" : "sts:AssumeRole" 20 | } 21 | ] 22 | }) 23 | 24 | } 25 | 26 | resource "aws_iam_role_policy" "atlantis_alias_admin" { 27 | name = "atlantis-alias-admin-passrole" 28 | role = aws_iam_role.atlantis_alias_admin.id 29 | 30 | policy = jsonencode({ 31 | "Statement" : [ 32 | { 33 | "Sid" : "AllowIAMPassRole", 34 | "Action" : [ 35 | "iam:PassRole" 36 | ], 37 | "Effect" : "Allow", 38 | "Resource" : "*" 39 | } 40 | ] 41 | }) 42 | 43 | } 44 | 45 | resource "aws_iam_role_policy_attachment" "atlantis_alias_admin" { 46 | role = aws_iam_role.atlantis_alias_admin.id 47 | policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" 48 | } 49 | 50 | data "aws_iam_policy_document" "atlantis_assume_role" { 51 | statement { 52 | actions = [ 53 | "sts:AssumeRole" 54 | ] 55 | 56 | resources = [ 57 | aws_iam_role.atlantis_alias_admin.arn 58 | ] 59 | } 60 | } 61 | 62 | resource "aws_iam_role_policy" "atlantis_assume_role" { 63 | name = "atlantis-assume-role" 64 | role = aws_iam_role.ecs_task_execution.id 65 | policy = data.aws_iam_policy_document.atlantis_assume_role.json 66 | } 67 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "Name to use on all resources created (VPC, ALB, etc)" 3 | type = string 4 | default = "atlantis" 5 | } 6 | 7 | variable "tags" { 8 | description = "A map of tags to use on all resources" 9 | type = map(string) 10 | default = {} 11 | } 12 | 13 | # VPC 14 | variable "vpc_id" { 15 | description = "ID of an existing VPC where resources will be created" 16 | type = string 17 | default = "" 18 | } 19 | 20 | variable "public_subnet_ids" { 21 | description = "A list of IDs of existing public subnets inside the VPC" 22 | type = list(string) 23 | default = [] 24 | } 25 | 26 | variable "private_subnet_ids" { 27 | description = "A list of IDs of existing private subnets inside the VPC" 28 | type = list(string) 29 | default = [] 30 | } 31 | 32 | variable "cidr" { 33 | description = "The CIDR block for the VPC which will be created if `vpc_id` is not specified" 34 | type = string 35 | default = "" 36 | } 37 | 38 | variable "azs" { 39 | description = "A list of availability zones in the region" 40 | type = list(string) 41 | default = [] 42 | } 43 | 44 | variable "public_subnets" { 45 | description = "A list of public subnets inside the VPC" 46 | type = list(string) 47 | default = [] 48 | } 49 | 50 | variable "private_subnets" { 51 | description = "A list of private subnets inside the VPC" 52 | type = list(string) 53 | default = [] 54 | } 55 | 56 | # ALB 57 | variable "alb_ingress_cidr_blocks" { 58 | description = "List of IPv4 CIDR ranges to use on all ingress rules of the ALB." 59 | type = list(string) 60 | default = ["0.0.0.0/0"] 61 | } 62 | 63 | variable "alb_log_bucket_name" { 64 | description = "S3 bucket (externally created) for storing load balancer access logs. Required if alb_logging_enabled is true." 65 | type = string 66 | default = "" 67 | } 68 | 69 | variable "alb_log_location_prefix" { 70 | description = "S3 prefix within the log_bucket_name under which logs are stored." 71 | type = string 72 | default = "" 73 | } 74 | 75 | variable "alb_logging_enabled" { 76 | description = "Controls if the ALB will log requests to S3." 77 | type = bool 78 | default = false 79 | } 80 | 81 | # ACM 82 | variable "certificate_arn" { 83 | description = "ARN of certificate issued by AWS ACM. If empty, a new ACM certificate will be created and validated using Route53 DNS" 84 | type = string 85 | default = "" 86 | } 87 | 88 | variable "acm_certificate_domain_name" { 89 | description = "Route53 domain name to use for ACM certificate. Route53 zone for this domain should be created in advance. Specify if it is different from value in `route53_zone_name`" 90 | type = string 91 | default = "" 92 | } 93 | 94 | # Route53 95 | variable "route53_zone_name" { 96 | description = "Route53 zone name to create ACM certificate in and main A-record, without trailing dot" 97 | type = string 98 | default = "" 99 | } 100 | 101 | variable "route53_zone_id" { 102 | type = string 103 | default = "" 104 | } 105 | 106 | variable "create_route53_record" { 107 | description = "Whether to create Route53 record for Atlantis" 108 | type = bool 109 | default = false 110 | } 111 | 112 | # Cloudwatch 113 | variable "cloudwatch_log_retention_in_days" { 114 | description = "Retention period of Atlantis CloudWatch logs" 115 | type = number 116 | default = 7 117 | } 118 | 119 | # SSM parameters for secrets 120 | variable "webhook_ssm_parameter_name" { 121 | description = "Name of SSM parameter to keep webhook secret" 122 | type = string 123 | default = "/atlantis/webhook/secret" 124 | } 125 | 126 | variable "atlantis_github_user_token_ssm_parameter_name" { 127 | description = "Name of SSM parameter to keep atlantis_github_user_token" 128 | type = string 129 | default = "/atlantis/github/user/token" 130 | } 131 | 132 | variable "atlantis_gitlab_user_token_ssm_parameter_name" { 133 | description = "Name of SSM parameter to keep atlantis_gitlab_user_token" 134 | type = string 135 | default = "/atlantis/gitlab/user/token" 136 | } 137 | 138 | variable "atlantis_bitbucket_user_token_ssm_parameter_name" { 139 | description = "Name of SSM parameter to keep atlantis_bitbucket_user_token" 140 | type = string 141 | default = "/atlantis/bitbucket/user/token" 142 | } 143 | 144 | variable "ssm_kms_key_arn" { 145 | description = "ARN of KMS key to use for entryption and decryption of SSM Parameters. Required only if your key uses a custom KMS key and not the default key" 146 | type = string 147 | default = "" 148 | } 149 | 150 | # ECS Service / Task 151 | variable "ecs_service_assign_public_ip" { 152 | description = "Should be true, if ECS service is using public subnets (more info: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_cannot_pull_image.html)" 153 | type = bool 154 | default = false 155 | } 156 | 157 | variable "policies_arn" { 158 | description = "A list of the ARN of the policies you want to apply" 159 | type = list(string) 160 | default = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"] 161 | } 162 | 163 | variable "ecs_service_desired_count" { 164 | description = "The number of instances of the task definition to place and keep running" 165 | type = number 166 | default = 1 167 | } 168 | 169 | variable "ecs_service_deployment_maximum_percent" { 170 | description = "The upper limit (as a percentage of the service's desiredCount) of the number of running tasks that can be running in a service during a deployment" 171 | type = number 172 | default = 200 173 | } 174 | 175 | variable "ecs_service_deployment_minimum_healthy_percent" { 176 | description = "The lower limit (as a percentage of the service's desiredCount) of the number of running tasks that must remain running and healthy in a service during a deployment" 177 | type = number 178 | default = 50 179 | } 180 | 181 | variable "ecs_task_cpu" { 182 | description = "The number of cpu units used by the task" 183 | type = number 184 | default = 256 185 | } 186 | 187 | variable "ecs_task_memory" { 188 | description = "The amount (in MiB) of memory used by the task" 189 | type = number 190 | default = 512 191 | } 192 | 193 | variable "container_memory_reservation" { 194 | description = "The amount of memory (in MiB) to reserve for the container" 195 | type = number 196 | default = 128 197 | } 198 | 199 | variable "custom_container_definitions" { 200 | description = "A list of valid container definitions provided as a single valid JSON document. By default, the standard container definition is used." 201 | type = string 202 | default = "" 203 | } 204 | 205 | # Atlantis 206 | variable "atlantis_image" { 207 | description = "Docker image to run Atlantis with. If not specified, official Atlantis image will be used" 208 | type = string 209 | default = "" 210 | } 211 | 212 | variable "atlantis_version" { 213 | description = "Verion of Atlantis to run. If not specified latest will be used" 214 | type = string 215 | default = "latest" 216 | } 217 | 218 | variable "atlantis_port" { 219 | description = "Local port Atlantis should be running on. Default value is most likely fine." 220 | type = number 221 | default = 4141 222 | } 223 | 224 | variable "atlantis_repo_whitelist" { 225 | description = "List of allowed repositories Atlantis can be used with" 226 | type = list(string) 227 | } 228 | 229 | variable "atlantis_allowed_repo_names" { 230 | description = "Github repositories where webhook should be created" 231 | type = list(string) 232 | default = [] 233 | } 234 | 235 | variable "allow_repo_config" { 236 | description = "When true allows the use of atlantis.yaml config files within the source repos." 237 | type = string 238 | default = "true" 239 | } 240 | 241 | # Github 242 | variable "atlantis_github_user" { 243 | description = "GitHub username that is running the Atlantis command" 244 | type = string 245 | default = "" 246 | } 247 | 248 | variable "atlantis_github_user_token" { 249 | description = "GitHub token of the user that is running the Atlantis command" 250 | type = string 251 | } 252 | 253 | # Gitlab 254 | variable "atlantis_gitlab_user" { 255 | description = "Gitlab username that is running the Atlantis command" 256 | type = string 257 | default = "" 258 | } 259 | 260 | variable "atlantis_gitlab_user_token" { 261 | description = "Gitlab token of the user that is running the Atlantis command" 262 | type = string 263 | default = "" 264 | } 265 | 266 | variable "atlantis_gitlab_hostname" { 267 | description = "Gitlab server hostname, defaults to gitlab.com" 268 | type = string 269 | default = "gitlab.com" 270 | } 271 | 272 | # Bitbucket 273 | variable "atlantis_bitbucket_user" { 274 | description = "Bitbucket username that is running the Atlantis command" 275 | type = string 276 | default = "" 277 | } 278 | 279 | variable "atlantis_bitbucket_user_token" { 280 | description = "Bitbucket token of the user that is running the Atlantis command" 281 | type = string 282 | default = "" 283 | } 284 | 285 | variable "atlantis_bitbucket_base_url" { 286 | description = "Base URL of Bitbucket Server, use for Bitbucket on prem (Stash)" 287 | type = string 288 | default = "" 289 | } 290 | 291 | variable "custom_environment_secrets" { 292 | description = "List of additional secrets the container will use (list should contain maps with `name` and `valueFrom`)" 293 | type = list(map(string)) 294 | default = [] 295 | } 296 | 297 | variable "custom_environment_variables" { 298 | description = "List of additional environment variables the container will use (list should contain maps with `name` and `value`)" 299 | type = list(map(string)) 300 | default = [] 301 | } 302 | 303 | variable "security_group_ids" { 304 | description = "List of one or more security groups to be added to the load balancer" 305 | type = list(string) 306 | default = [] 307 | } 308 | 309 | variable "aws_ssm_path" { 310 | description = "AWS ARN prefix for SSM (public AWS region or Govcloud). Valid options: aws, aws-us-gov." 311 | type = string 312 | default = "aws" 313 | } 314 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Terraform module which runs Atlantis on AWS Fargate 2 | 3 | [Atlantis](https://www.runatlantis.io/) is tool which provides unified workflow for collaborating on Terraform through GitHub, GitLab and Bitbucket Cloud. 4 | 5 | This repository contains Terraform infrastructure code which creates AWS resources required to run [Atlantis](https://www.runatlantis.io/) on AWS, including: 6 | 7 | - Virtual Private Cloud (VPC) 8 | - SSL certificate using Amazon Certificate Manager (ACM) 9 | - Application Load Balancer (ALB) 10 | - Domain name using AWS Route53 which points to ALB 11 | - [AWS Elastic Cloud Service (ECS)](https://aws.amazon.com/ecs/) and [AWS Fargate](https://aws.amazon.com/fargate/) running Atlantis Docker image 12 | - AWS Parameter Store to keep secrets and access them in ECS task natively 13 | 14 | [AWS Fargate](https://aws.amazon.com/fargate/) is used instead of AWS ECS/EC2 to reduce the bill, and it is also a cool AWS service. 15 | 16 | Depending on which SCM system you use, Github repositories or Gitlab projects has to be configured to post events to Atlantis webhook URL. 17 | 18 | See `README.md` in `examples` for Github or Gitlab for complete details. 19 | 20 | ## Terraform versions 21 | 22 | Terraform Version above 1.0.0. 23 | 24 | ### Before using Atlantis and the code in this repository please make sure that you have read and understood the security implications described in [the official Atlantis documentation](https://www.runatlantis.io/docs/security.html). 25 | 26 | ## How to use this? 27 | 28 | As often with the code published in [terraform-aws-modules GitHub organization](https://github.com/terraform-aws-modules) you should have everything to run this code and get Atlantis up and running. 29 | 30 | There are three ways to do this: 31 | 32 | 1. [As a standalone project](https://github.com/terraform-aws-modules/terraform-aws-atlantis#run-atlantis-as-a-standalone-project) 33 | 1. [As a Terraform module](https://github.com/terraform-aws-modules/terraform-aws-atlantis#run-atlantis-as-a-terraform-module) 34 | 1. [As a part of an existing AWS infrastructure](https://github.com/terraform-aws-modules/terraform-aws-atlantis#run-atlantis-as-a-part-of-an-existing-aws-infrastructure-use-existing-vpc) 35 | 36 | ### Run Atlantis as a standalone project 37 | 38 | 1. Clone this github repository: 39 | 40 | ``` 41 | $ git clone git@github.com:terraform-aws-modules/terraform-aws-atlantis.git 42 | $ cd terraform-aws-atlantis 43 | ``` 44 | 45 | 2. Copy sample `terraform.tfvars.sample` into `terraform.tfvars` and specify required variables there. 46 | 47 | 3. Run `terraform init` to download required providers and modules. 48 | 49 | 4. Run `terraform apply` to apply the Terraform configuration and create required infrastructure. 50 | 51 | 5. Run `terraform output atlantis_url` to get URL where Atlantis is publicly reachable. (Note: It may take a minute or two to get it reachable for the first time) 52 | 53 | 6. Github webhook is automatically created if `github_token`, `github_organization` and `github_repo_names` were specified. Read [Add GitHub Webhook](https://github.com/runatlantis/atlantis#add-github-webhook) in the official Atlantis documentation or check [example "GitHub repository webhook for Atlantis"](https://github.com/terraform-aws-modules/terraform-aws-atlantis/tree/master/examples/github-repository-webhook) to add more webhooks. 54 | 55 | ### Run Atlantis as a Terraform module 56 | 57 | This way allows integration with your existing Terraform configurations. 58 | 59 | ```hcl 60 | module "atlantis" { 61 | source = "terraform-aws-modules/atlantis/aws" 62 | version = "~> 2.0" 63 | 64 | name = "atlantis" 65 | 66 | # VPC 67 | cidr = "10.20.0.0/16" 68 | azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] 69 | private_subnets = ["10.20.1.0/24", "10.20.2.0/24", "10.20.3.0/24"] 70 | public_subnets = ["10.20.101.0/24", "10.20.102.0/24", "10.20.103.0/24"] 71 | 72 | # DNS (without trailing dot) 73 | route53_zone_name = "example.com" 74 | 75 | # ACM (SSL certificate) - Specify ARN of an existing certificate or new one will be created and validated using Route53 DNS 76 | certificate_arn = "arn:aws:acm:eu-west-1:135367859851:certificate/70e008e1-c0e1-4c7e-9670-7bb5bd4f5a84" 77 | 78 | # Atlantis 79 | atlantis_github_user = "atlantis-bot" 80 | atlantis_github_user_token = "examplegithubtoken" 81 | atlantis_repo_whitelist = ["github.com/terraform-aws-modules/*"] 82 | } 83 | ``` 84 | 85 | ### Run Atlantis as a part of an existing AWS infrastructure (use existing VPC) 86 | 87 | This way allows integration with your existing AWS resources - VPC, public and private subnets. Specify the following arguments (see methods described above): 88 | 89 | ``` 90 | vpc_id = "vpc-1651acf1" 91 | private_subnet_ids = ["subnet-1fe3d837", "subnet-129d66ab"] 92 | public_subnet_ids = ["subnet-1211eef5", "subnet-163466ab"] 93 | ``` 94 | 95 | If `vpc_id` is specified it will take precedence over `cidr` and existing VPC will be used. `private_subnet_ids` and `public_subnet_ids` must be specified also. 96 | 97 | Make sure that both private and public subnets were created in the same set of availability zones (ALB will be created in public subnets, ECS Fargate service in private subnets). 98 | 99 | If all provided subnets are public (no NAT gateway) then `ecs_service_assign_public_ip` should be set to `true`. 100 | 101 | ## Notes 102 | 103 | 1. AWS Route53 zone is not created by this module, so zone specified as a value in `route53_zone_name` should be created before using this module. Check documentation for [aws_route53_zone](https://www.terraform.io/docs/providers/aws/r/route53_zone.html). 104 | 1. Currently this module configures Atlantis in a way that it can not be used to work with GitHub and Gitlab simultaneously (can't make list of ECS secrets conditionally). 105 | 1. For Bitbucket Cloud webhook configuration follow instructions in [the official Atlantis documentation](https://www.runatlantis.io/docs/configuring-webhooks.html#bitbucket-cloud-bitbucket-org-webhook). 106 | 107 | 108 | 109 | ## Examples 110 | 111 | * [GitHub repository webhook for Atlantis](https://github.com/terraform-aws-modules/terraform-aws-atlantis/tree/master/examples/github-repository-webhook) 112 | * [GitLab repository webhook for Atlantis](https://github.com/terraform-aws-modules/terraform-aws-atlantis/tree/master/examples/gitlab-repository-webhook) 113 | 114 | 115 | ## Providers 116 | 117 | | Name | Version | 118 | |------|---------| 119 | | aws | n/a | 120 | | random | n/a | 121 | 122 | ## Inputs 123 | 124 | | Name | Description | Type | Default | Required | 125 | |------|-------------|------|---------|:-----:| 126 | | acm\_certificate\_domain\_name | Route53 domain name to use for ACM certificate. Route53 zone for this domain should be created in advance. Specify if it is different from value in `route53_zone_name` | `string` | `""` | no | 127 | | alb\_ingress\_cidr\_blocks | List of IPv4 CIDR ranges to use on all ingress rules of the ALB. | `list(string)` |
[
"0.0.0.0/0"
]
| no | 128 | | alb\_log\_bucket\_name | S3 bucket (externally created) for storing load balancer access logs. Required if alb\_logging\_enabled is true. | `string` | `""` | no | 129 | | alb\_log\_location\_prefix | S3 prefix within the log\_bucket\_name under which logs are stored. | `string` | `""` | no | 130 | | alb\_logging\_enabled | Controls if the ALB will log requests to S3. | `bool` | `false` | no | 131 | | allow\_repo\_config | When true allows the use of atlantis.yaml config files within the source repos. | `string` | `"false"` | no | 132 | | atlantis\_allowed\_repo\_names | Github repositories where webhook should be created | `list(string)` | `[]` | no | 133 | | atlantis\_bitbucket\_base\_url | Base URL of Bitbucket Server, use for Bitbucket on prem (Stash) | `string` | `""` | no | 134 | | atlantis\_bitbucket\_user | Bitbucket username that is running the Atlantis command | `string` | `""` | no | 135 | | atlantis\_bitbucket\_user\_token | Bitbucket token of the user that is running the Atlantis command | `string` | `""` | no | 136 | | atlantis\_bitbucket\_user\_token\_ssm\_parameter\_name | Name of SSM parameter to keep atlantis\_bitbucket\_user\_token | `string` | `"/atlantis/bitbucket/user/token"` | no | 137 | | atlantis\_github\_user | GitHub username that is running the Atlantis command | `string` | `""` | no | 138 | | atlantis\_github\_user\_token | GitHub token of the user that is running the Atlantis command | `string` | `""` | no | 139 | | atlantis\_github\_user\_token\_ssm\_parameter\_name | Name of SSM parameter to keep atlantis\_github\_user\_token | `string` | `"/atlantis/github/user/token"` | no | 140 | | atlantis\_gitlab\_hostname | Gitlab server hostname, defaults to gitlab.com | `string` | `"gitlab.com"` | no | 141 | | atlantis\_gitlab\_user | Gitlab username that is running the Atlantis command | `string` | `""` | no | 142 | | atlantis\_gitlab\_user\_token | Gitlab token of the user that is running the Atlantis command | `string` | `""` | no | 143 | | atlantis\_gitlab\_user\_token\_ssm\_parameter\_name | Name of SSM parameter to keep atlantis\_gitlab\_user\_token | `string` | `"/atlantis/gitlab/user/token"` | no | 144 | | atlantis\_image | Docker image to run Atlantis with. If not specified, official Atlantis image will be used | `string` | `""` | no | 145 | | atlantis\_port | Local port Atlantis should be running on. Default value is most likely fine. | `number` | `4141` | no | 146 | | atlantis\_repo\_whitelist | List of allowed repositories Atlantis can be used with | `list(string)` | n/a | yes | 147 | | atlantis\_version | Verion of Atlantis to run. If not specified latest will be used | `string` | `"latest"` | no | 148 | | aws\_ssm\_path | AWS ARN prefix for SSM (public AWS region or Govcloud). Valid options: aws, aws-us-gov. | `string` | `"aws"` | no | 149 | | azs | A list of availability zones in the region | `list(string)` | `[]` | no | 150 | | certificate\_arn | ARN of certificate issued by AWS ACM. If empty, a new ACM certificate will be created and validated using Route53 DNS | `string` | `""` | no | 151 | | cidr | The CIDR block for the VPC which will be created if `vpc_id` is not specified | `string` | `""` | no | 152 | | cloudwatch\_log\_retention\_in\_days | Retention period of Atlantis CloudWatch logs | `number` | `7` | no | 153 | | container\_memory\_reservation | The amount of memory (in MiB) to reserve for the container | `number` | `128` | no | 154 | | create\_route53\_record | Whether to create Route53 record for Atlantis | `bool` | `true` | no | 155 | | custom\_container\_definitions | A list of valid container definitions provided as a single valid JSON document. By default, the standard container definition is used. | `string` | `""` | no | 156 | | custom\_environment\_secrets | List of additional secrets the container will use (list should contain maps with `name` and `valueFrom`) | `list(map(string))` | `[]` | no | 157 | | custom\_environment\_variables | List of additional environment variables the container will use (list should contain maps with `name` and `value`) | `list(map(string))` | `[]` | no | 158 | | ecs\_service\_assign\_public\_ip | Should be true, if ECS service is using public subnets (more info: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_cannot_pull_image.html) | `bool` | `false` | no | 159 | | ecs\_service\_deployment\_maximum\_percent | The upper limit (as a percentage of the service's desiredCount) of the number of running tasks that can be running in a service during a deployment | `number` | `200` | no | 160 | | ecs\_service\_deployment\_minimum\_healthy\_percent | The lower limit (as a percentage of the service's desiredCount) of the number of running tasks that must remain running and healthy in a service during a deployment | `number` | `50` | no | 161 | | ecs\_service\_desired\_count | The number of instances of the task definition to place and keep running | `number` | `1` | no | 162 | | ecs\_task\_cpu | The number of cpu units used by the task | `number` | `256` | no | 163 | | ecs\_task\_memory | The amount (in MiB) of memory used by the task | `number` | `512` | no | 164 | | name | Name to use on all resources created (VPC, ALB, etc) | `string` | `"atlantis"` | no | 165 | | policies\_arn | A list of the ARN of the policies you want to apply | `list(string)` |
[
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
]
| no | 166 | | private\_subnet\_ids | A list of IDs of existing private subnets inside the VPC | `list(string)` | `[]` | no | 167 | | private\_subnets | A list of private subnets inside the VPC | `list(string)` | `[]` | no | 168 | | public\_subnet\_ids | A list of IDs of existing public subnets inside the VPC | `list(string)` | `[]` | no | 169 | | public\_subnets | A list of public subnets inside the VPC | `list(string)` | `[]` | no | 170 | | route53\_zone\_name | Route53 zone name to create ACM certificate in and main A-record, without trailing dot | `string` | `""` | no | 171 | | security\_group\_ids | List of one or more security groups to be added to the load balancer | `list(string)` | `[]` | no | 172 | | ssm\_kms\_key\_arn | ARN of KMS key to use for entryption and decryption of SSM Parameters. Required only if your key uses a custom KMS key and not the default key | `string` | `""` | no | 173 | | tags | A map of tags to use on all resources | `map(string)` | `{}` | no | 174 | | vpc\_id | ID of an existing VPC where resources will be created | `string` | `""` | no | 175 | | webhook\_ssm\_parameter\_name | Name of SSM parameter to keep webhook secret | `string` | `"/atlantis/webhook/secret"` | no | 176 | 177 | ## Outputs 178 | 179 | | Name | Description | 180 | |------|-------------| 181 | | alb\_dns\_name | Dns name of alb | 182 | | atlantis\_allowed\_repo\_names | Github repositories where webhook should be created | 183 | | atlantis\_url | URL of Atlantis | 184 | | atlantis\_url\_events | Webhook events URL of Atlantis | 185 | | ecs\_task\_definition | Task definition for ECS service (used for external triggers) | 186 | | task\_role\_arn | The Atlantis ECS task role arn | 187 | | vpc\_id | ID of the VPC that was created or passed in | 188 | | webhook\_secret | Webhook secret | 189 | 190 | 191 | 192 | ## Authors 193 | 194 | Module is created and maintained by [Anton Babenko](https://github.com/antonbabenko). 195 | 196 | [Seth Vargo](https://github.com/sethvargo) has created [atlantis-on-gke](https://github.com/sethvargo/atlantis-on-gke)(Terraform configurations for running Atlantis on [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine)). This inspired me to do similar stuff for AWS Fargate. 197 | 198 | ## License 199 | 200 | Apache 2 Licensed. See LICENSE for full details. 201 | 202 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # VPC - existing or new? 3 | vpc_id = var.vpc_id == "" ? module.vpc.vpc_id : var.vpc_id 4 | private_subnet_ids = coalescelist(module.vpc.private_subnets, var.private_subnet_ids, [""]) 5 | public_subnet_ids = coalescelist(module.vpc.public_subnets, var.public_subnet_ids, [""]) 6 | 7 | # Atlantis 8 | atlantis_image = var.atlantis_image == "" ? "runatlantis/atlantis:${var.atlantis_version}" : var.atlantis_image 9 | atlantis_url = "https://${coalesce( 10 | element(concat(aws_route53_record.atlantis.*.fqdn, [""]), 0), 11 | module.alb.dns_name, 12 | "_" 13 | )}" 14 | atlantis_url_events = "${local.atlantis_url}/events" 15 | 16 | # Include only one group of secrets - for github, gitlab or bitbucket 17 | has_secrets = var.atlantis_gitlab_user_token != "" || var.atlantis_github_user_token != "" || var.atlantis_bitbucket_user_token != "" 18 | 19 | secret_name_key = local.has_secrets ? var.atlantis_gitlab_user_token != "" ? "ATLANTIS_GITLAB_TOKEN" : var.atlantis_github_user_token != "" ? "ATLANTIS_GH_TOKEN" : "ATLANTIS_BITBUCKET_TOKEN" : "unknown_secret_name_key" 20 | 21 | secret_name_value_from = local.has_secrets ? var.atlantis_gitlab_user_token != "" ? var.atlantis_gitlab_user_token_ssm_parameter_name : var.atlantis_github_user_token != "" ? var.atlantis_github_user_token_ssm_parameter_name : var.atlantis_bitbucket_user_token_ssm_parameter_name : "unknown_secret_name_value" 22 | 23 | secret_webhook_key = local.has_secrets ? var.atlantis_gitlab_user_token != "" ? "ATLANTIS_GITLAB_WEBHOOK_SECRET" : var.atlantis_github_user_token != "" ? "ATLANTIS_GH_WEBHOOK_SECRET" : "ATLANTIS_BITBUCKET_WEBHOOK_SECRET" : "unknown_secret_webhook_key" 24 | 25 | # Container definitions 26 | container_definitions = var.custom_container_definitions == "" ? var.atlantis_bitbucket_user_token != "" ? module.container_definition_bitbucket.json_map_encoded_list : module.container_definition_github_gitlab.json_map_encoded_list : var.custom_container_definitions 27 | 28 | container_definition_environment = [ 29 | { 30 | name = "ATLANTIS_ALLOW_REPO_CONFIG" 31 | value = var.allow_repo_config 32 | }, 33 | { 34 | name = "ATLANTIS_GITLAB_HOSTNAME" 35 | value = var.atlantis_gitlab_hostname 36 | }, 37 | { 38 | name = "ATLANTIS_LOG_LEVEL" 39 | value = "debug" 40 | }, 41 | { 42 | name = "ATLANTIS_PORT" 43 | value = var.atlantis_port 44 | }, 45 | { 46 | name = "ATLANTIS_ATLANTIS_URL" 47 | value = local.atlantis_url 48 | }, 49 | { 50 | name = "ATLANTIS_GH_USER" 51 | value = var.atlantis_github_user 52 | }, 53 | { 54 | name = "ATLANTIS_GITLAB_USER" 55 | value = var.atlantis_gitlab_user 56 | }, 57 | { 58 | name = "ATLANTIS_BITBUCKET_USER" 59 | value = var.atlantis_bitbucket_user 60 | }, 61 | { 62 | name = "ATLANTIS_BITBUCKET_BASE_URL" 63 | value = var.atlantis_bitbucket_base_url 64 | }, 65 | { 66 | name = "ATLANTIS_REPO_WHITELIST" 67 | value = join(",", var.atlantis_repo_whitelist) 68 | }, 69 | ] 70 | 71 | # Secret access tokens 72 | container_definition_secrets_1 = [ 73 | { 74 | name = local.secret_name_key 75 | valueFrom = local.secret_name_value_from 76 | }, 77 | ] 78 | 79 | # Webhook secrets are not supported by BitBucket 80 | container_definition_secrets_2 = [ 81 | { 82 | name = local.secret_webhook_key 83 | valueFrom = var.webhook_ssm_parameter_name 84 | }, 85 | ] 86 | 87 | tags = merge( 88 | { 89 | "Name" = var.name 90 | }, 91 | var.tags, 92 | ) 93 | } 94 | 95 | data "aws_region" "current" {} 96 | 97 | data "aws_caller_identity" "current" {} 98 | 99 | data "aws_route53_zone" "this" { 100 | count = var.create_route53_record ? 1 : 0 101 | 102 | name = var.route53_zone_name 103 | private_zone = false 104 | } 105 | 106 | ################### 107 | # Secret for webhook 108 | ################### 109 | resource "random_id" "webhook" { 110 | byte_length = "64" 111 | } 112 | 113 | resource "aws_ssm_parameter" "webhook" { 114 | count = var.atlantis_bitbucket_user_token != "" ? 0 : 1 115 | 116 | name = var.webhook_ssm_parameter_name 117 | type = "SecureString" 118 | value = random_id.webhook.hex 119 | } 120 | 121 | resource "aws_ssm_parameter" "atlantis_github_user_token" { 122 | count = var.atlantis_github_user_token != "" ? 1 : 0 123 | 124 | name = var.atlantis_github_user_token_ssm_parameter_name 125 | type = "SecureString" 126 | value = var.atlantis_github_user_token 127 | } 128 | 129 | resource "aws_ssm_parameter" "atlantis_gitlab_user_token" { 130 | count = var.atlantis_gitlab_user_token != "" ? 1 : 0 131 | 132 | name = var.atlantis_gitlab_user_token_ssm_parameter_name 133 | type = "SecureString" 134 | value = var.atlantis_gitlab_user_token 135 | } 136 | 137 | resource "aws_ssm_parameter" "atlantis_bitbucket_user_token" { 138 | count = var.atlantis_bitbucket_user_token != "" ? 1 : 0 139 | 140 | name = var.atlantis_bitbucket_user_token_ssm_parameter_name 141 | type = "SecureString" 142 | value = var.atlantis_bitbucket_user_token 143 | } 144 | 145 | ################### 146 | # VPC 147 | ################### 148 | module "vpc" { 149 | source = "terraform-aws-modules/vpc/aws" 150 | version = "v5.12.1" 151 | 152 | create_vpc = var.vpc_id == "" 153 | 154 | name = var.name 155 | 156 | cidr = var.cidr 157 | azs = var.azs 158 | private_subnets = var.private_subnets 159 | public_subnets = var.public_subnets 160 | 161 | enable_nat_gateway = true 162 | single_nat_gateway = true 163 | 164 | tags = local.tags 165 | } 166 | 167 | ################### 168 | # ALB 169 | ################### 170 | module "alb" { 171 | source = "terraform-aws-modules/alb/aws" 172 | version = "v9.11.0" 173 | 174 | name = var.name 175 | 176 | vpc_id = local.vpc_id 177 | subnets = local.public_subnet_ids 178 | security_groups = flatten([module.alb_https_sg.security_group_id, module.alb_http_sg.security_group_id, var.security_group_ids]) 179 | 180 | access_logs = { 181 | bucket = var.alb_log_bucket_name 182 | enabled = var.alb_logging_enabled 183 | prefix = var.alb_log_location_prefix 184 | } 185 | 186 | listeners = [ 187 | { 188 | port = 443 189 | protocol = "HTTPS" 190 | certificate_arn = var.certificate_arn == "" ? module.acm.acm_certificate_arn : var.certificate_arn 191 | forward = { 192 | target_group_key = "target" 193 | } 194 | }, 195 | { 196 | port = 80 197 | protocol = "HTTP" 198 | redirect = { 199 | port = "443" 200 | protocol = "HTTPS" 201 | status_code = "HTTP_301" 202 | } 203 | }, 204 | ] 205 | 206 | target_groups = { 207 | target = { 208 | # name_prefix = "target" 209 | backend_protocol = "HTTP" 210 | backend_port = var.atlantis_port 211 | create_attachment = false 212 | target_type = "ip" 213 | deregistration_delay = 10 214 | } 215 | } 216 | 217 | tags = local.tags 218 | } 219 | 220 | ################### 221 | # Security groups 222 | ################### 223 | module "alb_https_sg" { 224 | source = "terraform-aws-modules/security-group/aws//modules/https-443" 225 | version = "v5.1.2" 226 | 227 | name = "${var.name}-alb-https" 228 | vpc_id = local.vpc_id 229 | description = "Security group with HTTPS ports open for specific IPv4 CIDR block (or everybody), egress ports are all world open" 230 | 231 | ingress_cidr_blocks = var.alb_ingress_cidr_blocks 232 | 233 | tags = local.tags 234 | } 235 | 236 | module "alb_http_sg" { 237 | source = "terraform-aws-modules/security-group/aws//modules/http-80" 238 | version = "v5.1.2" 239 | 240 | name = "${var.name}-alb-http" 241 | vpc_id = local.vpc_id 242 | description = "Security group with HTTP ports open for specific IPv4 CIDR block (or everybody), egress ports are all world open" 243 | 244 | ingress_cidr_blocks = var.alb_ingress_cidr_blocks 245 | 246 | tags = local.tags 247 | } 248 | 249 | module "atlantis_sg" { 250 | source = "terraform-aws-modules/security-group/aws" 251 | version = "v5.1.2" 252 | 253 | name = var.name 254 | vpc_id = local.vpc_id 255 | description = "Security group with open port for Atlantis (${var.atlantis_port}) from ALB, egress ports are all world open" 256 | 257 | computed_ingress_with_source_security_group_id = [ 258 | { 259 | from_port = var.atlantis_port 260 | to_port = var.atlantis_port 261 | protocol = "tcp" 262 | description = "Atlantis" 263 | source_security_group_id = module.alb_https_sg.security_group_id 264 | }, 265 | ] 266 | 267 | number_of_computed_ingress_with_source_security_group_id = 1 268 | 269 | egress_rules = ["all-all"] 270 | 271 | tags = local.tags 272 | } 273 | 274 | ################### 275 | # ACM (SSL certificate) 276 | ################### 277 | module "acm" { 278 | source = "terraform-aws-modules/acm/aws" 279 | version = "v5.1.0" 280 | 281 | create_certificate = var.certificate_arn == "" 282 | 283 | domain_name = var.acm_certificate_domain_name == "" ? join(".", [var.name, var.route53_zone_name]) : var.acm_certificate_domain_name 284 | 285 | validation_method = "DNS" 286 | 287 | # zone_id = var.certificate_arn == "" ? element(concat(data.aws_route53_zone.this.*.id, [""]), 0) : "" 288 | zone_id = var.route53_zone_id 289 | tags = local.tags 290 | } 291 | 292 | ################### 293 | # Route53 record 294 | ################### 295 | resource "aws_route53_record" "atlantis" { 296 | zone_id = var.route53_zone_id 297 | name = var.name 298 | type = "A" 299 | 300 | alias { 301 | name = module.alb.dns_name 302 | zone_id = module.alb.zone_id 303 | evaluate_target_health = true 304 | } 305 | } 306 | 307 | ################### 308 | # ECS 309 | ################### 310 | module "ecs" { 311 | source = "terraform-aws-modules/ecs/aws" 312 | version = "v5.11.4" 313 | 314 | cluster_name = var.name 315 | } 316 | 317 | data "aws_iam_policy_document" "ecs_tasks" { 318 | statement { 319 | actions = [ 320 | "sts:AssumeRole", 321 | ] 322 | 323 | effect = "Allow" 324 | 325 | principals { 326 | type = "Service" 327 | identifiers = ["ecs-tasks.amazonaws.com"] 328 | } 329 | } 330 | } 331 | 332 | resource "aws_iam_role" "ecs_task_execution" { 333 | name = "${var.name}-ecs_task_execution" 334 | assume_role_policy = data.aws_iam_policy_document.ecs_tasks.json 335 | } 336 | 337 | resource "aws_iam_role_policy_attachment" "ecs_task_execution" { 338 | count = length(var.policies_arn) 339 | 340 | role = aws_iam_role.ecs_task_execution.id 341 | policy_arn = element(var.policies_arn, count.index) 342 | } 343 | 344 | // ref: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html 345 | data "aws_iam_policy_document" "ecs_task_access_secrets" { 346 | statement { 347 | effect = "Allow" 348 | 349 | resources = [ 350 | "arn:${var.aws_ssm_path}:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${var.webhook_ssm_parameter_name}", 351 | "arn:${var.aws_ssm_path}:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${var.atlantis_github_user_token_ssm_parameter_name}", 352 | "arn:${var.aws_ssm_path}:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${var.atlantis_gitlab_user_token_ssm_parameter_name}", 353 | "arn:${var.aws_ssm_path}:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${var.atlantis_bitbucket_user_token_ssm_parameter_name}", 354 | ] 355 | 356 | actions = [ 357 | "ssm:GetParameters", 358 | "secretsmanager:GetSecretValue", 359 | ] 360 | } 361 | } 362 | 363 | data "aws_iam_policy_document" "ecs_task_access_secrets_with_kms" { 364 | count = var.ssm_kms_key_arn == "" ? 0 : 1 365 | 366 | source_policy_documents = [data.aws_iam_policy_document.ecs_task_access_secrets.json] 367 | 368 | statement { 369 | sid = "AllowKMSDecrypt" 370 | effect = "Allow" 371 | actions = ["kms:Decrypt"] 372 | resources = [var.ssm_kms_key_arn] 373 | } 374 | } 375 | 376 | resource "aws_iam_role_policy" "ecs_task_access_secrets" { 377 | count = var.atlantis_github_user_token != "" || var.atlantis_gitlab_user_token != "" || var.atlantis_bitbucket_user_token != "" ? 1 : 0 378 | 379 | name = "ECSTaskAccessSecretsPolicy" 380 | 381 | role = aws_iam_role.ecs_task_execution.id 382 | 383 | policy = element( 384 | compact( 385 | concat( 386 | data.aws_iam_policy_document.ecs_task_access_secrets_with_kms.*.json, 387 | data.aws_iam_policy_document.ecs_task_access_secrets.*.json, 388 | ), 389 | ), 390 | 0, 391 | ) 392 | } 393 | 394 | module "container_definition_github_gitlab" { 395 | source = "cloudposse/ecs-container-definition/aws" 396 | version = "v0.61.1" 397 | 398 | container_name = var.name 399 | container_image = local.atlantis_image 400 | 401 | container_cpu = var.ecs_task_cpu 402 | container_memory = var.ecs_task_memory 403 | container_memory_reservation = var.container_memory_reservation 404 | 405 | port_mappings = [ 406 | { 407 | containerPort = var.atlantis_port 408 | hostPort = var.atlantis_port 409 | protocol = "tcp" 410 | }, 411 | ] 412 | log_configuration = { 413 | logDriver = "awslogs" 414 | options = { 415 | "awslogs-region" = data.aws_region.current.name 416 | "awslogs-group" = aws_cloudwatch_log_group.atlantis.name 417 | "awslogs-stream-prefix" = "ecs" 418 | } 419 | } 420 | 421 | environment = concat( 422 | local.container_definition_environment, 423 | var.custom_environment_variables, 424 | ) 425 | 426 | secrets = concat( 427 | local.container_definition_secrets_1, 428 | local.container_definition_secrets_2, 429 | var.custom_environment_secrets, 430 | ) 431 | } 432 | 433 | module "container_definition_bitbucket" { 434 | source = "cloudposse/ecs-container-definition/aws" 435 | version = "v0.61.1" 436 | 437 | container_name = var.name 438 | container_image = local.atlantis_image 439 | 440 | container_cpu = var.ecs_task_cpu 441 | container_memory = var.ecs_task_memory 442 | container_memory_reservation = var.container_memory_reservation 443 | 444 | port_mappings = [ 445 | { 446 | containerPort = var.atlantis_port 447 | hostPort = var.atlantis_port 448 | protocol = "tcp" 449 | }, 450 | ] 451 | 452 | log_configuration = { 453 | logDriver = "awslogs" 454 | options = { 455 | "awslogs-region" = data.aws_region.current.name 456 | "awslogs-group" = aws_cloudwatch_log_group.atlantis.name 457 | "awslogs-stream-prefix" = "ecs" 458 | } 459 | } 460 | 461 | environment = concat( 462 | local.container_definition_environment, 463 | var.custom_environment_variables, 464 | ) 465 | 466 | secrets = concat( 467 | local.container_definition_secrets_1, 468 | var.custom_environment_secrets, 469 | ) 470 | } 471 | 472 | resource "aws_ecs_task_definition" "atlantis" { 473 | family = var.name 474 | execution_role_arn = aws_iam_role.ecs_task_execution.arn 475 | task_role_arn = aws_iam_role.ecs_task_execution.arn 476 | network_mode = "awsvpc" 477 | requires_compatibilities = ["FARGATE"] 478 | cpu = var.ecs_task_cpu 479 | memory = var.ecs_task_memory 480 | 481 | container_definitions = local.container_definitions 482 | } 483 | 484 | data "aws_ecs_task_definition" "atlantis" { 485 | task_definition = var.name 486 | 487 | depends_on = [aws_ecs_task_definition.atlantis] 488 | } 489 | 490 | resource "aws_ecs_service" "atlantis" { 491 | name = var.name 492 | cluster = module.ecs.cluster_id 493 | task_definition = "${data.aws_ecs_task_definition.atlantis.family}:${max( 494 | aws_ecs_task_definition.atlantis.revision, 495 | data.aws_ecs_task_definition.atlantis.revision, 496 | )}" 497 | desired_count = var.ecs_service_desired_count 498 | launch_type = "FARGATE" 499 | deployment_maximum_percent = var.ecs_service_deployment_maximum_percent 500 | deployment_minimum_healthy_percent = var.ecs_service_deployment_minimum_healthy_percent 501 | 502 | network_configuration { 503 | subnets = local.private_subnet_ids 504 | security_groups = [module.atlantis_sg.security_group_id] 505 | assign_public_ip = var.ecs_service_assign_public_ip 506 | } 507 | 508 | load_balancer { 509 | container_name = var.name 510 | container_port = var.atlantis_port 511 | target_group_arn = module.alb.target_groups["target"].arn 512 | } 513 | } 514 | 515 | ################### 516 | # Cloudwatch logs 517 | ################### 518 | resource "aws_cloudwatch_log_group" "atlantis" { 519 | name = var.name 520 | retention_in_days = var.cloudwatch_log_retention_in_days 521 | 522 | tags = local.tags 523 | } 524 | 525 | --------------------------------------------------------------------------------