Deploying Keycloak on AWS ECS with Fargate using Terraform

Mi Do
8 min readFeb 7, 2025

· TL;DR
· Architecture Overview
Key Components:
· Step 1: Setting Up the AWS Environment
1.1: Create a VPC
1.2: Create Subnets
· Step 2: Configure Security Groups
2.1: Security Group for ECS
· Step 3: Set Up the ECS Cluster
· Step 4: Configure AWS RDS for Keycloak
· Step 5: Create an ECS Task Definition for Keycloak
· Step 6: Deploy ECS Service
· Step 7: Set Up Auto Scaling
7.1: CPU Auto Scaling Policy
· Step 8: Configure ALB for Secure Access
AWS Application Load Balancer (ALB)
Target Group (Attaches ECS Containers to ALB)
· Step 9: Deploy and Test

TL;DR

You can find deployment ready solution here — https://github.com/metronom72/keycloak_deployment/tree/main

Architecture Overview

Key Components:

  1. AWS ECR (Elastic Container Registry) for storing container images.
  2. AWS ECS Cluster running Keycloak as a Fargate task.
  3. AWS RDS (PostgreSQL) as the database backend.
  4. AWS ALB (Application Load Balancer) for secure access.
  5. AWS CloudWatch for logging.
  6. AWS IAM Roles for permissions.
  7. AWS Auto Scaling Policies for optimizing performance.

Step 1: Setting Up the AWS Environment

1.1: Create a VPC

resource "aws_vpc" "keycloak" {
cidr_block = "10.0.0.0/16"
}

A VPC (Virtual Private Cloud) is a private network within AWS where you can launch resources (e.g., EC2 instances, databases). It allows you to control:

  1. IP addressing (e.g., private and public subnets)
  2. Network access (firewalls, security groups, and route tables)
  3. Connectivity (VPNs, peering, and gateways)

This specifies the CIDR (Classless Inter-Domain Routing) block for the VPC. 10.0.0.0/16 means:

  1. The network starts at 10.0.0.0.
  2. The /16 means it includes IP addresses from 10.0.0.0 to 10.0.255.255 (65,536 addresses).
  3. This allows subnetting within the VPC.

1.2: Create Subnets

resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.keycloak.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = element(["eu-central-1a", "eu-central-1b"], count.index)
}

A subnet (subnetwork) is a segment of a VPC that groups resources (e.g., EC2 instances) into smaller networks. Subnets help with: Organizing resources (e.g., private vs. public subnets)

  1. Two subnets allow redundancy (if one AZ fails, the other is still running).
  2. Private subnets keep internal resources (e.g., databases) hidden from the public internet.
  3. CIDR allocation ensures that each subnet gets a unique range of IP addresses.

This Terraform block defines two private subnets inside the VPC (aws_vpc.keycloak). Let's break it down step by step:

  1. count = 2

This tells Terraform to create two subnets instead of just one.
Each subnet will have a different index (
0 for the first, 1 for the second).

2. vpc_id = aws_vpc.keycloak.id

This links each subnet to the VPC named “keycloak”.
aws_vpc.keycloak.id retrieves the ID of the previously defined VPC.

3. cidr_block = "10.0.${count.index}.0/24"

This assigns a unique CIDR block (IP range) to each subnet.
The
${count.index} dynamically inserts 0 for the first subnet and 1 for the second:

  • First subnet: 10.0.0.0/24 (256 IP addresses: 10.0.0.0 – 10.0.0.255)
  • Second subnet: 10.0.1.0/24 (256 IP addresses: 10.0.1.0 – 10.0.1.255)

4. availability_zone = element([“eu-central-1a”, “eu-central-1b”], count.index)

Step 2: Configure Security Groups

2.1: Security Group for ECS

resource "aws_security_group" "ecs_sg" {
vpc_id = aws_vpc.keycloak.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

A security group in AWS acts as a virtual firewall for controlling inbound and outbound traffic for your resources (such as EC2 instances or ECS tasks).

1. resource "aws_security_group" "ecs_sg"

This defines an AWS Security Group named ecs_sg in Terraform.

2. vpc_id = aws_vpc.keycloak.id

This security group is attached to the VPC named “keycloak”.

3. ingress { ... } (Inbound Rules)

This rule allows ALL inbound traffic from anywhere in the world (0.0.0.0/0).
from_port = 0, to_port = 0 and protocol = "-1" mean all protocols and ports are open.

4. egress { ... } (Outbound Rules)

This rule allows ALL outbound traffic to any destination (0.0.0.0/0).
Like ingress, from_port = 0, to_port = 0, protocol = "-1" means all traffic is allowed.

Step 3: Set Up the ECS Cluster

resource "aws_ecs_cluster" "keycloak_cluster" {
name = "keycloak-cluster"
}

An ECS (Elastic Container Service) cluster is a logical grouping of containerized applications running on AWS. It allows you to deploy, manage, and scale Docker containers using AWS Fargate (serverless) or EC2 instances (self-managed).

1. resource “aws_ecs_cluster” “keycloak_cluster”

This creates an ECS cluster in AWS.
“keycloak_cluster” is a Terraform identifier (used for referencing within the configuration).

2. name = “keycloak-cluster”

This assigns the name “keycloak-cluster” to the ECS cluster.
You will see this name in the AWS ECS Console under “Clusters”.

Step 4: Configure AWS RDS for Keycloak

resource "aws_db_instance" "postgres" {
identifier = "keycloak-db"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = "securepassword"
publicly_accessible = false
}

AWS RDS (Relational Database Service) is a managed database service that automates setup, scaling, backups, and maintenance. This code creates a PostgreSQL database instance on AWS.

1. resource "aws_db_instance" "postgres"

  • This creates an AWS RDS database instance.
  • "postgres" is a Terraform identifier for internal use.

2. identifier = "keycloak-db"

  • This sets the database instance name to "keycloak-db" (visible in AWS Console).

3. engine = "postgres"

  • This specifies that the database will run PostgreSQL.

4. instance_class = "db.t3.micro"

  • Defines the instance type (CPU, memory, networking):
  • "db.t3.micro" is a small and cost-effective instance.
  • Suitable for development/testing, but not for high-traffic production.

5. allocated_storage = 20

  • Sets the storage size to 20GB.
  • Can be increased but not decreased after creation.

6. username = "admin" & password = "securepassword"

  • Defines the database administrator credentials.
  • Then, define db_password in a Terraform variable file or use AWS Secrets Manager.

7. publicly_accessible = false

  • The database CANNOT be accessed from the public internet (good for security).
  • Only resources inside the VPC can connect.

Step 5: Create an ECS Task Definition for Keycloak

resource "aws_ecs_task_definition" "keycloak" {
family = "keycloak-task"
requires_compatibilities = ["FARGATE"]
memory = "2048"
cpu = "1024"
execution_role_arn = aws_iam_role.ecsTaskExecutionRole.arn
task_role_arn = aws_iam_role.ecsTaskExecutionRole.arn
network_mode = "awsvpc"

container_definitions = jsonencode([
{
name = "keycloak"
image = "${aws_ecr_repository.keycloak.repository_url}:latest"
memory = 2048
cpu = 1024
essential = true
portMappings = [
{ containerPort = 8443, protocol = "tcp" },
{ containerPort = 8080, protocol = "tcp" }
]
environment = [
{ name = "KC_DB", value = "postgres" },
{ name = "KC_DB_URL", value = "jdbc:postgresql://${aws_db_instance.postgres.endpoint}/keycloak" },
{ name = "KC_HOSTNAME", value = "keycloak.example.com" }
]
secrets = [
{ name = "KEYCLOAK_ADMIN", valueFrom = "${aws_secretsmanager_secret.keycloak_admin.arn}:username::" },
{ name = "KEYCLOAK_ADMIN_PASSWORD", valueFrom = "${aws_secretsmanager_secret.keycloak_admin.arn}:password::" }
]
}
])
}

An ECS Task Definition defines how a containerized application should run in AWS ECS. It includes CPU, memory, container image, environment variables, ports, and secrets.

  1. ECS starts 2 Keycloak containers using AWS Fargate.
  2. Containers run inside private subnets.
  3. The load balancer distributes incoming traffic to healthy Keycloak containers.
  4. If a container crashes, ECS replaces it automatically.
  5. Security groups ensure controlled network access.

Step 6: Deploy ECS Service

resource "aws_ecs_service" "keycloak_service" {
name = "keycloak-service"
cluster = aws_ecs_cluster.keycloak_cluster.id
task_definition = "${aws_ecs_task_definition.keycloak.family}:${aws_ecs_task_definition.keycloak.revision}"
launch_type = "FARGATE"
desired_count = 2

network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.ecs_sg.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.keycloak_tg.arn
container_name = "keycloak"
container_port = 8443
}
}

An ECS Service is responsible for running and managing multiple copies (tasks) of a containerized application.

It ensures:

  1. Automatic scaling (if a container stops, it restarts).
  2. Load balancing (distributes traffic across running tasks).
  3. Integration with networking (subnets, security groups).

Step 7: Set Up Auto Scaling

resource "aws_appautoscaling_target" "ecs_scaling" {
resource_id = "service/${aws_ecs_cluster.keycloak_cluster.name}/${aws_ecs_service.keycloak_service.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
min_capacity = 2
max_capacity = 10
}

7.1: CPU Auto Scaling Policy

resource "aws_appautoscaling_policy" "cpu_scaling" {
name = "cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs_scaling.resource_id
scalable_dimension = aws_appautoscaling_target.ecs_scaling.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs_scaling.service_namespace
}

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 60.0
scale_in_cooldown = 60
scale_out_cooldown = 60
}
}

Step 8: Configure ALB for Secure Access

resource "aws_lb" "keycloak_alb" {
name = "keycloak-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.ecs_sg.id]
subnets = aws_subnet.private[*].id
}

resource "aws_lb_target_group" "keycloak_tg" {
name = "keycloak-tg"
port = 8443
protocol = "HTTPS"
vpc_id = aws_vpc.keycloak.id
}

An AWS Load Balancer (ALB — Application Load Balancer) distributes incoming traffic to multiple ECS tasks (Keycloak containers) running in different availability zones.

  1. High availability (distributes traffic across multiple containers)
  2. Scalability (handles increasing users without crashing)
  3. Security (hides internal resources and prevents direct access)

AWS Application Load Balancer (ALB)

resource "aws_lb" "keycloak_alb" {
name = "keycloak-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.ecs_sg.id]
subnets = aws_subnet.private[*].id
}

1. name = "keycloak-alb"

  • Defines the name of the Load Balancer as "keycloak-alb".

2. internal = false

  • The ALB is publicly accessible (needed for external users).
  • If set to true, only internal services can access it.

3. load_balancer_type = "application"

  • Specifies this is an Application Load Balancer (ALB), optimized for routing HTTP and HTTPS traffic.

4. security_groups = [aws_security_group.ecs_sg.id]

  • The security group controls which traffic is allowed.

5. subnets = aws_subnet.private[*].id

  • The ALB is deployed in private subnets (should be public subnets if external users need access).

Target Group (Attaches ECS Containers to ALB)

resource "aws_lb_target_group" "keycloak_tg" {
name = "keycloak-tg"
port = 8443
protocol = "HTTPS"
vpc_id = aws_vpc.keycloak.id
}

1. name = "keycloak-tg"

  • Names the Target Group "keycloak-tg" (used by ECS service).

2. port = 8443 & protocol = "HTTPS"

  • The ALB will listen on port 8443 and route HTTPS traffic.

3. vpc_id = aws_vpc.keycloak.id

  • The Target Group is attached to the Keycloak VPC.

Step 9: Deploy and Test

  1. Run terraform apply to provision the infrastructure.
  2. Verify the ECS Service in AWS Console.
  3. Check the ALB URL for Keycloak Login.

Sample Docker file content is

FROM quay.io/keycloak/keycloak:26.0.6 AS builder

WORKDIR /opt/keycloak

ARG STOREPASS

RUN /opt/keycloak/bin/kc.sh build --health-enabled true \
--metrics-enabled true \
--db postgres \
--features preview \
--metrics-enabled=true

RUN keytool -genkeypair -storepass password \
-storetype PKCS12 -keyalg RSA \
-keysize 2048 -dname "CN=server" \
-alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" \
-keystore /opt/keycloak/conf/server.keystore

FROM quay.io/keycloak/keycloak:26.0.6

USER root

COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

USER keycloak

COPY --from=builder /opt/keycloak/ /opt/keycloak/

COPY cache-ispn-jdbc-ping.xml /opt/keycloak/conf/cache-ispn-jdbc-ping.xml

ENTRYPOINT ["/docker-entrypoint.sh"]

docker-entrypoint.sh is

#!/bin/sh
set -e

exec /opt/keycloak/bin/kc.sh start \
--optimized \
--proxy-headers=xforwarded \
--http-metrics-slos=true \
"$@"

In addition you can set up AWS ECR, and publish your docker there

If you want to integration Keycloak into your project and don’t know where to start — you can reach me in telegram https://t.me/r137y or here https://dorokhovich.com/

You can find cache-ispn-jdbc-ping.xml that is used for JDBC PING setup here — https://gist.github.com/metronom72/8d581e94904613e1d1eb0edca5eb96f8

--

--

Mi Do
Mi Do

Written by Mi Do

You need any support - feel free to reach me out https://dorokhovich.com/

No responses yet