Setting up AWS App Runner with Terraform

Learn how to deploy and manage containerized web applications using AWS App Runner and Terraform, including auto-scaling, monitoring, and security best practices

Setting up AWS App Runner with Terraform

AWS App Runner is a fully managed service that makes it easy to deploy containerized web applications and APIs. This guide demonstrates how to set up and manage App Runner using Terraform.

Video Tutorial

Learn more about managing AWS App Runner with Terraform in this comprehensive video tutorial:

View Source Code

Prerequisites

  • AWS CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • Docker installed locally
  • Container image in Amazon ECR or source code in GitHub

Project Structure

terraform-apprunner/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│   └── apprunner/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── config/
    └── auto_scaling.json

App Runner Configuration

Create modules/apprunner/main.tf:

# App Runner Service
resource "aws_apprunner_service" "main" {
  service_name = "${var.project_name}-service"

  source_configuration {
    authentication_configuration {
      access_role_arn = aws_iam_role.access_role.arn
    }

    image_repository {
      image_configuration {
        port = var.container_port
        runtime_environment_variables = {
          ENV  = var.environment
          PORT = var.container_port
        }
        start_command = var.start_command
      }
      image_identifier      = "${var.ecr_repository_url}:${var.image_tag}"
      image_repository_type = "ECR"
    }

    auto_deployments_enabled = true
  }

  instance_configuration {
    cpu    = var.cpu
    memory = var.memory

    instance_role_arn = aws_iam_role.instance_role.arn
  }

  health_check_configuration {
    healthy_threshold   = 3
    interval           = 5
    path               = "/health"
    protocol           = "HTTP"
    timeout            = 2
    unhealthy_threshold = 3
  }

  auto_scaling_configuration_arn = aws_apprunner_auto_scaling_configuration_version.main.arn

  network_configuration {
    egress_configuration {
      egress_type       = "VPC"
      vpc_connector_arn = aws_apprunner_vpc_connector.main.arn
    }
  }

  tags = merge(
    var.tags,
    {
      Name = "${var.project_name}-service"
    }
  )
}

# Auto Scaling Configuration
resource "aws_apprunner_auto_scaling_configuration_version" "main" {
  auto_scaling_configuration_name = "${var.project_name}-auto-scaling"

  max_concurrency = 100
  max_size        = 25
  min_size        = 1

  tags = merge(
    var.tags,
    {
      Name = "${var.project_name}-auto-scaling"
    }
  )
}

# VPC Connector
resource "aws_apprunner_vpc_connector" "main" {
  vpc_connector_name = "${var.project_name}-connector"
  subnets           = var.subnet_ids
  security_groups   = [aws_security_group.apprunner.id]

  tags = merge(
    var.tags,
    {
      Name = "${var.project_name}-connector"
    }
  )
}

# Security Group
resource "aws_security_group" "apprunner" {
  name        = "${var.project_name}-apprunner"
  description = "Security group for App Runner VPC connector"
  vpc_id      = var.vpc_id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(
    var.tags,
    {
      Name = "${var.project_name}-apprunner"
    }
  )
}

# IAM Roles
resource "aws_iam_role" "access_role" {
  name = "${var.project_name}-access-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "build.apprunner.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "access_role" {
  role       = aws_iam_role.access_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

resource "aws_iam_role" "instance_role" {
  name = "${var.project_name}-instance-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "tasks.apprunner.amazonaws.com"
        }
      }
    ]
  })
}

# Custom Domain
resource "aws_apprunner_custom_domain_association" "main" {
  domain_name = var.domain_name
  service_arn = aws_apprunner_service.main.arn
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "apprunner" {
  name              = "/aws/apprunner/${var.project_name}"
  retention_in_days = 30

  tags = merge(
    var.tags,
    {
      Name = "${var.project_name}-logs"
    }
  )
}

Monitoring and Alerts

  1. CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "response_time" {
  alarm_name          = "${var.project_name}-response-time"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name        = "RequestLatency"
  namespace          = "AWS/AppRunner"
  period             = "300"
  statistic          = "Average"
  threshold          = "1000"
  alarm_description  = "Response time is too high"
  alarm_actions      = [aws_sns_topic.alerts.arn]

  dimensions = {
    ServiceName = aws_apprunner_service.main.service_name
  }
}

resource "aws_cloudwatch_metric_alarm" "error_rate" {
  alarm_name          = "${var.project_name}-error-rate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name        = "HTTP5xxErrorRate"
  namespace          = "AWS/AppRunner"
  period             = "300"
  statistic          = "Average"
  threshold          = "5"
  alarm_description  = "Error rate is too high"
  alarm_actions      = [aws_sns_topic.alerts.arn]

  dimensions = {
    ServiceName = aws_apprunner_service.main.service_name
  }
}

Observability Configuration

  1. X-Ray Tracing
resource "aws_iam_role_policy_attachment" "xray" {
  role       = aws_iam_role.instance_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
}

resource "aws_apprunner_service" "with_tracing" {
  # ... other configuration ...

  observability_configuration {
    observability_enabled = true
    trace_configuration {
      vendor = "AWSXRAY"
    }
  }
}
  1. Application Insights
resource "aws_applicationinsights_application" "main" {
  resource_group_name = aws_resourcegroups_group.main.name
  auto_config_enabled = true
}

resource "aws_resourcegroups_group" "main" {
  name = "${var.project_name}-group"

  resource_query {
    query = jsonencode({
      ResourceTypeFilters = ["AWS::AppRunner::Service"]
      TagFilters = [
        {
          Key = "Project"
          Values = [var.project_name]
        }
      ]
    })
  }
}

Integration Examples

  1. GitHub Source
resource "aws_apprunner_service" "github" {
  service_name = "${var.project_name}-github"

  source_configuration {
    authentication_configuration {
      connection_arn = aws_apprunner_connection.github.arn
    }

    code_repository {
      code_configuration {
        code_configuration_values {
          build_command = "npm install"
          port         = "3000"
          runtime      = "NODEJS_16"
          start_command = "npm start"
        }
        configuration_source = "API"
      }

      repository_url = var.github_repository_url
      source_code_version {
        type  = "BRANCH"
        value = "main"
      }
    }
  }
}
  1. Private ECR
resource "aws_ecr_repository" "main" {
  name                 = var.project_name
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "main" {
  repository = aws_ecr_repository.main.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "Keep last 30 images"
        selection = {
          tagStatus   = "any"
          countType   = "imageCountMoreThan"
          countNumber = 30
        }
        action = {
          type = "expire"
        }
      }
    ]
  })
}

Best Practices

  1. Performance Optimization

    • Configure auto-scaling
    • Optimize container size
    • Use health checks
    • Monitor response times
  2. High Availability

    • Enable auto-deployments
    • Configure health checks
    • Use multiple instances
    • Monitor availability
  3. Security

    • Use VPC connectivity
    • Implement IAM roles
    • Enable encryption
    • Regular updates
  4. Cost Optimization

    • Use appropriate instance size
    • Implement auto-scaling

Conclusion

You’ve learned how to set up and manage AWS App Runner using Terraform. This setup provides:

  • Containerized application deployment
  • Auto-scaling capabilities
  • VPC connectivity
  • Monitoring and observability

Remember to:

  • Monitor application health
  • Implement security best practices
  • Optimize performance
  • Maintain container images

Advanced Features

  1. Custom Runtime Configuration
resource "aws_apprunner_service" "custom" {
  # ... other configuration ...

  source_configuration {
    image_repository {
      image_configuration {
        runtime_environment_secrets = {
          API_KEY = "arn:aws:secretsmanager:region:account:secret:api-key"
        }
        runtime_environment_variables = {
          NODE_ENV = "production"
          REGION   = data.aws_region.current.name
        }
      }
    }
  }
}
  1. Private Service
resource "aws_apprunner_vpc_ingress_connection" "main" {
  name        = "${var.project_name}-ingress"
  service_arn = aws_apprunner_service.main.arn

  ingress_vpc_configuration {
    vpc_id          = var.vpc_id
    vpc_endpoint_id = aws_vpc_endpoint.apprunner.id
  }
}