Atlantis on AWS Fargate Terraform Module

January 8, 2026 ยท View on GitHub

SWUbanner

Atlantis is tool which provides unified workflow for collaborating on Terraform through GitHub, GitLab and Bitbucket Cloud.

Caution

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.

Usage

GitHub is shown below in usage examples; however, any git provider supported by Atlantis can be used by simply using the correct Atlantis environment variables and configuring the respective webhook for the given git provider.

See the Supplemental Docs for additional details on integrating with git providers.

GitHub Complete

The Atlantis module creates all resources required to run Atlantis on AWS Fargate.

module "atlantis" {
  source  = "terraform-aws-modules/atlantis/aws"

  name   = "atlantis"
  vpc_id = "vpc-1234556abcdef"

  # ECS Container Definition
  atlantis = {
    environment = [
      {
        name  = "ATLANTIS_GH_USER"
        value = "myuser"
      },
      {
        name  = "ATLANTIS_REPO_ALLOWLIST"
        value = "github.com/terraform-aws-modules/*"
      },
    ]
    secrets = [
      {
        name      = "ATLANTIS_GH_TOKEN"
        valueFrom = "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes256-7g8H9i"
      },
      {
        name      = "ATLANTIS_GH_WEBHOOK_SECRET"
        valueFrom = "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes192-4D5e6F"
      },
    ]
  }

  # ECS Service
  service = {
    subnet_ids = ["subnet-xyzde987", "subnet-slkjf456", "subnet-qeiru789"]

    task_exec_secret_arns = [
      "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes256-7g8H9i",
      "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes192-4D5e6F",
    ]
    # Provide Atlantis permission necessary to create/destroy resources
    tasks_iam_role_policies = {
      AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess"
    }
  }

  # ALB
  alb = {
    subnet_ids = ["subnet-abcde012", "subnet-bcde012a", "subnet-fghi345a"]
  }

  # ACM
  certificate_domain_name = "example.com"
  route53_zone_id         = "Z2ES7B9AZ6SHAE"

  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}

GitHub Separate

The Atlantis module creates most of resources required to run Atlantis on AWS Fargate, except for the ECS Cluster and ALB. This allows you to integrate Atlantis with your existing AWS infrastructure.

module "atlantis" {
  source  = "terraform-aws-modules/atlantis/aws"

  name   = "atlantis"
  vpc_id = "vpc-1234556abcdef"

  # Existing cluster
  create_cluster = false
  cluster_arn    = "arn:aws:ecs:eu-west-1:123456789012:cluster/default"

  # Existing ALB
  create_alb            = false
  alb_target_group_arn  = "arn:aws:elasticloadbalancing:eu-west-1:1234567890:targetgroup/bluegreentarget1/209a844cd01825a4"
  alb_security_group_id = "sg-12345678"

  # ECS Container Definition
  atlantis = {
    environment = [
      {
        name  = "ATLANTIS_GH_USER"
        value = "myuser"
      },
      {
        name  = "ATLANTIS_REPO_ALLOWLIST"
        value = "github.com/terraform-aws-modules/*"
      },
    ]
    secrets = [
      {
        name      = "ATLANTIS_GH_TOKEN"
        valueFrom = "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes256-7g8H9i"
      },
      {
        name      = "ATLANTIS_GH_WEBHOOK_SECRET"
        valueFrom = "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes192-4D5e6F"
      },
    ]
  }

  # ECS Service
  service = {
    subnet_ids = ["subnet-xyzde987", "subnet-slkjf456", "subnet-qeiru789"]

    task_exec_secret_arns = [
      "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes256-7g8H9i",
      "arn:aws:secretsmanager:eu-west-1:111122223333:secret:aes192-4D5e6F",
    ]
    # Provide Atlantis permission necessary to create/destroy resources
    tasks_iam_role_policies = {
      AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess"
    }
  }

  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}

Utilize EFS for Persistent Storage

You can enable EFS to ensure that any plan outputs are persisted to EFS in the event that the Atlantis Task is replaced:

module "atlantis" {
  source  = "terraform-aws-modules/atlantis/aws"

  # Truncated for brevity ...

  # EFS
  enable_efs = true
  efs = {
    mount_targets = {
      "eu-west-1a" = {
        subnet_id = "subnet-xyzde987"
      }
      "eu-west-1b" = {
        subnet_id = "subnet-slkjf456"
      }
      "eu-west-1c" = {
        subnet_id = "subnet-qeiru789"
      }
    }
  }
}

Supply Atlantis server configuration

server-atlantis.yaml

repos:
  - id: /.*/
    allow_custom_workflows: true
    allowed_overrides:
      - apply_requirements
      - workflow
    apply_requirements:
      - approved
    workflow: default

main.tf

module "atlantis" {
  source  = "terraform-aws-modules/atlantis/aws"

  # ...

  atlantis = {
    environment = [
      {
        name : "ATLANTIS_REPO_CONFIG_JSON",
        value : jsonencode(yamldecode(file("${path.module}/server-atlantis.yaml"))),
      },
    ]
  }
}

Examples

Requirements

NameVersion
terraform>= 1.10
aws>= 6.28

Providers

No providers.

Modules

NameSourceVersion
acmterraform-aws-modules/acm/aws6.1.1
albterraform-aws-modules/alb/aws10.2.0
ecs_clusterterraform-aws-modules/ecs/aws//modules/cluster6.7.0
ecs_serviceterraform-aws-modules/ecs/aws//modules/service6.7.0
efsterraform-aws-modules/efs/aws2.0.0

Resources

No resources.

Inputs

NameDescriptionTypeDefaultRequired
albMap of values passed to ALB module definition. See the ALB module for full list of arguments supported
object({
# Load Balancer
access_logs = optional(object({
bucket = string
enabled = optional(bool, true)
prefix = optional(string)
}))
connection_logs = optional(object({
bucket = string
enabled = optional(bool, true)
prefix = optional(string)
}))
drop_invalid_header_fields = optional(bool, true)
enable_cross_zone_load_balancing = optional(bool, true)
enable_deletion_protection = optional(bool, true)
enable_http2 = optional(bool, true)
enable_waf_fail_open = optional(bool)
enable_zonal_shift = optional(bool, true)
idle_timeout = optional(number)
internal = optional(bool)
ip_address_type = optional(string)
name = optional(string)
preserve_host_header = optional(bool)
security_groups = optional(list(string), [])
subnet_ids = optional(list(string), [])

# Listener(s)
default_port = optional(number, 80)
default_protocol = optional(string, "HTTP")
https_listener_ssl_policy = optional(string, "ELBSecurityPolicy-TLS13-1-2-2021-06")
https_default_action = optional(any, {
forward = {
target_group_key = "atlantis"
}
})
https_listener = optional(any, {})
listeners = optional(any, {})

# Target Group(s)
target_groups = optional(any, {})

# Securtity Group(s)
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
name = optional(string)
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{
http = {
from_port = 80
cidr_ipv4 = "0.0.0.0/0"
}
https = {
from_port = 443
cidr_ipv4 = "0.0.0.0/0"
}
}
)
security_group_egress_rules = optional(
map(object({
name = optional(string)
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{
all = {
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
}
)
security_group_tags = optional(map(string), {})

# Route53 Record(s)
route53_records = optional(map(object({
zone_id = string
name = optional(string)
type = string
evaluate_target_health = optional(bool, true)
})))

# WAF
associate_web_acl = optional(bool, false)
web_acl_arn = optional(string)

tags = optional(map(string), {})
})
{}no
alb_security_group_idID of an existing security group that will be used by ALB. Required if create_alb is falsestring""no
alb_target_group_arnARN of an existing ALB target group that will be used to route traffic to the Atlantis service. Required if create_alb is falsestring""no
atlantisMap of values passed to Atlantis container definition. See the ECS container definition module for full list of arguments supported
object({
uid = optional(string, 100)
gid = optional(string, 1000)

command = optional(list(string))
cpu = optional(number, 2048)
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})), [])
environmentFiles = optional(list(object({
type = string
value = string
})))
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
type = string
options = optional(map(string))
configFile = optional(object({
type = string
content = string
}))
}))
fqdn = optional(string)
healthCheck = optional(object({
command = optional(list(string), [])
interval = optional(number, 30)
retries = optional(number, 3)
startPeriod = optional(number)
timeout = optional(number, 5)
}))
hostname = optional(string)
image = optional(string, "ghcr.io/runatlantis/atlantis:latest")
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number, 4096)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})))
port = optional(number, 4141)
privileged = optional(bool, false)
readonlyRootFilesystem = optional(bool, false)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool, true)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}),
# Default
{
enabled = true
}
)
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number, 30)
stopTimeout = optional(number, 120)
user = optional(string, "atlantis")
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# CloudWatch Log Group
enable_cloudwatch_logging = optional(bool, true)
create_cloudwatch_log_group = optional(bool, true)
cloudwatch_log_group_use_name_prefix = optional(bool, true)
cloudwatch_log_group_retention_in_days = optional(number, 14)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_kms_key_id = optional(string)
})
{}no
certificate_arnARN of certificate issued by AWS ACM. If empty, a new ACM certificate will be created and validated using Route53 DNSstring""no
certificate_domain_nameRoute53 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_namestring""no
clusterMap of values passed to ECS cluster module definition. See the ECS cluster module for full list of arguments supported
object({
# Cluster
name = optional(string)
configuration = optional(object({
execute_command_configuration = optional(object({
kms_key_id = optional(string)
log_configuration = optional(object({
cloud_watch_encryption_enabled = optional(bool)
cloud_watch_log_group_name = optional(string)
s3_bucket_encryption_enabled = optional(bool)
s3_bucket_name = optional(string)
s3_kms_key_id = optional(string)
s3_key_prefix = optional(string)
}))
logging = optional(string, "OVERRIDE")
}))
managed_storage_configuration = optional(object({
fargate_ephemeral_storage_kms_key_id = optional(string)
kms_key_id = optional(string)
}))
}),
# Default
{
execute_command_configuration = {
log_configuration = {
cloud_watch_log_group_name = "placeholder" # will use CloudWatch log group created by module
}
}
}
)
setting = optional(list(object({
name = string
value = string
})),
# Default
[{
name = "containerInsights"
value = "enabled"
}]
)

# Cloudwatch log group
create_cloudwatch_log_group = optional(bool, true)
cloudwatch_log_group_retention_in_days = optional(number, 90)
cloudwatch_log_group_kms_key_id = optional(string)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_tags = optional(map(string), {})

# Capacity providers
default_capacity_provider_strategy = optional(
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
})),
# Default
{
FARGATE = {
weight = 100
}
}
)
})
{}no
cluster_arnARN of an existing ECS cluster where resources will be created. Required when create_cluster is falsestring""no
createControls if resources should be created (affects nearly all resources)booltrueno
create_albDetermines whether to create an ALB or notbooltrueno
create_certificateDetermines whether to create an ACM certificate or not. If false, certificate_arn must be providedbooltrueno
create_clusterWhether to create an ECS cluster or notbooltrueno
create_route53_recordsDetermines whether to create Route53 A and AAAA records for the loadbalancerbooltrueno
efsMap of values passed to EFS module definition. See the EFS module for full list of arguments supported
object({
name = optional(string)

# File System
availability_zone_name = optional(string)
creation_token = optional(string)
performance_mode = optional(string)
encrypted = optional(bool, true)
kms_key_arn = optional(string)
provisioned_throughput_in_mibps = optional(number)
throughput_mode = optional(string)
lifecycle_policy = optional(object({
transition_to_ia = optional(string)
transition_to_archive = optional(string)
transition_to_primary_storage_class = optional(string)
}))
protection = optional(object({
replication_overwrite = optional(string)
}))

# File System Policy
attach_policy = optional(bool, true)
bypass_policy_lockout_safety_check = optional(bool)
source_policy_documents = optional(list(string), [])
override_policy_documents = optional(list(string), [])
policy_statements = optional(any, {})
deny_nonsecure_transport = optional(bool, true)
deny_nonsecure_transport_via_mount_target = optional(bool, true)

# Mount targets
mount_targets = optional(map(object({
ip_address = optional(string)
ip_address_type = optional(string)
ipv6_address = optional(string)
region = optional(string)
security_groups = optional(list(string), [])
subnet_id = string
})),
# Default
{}
)

# Security Group
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
security_group_description = optional(string)
security_group_ingress_rules = optional(any, {})

# Access Point(s)
access_points = optional(any, {})
})
{}no
enable_efsDetermines whether to create and utilize an EFS filesystemboolfalseno
nameCommon name to use on all resources created unless a more specific name is providedstring"atlantis"no
regionRegion where the resource(s) will be managed. Defaults to the Region set in the provider configurationstringnullno
route53_record_nameName of Route53 record to create ACM certificate in and main A-record. If null is specified, var.name is used instead. Provide empty string to point root domain name to ALB.stringnullno
route53_zone_idRoute53 zone ID to use for ACM certificate and Route53 recordsstring""no
serviceMap of values passed to ECS service module definition. See the ECS service module for full list of arguments supported
object({
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
enable_ecs_managed_tags = optional(bool, true)
force_new_deployment = optional(bool, true)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string, "FARGATE")
load_balancer = optional(any, {})
name = optional(string)
assign_public_ip = optional(bool, false)
security_group_ids = optional(list(string), [])
subnet_ids = optional(list(string), [])
platform_version = optional(string)
propagate_tags = optional(string)
timeouts = optional(object({
create = optional(string)
delete = optional(string)
update = optional(string)
}))
triggers = optional(map(string))
wait_for_steady_state = optional(bool)

# Service IAM Role
create_iam_role = optional(bool, true)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool, true)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string), {})
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))

# Task Definition
create_task_definition = optional(bool, true)
task_definition_arn = optional(string)
container_definitions = optional(any, {})
cpu = optional(number, 2048)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
memory = optional(number, 4096)
requires_compatibilities = optional(list(string), ["FARGATE"])
runtime_platform = optional(
object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}),
# Default
{
operating_system_family = "LINUX"
cpu_architecture = "ARM64"
}
)
volume = optional(any, {})
task_tags = optional(map(string), {})

# Task Execution IAM Role
create_task_exec_iam_role = optional(bool, true)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool, true)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string), {})
task_exec_iam_role_policies = optional(map(string), {})
task_exec_iam_role_max_session_duration = optional(number)

# Task Execution IAM Role Policy
create_task_exec_policy = optional(bool, true)
task_exec_ssm_param_arns = optional(list(string), [])
task_exec_secret_arns = optional(list(string), [])
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))

# Tasks - IAM role
create_tasks_iam_role = optional(bool, true)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool, true)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string), {})
tasks_iam_role_policies = optional(map(string), {})
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))

# Security Group
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
security_group_description = optional(string)
security_group_ingress_rules = optional(any, {})
security_group_egress_rules = optional(any,
{
egress = {
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
}
)
security_group_tags = optional(map(string), {})
})
{}no
tagsA map of tags to add to all resourcesmap(string){}no
validate_certificateDetermines whether to validate ACM certificate using Route53 DNS. If false, certificate will be created but not validatedbooltrueno
vpc_idID of the VPC where the resources will be provisionedstring""no

Outputs

NameDescription
albALB created and all of its associated outputs
clusterECS cluster created and all of its associated outputs
efsEFS created and all of its associated outputs
serviceECS service created and all of its associated outputs
urlURL of Atlantis

Authors

Module is maintained by Anton Babenko with help from these awesome contributors.

License

Apache 2 Licensed. See LICENSE for full details.

Additional information for users from Russia and Belarus