DEV Community

Abdulrasheed Abdulazeez
Abdulrasheed Abdulazeez

Posted on

Deploying Adminer on ECS to Access a Private RDS Database (Terraform)

Recently, I worked on a project where we set up Elastic Container Service (ECS) and an RDS PostgreSQL database on AWS. For security reasons, both were deployed in a private subnet, making them inaccessible from the public internet.

The challenge was:

  • How can administrators access the database to view tables, inspect data, and perform troubleshooting when it’s not publicly accessible?
  • If developers cannot directly connect using desktop database clients, or connect to the database endpoint locally when they run the service, how do we make it convenient for them to run quick fix and debugging?

This is where Adminer becomes very useful.


Why Traditional Approaches Fall Short

When i first thought about how to let administrators access a database in a private subnet, i explored the usual options and quickly realized each one had serious drawbacks.

Bastion Host (Jump Server)

One common approach is to spin up a bastion host, or jump server, in a public subnet. The idea is simple, admins SSH into the bastion, then connect to the private database.

But in practice, it’s a hassle:

  • You need to manage EC2 instances and keep them secure.
  • SSH keys must be distributed and rotated.
  • Every extra server adds cost and increases the attack surface.

For a small team or occasional access, this felt like overkill.

VPN (Client or Site-to-Site)

Next, i considered a VPN. It can give admins direct access to the private network, which sounds ideal.

The problem? Setting up and maintaining a VPN is complicated. There’s software to install, configurations to manage, and costs to consider, especially if it’s just for occasional database access. For our needs, it was more effort than it was worth.

Public RDS Access

Some teams might be tempted to just open up the RDS instance to the internet. It’s quick, and you can connect from anywhere. I knew immediately that this was not what the team wanted, exposing a production database publicly is a serious security risk. Not an option.

Port Forwarding via SSM

AWS offers Session Manager port forwarding via SSM as another way to reach private resources. While technically feasible, it’s not exactly user-friendly:

  • Sessions are temporary and can drop unexpectedly.
  • Extra tooling and scripts are often required.
  • Collaboration among team members becomes awkward.

By the time i weighed all these options, it became clear i needed a solution that was simple, secure, and easy for anyone on the team to use. That’s when Adminer entered the picture. By containerizing Adminer, we could keep it inside the same VPC and private subnet as our RDS database, giving our admins secure access without opening the database to the public internet.


What is Adminer

Adminer is a full-featured database management tool built with PHP. It’s similar to phpMyAdmin, but much lighter and faster, making it ideal for modern cloud environments where you want something quick to deploy and easy to scale down or remove when you’re done.

It supports multiple database engines such as PostgreSQL, MySQL, MS SQL, Oracle, and more, and it comes with a clean, straightforward interface for running queries, browsing tables, and inspecting data.

Adminer is especially useful when your database lives in a private subnet and you still need controlled access for troubleshooting. Instead of exposing the database publicly or forcing every developer to configure local database clients, Adminer provides a secure, browser-based way to inspect and debug.

Think of it as a web alternative to tools like pgAdmin, DBeaver, or TablePlus.


Architecture

Here’s a simplified view of how it works:

  • Adminer lives in the same ECS cluster and subnet as our app
  • It reuses the existing ALB, so we don’t need extra servers.
  • We can turn it off, scale it down, or remove it whenever we want.
  • And admins just need a browser, no local tools required.

Implementation:

For this setup, I used the official Adminer Docker image (adminer:latest). It’s lightweight (~90MB), ships with PHP + Apache, exposes port 8080, and supports PostgreSQL out of the box.

Adminer runs as an ECS service inside the same private network as the application, while access is provided securely through the existing public ALB.

Adminer ECS Task Definition

Adminer is deployed as an ECS task running on EC2 launch type, using minimal resources:

CPU: 128 units

Memory: 256MB

Container port: 8080

Adminer is configured using environment variables so it knows which database to connect to:

  • ADMINER_DEFAULT_SERVER → RDS endpoint
  • ADMINER_DEFAULT_DB_DRIVERpgsql
  • ADMINER_DEFAULT_DB_NAME → database name
  • ADMINER_DESIGN → optional UI theme
# Adminer Task Definition
resource "aws_ecs_task_definition" "adminer" {
  count              = var.enable_adminer ? 1 : 0
  family             = "myapp-adminer-${var.environment}"
  network_mode       = "bridge"
  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn
  container_definitions = jsonencode([
    {
      name      = "adminer"
      image     = "adminer:latest"
      essential = true
      memory    = 256
      cpu       = 128
            portMappings = [
        {
          containerPort = 8080
          hostPort      = 0
          protocol      = "tcp"
        }
      ]
      environment = [
        {
          name  = "ADMINER_DEFAULT_SERVER"
          value = var.db_host
        },
        {
          name  = "ADMINER_DEFAULT_DB_DRIVER"
          value = "pgsql"
        },
        {
          name  = "ADMINER_DEFAULT_DB_NAME"
          value = var.db_name
        },
        {
          name  = "ADMINER_DESIGN"
          value = "pepa-linha-dark"
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.ecs.name
          awslogs-region        = data.aws_region.current.name
          awslogs-stream-prefix = "adminer"
        }
      }
      healthCheck = {
        command     = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider <http://localhost:8080/> || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
    }
  ])
  tags = {
    Name        = "myapp-adminer-${var.environment}"
    Environment = var.environment
  }
}
Enter fullscreen mode Exit fullscreen mode

Adminer ECS Service (Behind the ALB)

The ECS service ensures Adminer stays running and is reachable through the ALB.

  • Service name: myapp-adminer-dev
  • Desired count: 1
  • Registered into the existing ALB target group
  • Health checks handled by the ALB
# Adminer ECS Service
resource "aws_ecs_service" "adminer" {
  count           = var.enable_adminer ? 1 : 0
  name            = "myapp-adminer-${var.environment}"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.adminer[0].arn
  desired_count   = 1
  launch_type     = "EC2"
  deployment_maximum_percent         = 200
  deployment_minimum_healthy_percent = 100
  load_balancer {
    target_group_arn = var.adminer_target_group_arn
    container_name   = "adminer"
    container_port   = 8080
  }
  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }
  ordered_placement_strategy {
    type  = "spread"
    field = "instanceId"
  }
  ordered_placement_strategy {
    type  = "spread"
    field = "attribute:ecs.availability-zone"
  }
  tags = {
    Name        = "myapp-adminer-${var.environment}"
    Environment = var.environment
  }
  depends_on = [var.adminer_target_group_arn]
}
Enter fullscreen mode Exit fullscreen mode

Routing With ALB

Adminer is routed using a path-based rule, so only requests matching:

/adminer*
Enter fullscreen mode Exit fullscreen mode

are forwarded to the Adminer target group.

This keeps the application routing unchanged and prevents interference with normal app traffic.

Security

No extra security groups were required beyond what already existed:

  • ALB handles HTTP ingress to Adminer
  • ECS can reach RDS on port 5432
  • Database authentication is still required, meaning Adminer does not bypass credentials

Secrets Manager (Created Using AWS CLI)

In my setup, database credentials are stored in AWS Secrets Manager, and the same secret is reused by both the application and Adminer.

Create secret

aws secretsmanager create-secret \\
  --name myapp/${ENV}/db_credentials \\
  --description"PostgreSQL credentials for Adminer + app" \\
  --secret-string '{
    "username": "db_user",
    "password": "db_password"
  }'
Enter fullscreen mode Exit fullscreen mode

Update secret value

aws secretsmanager put-secret-value \\
  --secret-id myapp/${ENV}/db_credentials \\
  --secret-string '{
    "username": "db_user",
    "password": "new_password"
  }'
Enter fullscreen mode Exit fullscreen mode

Using Adminer

Once deployed, access is simple:

  1. Open:
http://<alb-dns-name>/adminer
Enter fullscreen mode Exit fullscreen mode
  1. The login screen appears with prefilled values:
  2. System: PostgreSQL
  3. Server: RDS endpoint
  4. Database: your DB name

  1. Enter credentials and log in.

Deployment Flow

Everything is managed as Infrastructure as Code (IaC) using Terraform:

  1. Infrastructure
  2. Create task definition
  3. Create ECS service
  4. Create ALB target group + listener rule

2. Deploy

  • Run terraform plan
  • Run terraform apply

3. Verify

  • ECS task is running
  • target group is healthy
  • Adminer UI loads successfully

Conclusion

By running Adminer inside ECS, it gave our admins secure, browser-based access to a private RDS database without compromising our VPC or infrastructure.

It’s fast, safe, and easy to scale or remove, a perfect solution for teams that need occasional database access without the headaches of VPNs or bastion hosts.

Adminer may be small, but in our setup, it became a powerful tool that makes life easier for developers.

Top comments (0)