Using RDS IAM authentication (AWSAuthenticationPlugin) with a Docker dev-database
Question
An AWS RDS MySQL schema creates IAM-authenticated users, for example:
CREATE USER 'app_iam' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';
Running atlas migrate diff (or any command that uses the dev-database) against a standard
mysql Docker image fails, because that plugin exists only on RDS:
Error: failed to run `atlas migrate diff`: Error: sql/migrate: read migration directory state:
sql/migrate: executing statement "CREATE USER 'app_iam' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';"
from version "20260602081826": Error 1524 (HY000): Plugin 'AWSAuthenticationPlugin' is not loaded
Must the dev-url point at a real RDS instance, or can the dev-database run locally in Docker?
Answer
Atlas replays your schema or migration directory on the dev-database to compute its state. A vanilla mysql image
has no AWSAuthenticationPlugin, so that statement fails with Error 1524. Pointing --dev-url at a real RDS
instance works, but it is slow, requires network access and credentials, and gives up the hermetic, disposable
dev-database.
Instead, build a local MySQL dev image that ships a no-op mock of the plugin: it implements just enough of the
interface for the CREATE USER statement to succeed, but always rejects authentication, so nobody can actually
log in through it. That makes it safe to run locally and in CI.
1. Save the Dockerfile
Save this next to your atlas.hcl as aws-auth-mock.Dockerfile. It compiles a small mock plugin and installs it
into the official MySQL image:
# syntax=docker/dockerfile:1
# Build a MySQL image with a no-op mock AWSAuthenticationPlugin.
ARG MYSQL_VERSION=8.4
FROM oraclelinux:9-slim AS builder
RUN microdnf install -y gcc && microdnf clean all
COPY <<'EOF' /src/aws_auth_plugin.c
/**
* No-Op Mock for AWSAuthenticationPlugin.
*
* Mimics the AWS IAM authentication plugin interface but always rejects
* authentication. Intended for local development with Docker so that MySQL
* does not error about a missing plugin while still preventing IAM login.
*
* Minimal MySQL plugin ABI definitions are embedded inline to avoid a
* build-time dependency on mysql-community-devel packages.
*/
#include <stddef.h>
#define CR_AUTH_USER_CREDENTIALS 1045
#define MYSQL_AUTHENTICATION_INTERFACE_VERSION 0x0201
#define MYSQL_AUTHENTICATION_PLUGIN 7
#define MYSQL_PLUGIN_INTERFACE_VERSION 0x010B
#define PLUGIN_LICENSE_GPL 1
typedef struct st_mysql_plugin_vio MYSQL_PLUGIN_VIO;
typedef struct st_mysql_server_auth_info {
char *user_name;
unsigned int user_name_length;
const char *auth_string;
unsigned long auth_string_length;
char authenticated_as[512];
char external_user[512];
int password_used;
const char *host_or_ip;
unsigned int host_or_ip_length;
} MYSQL_SERVER_AUTH_INFO;
struct st_mysql_auth {
int interface_version;
const char *client_auth_plugin;
int (*authenticate_user)(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info);
int (*generate_authentication_string)(char *outbuf, unsigned int *outbuflen,
const char *inbuf, unsigned int inbuflen);
int (*validate_authentication_string)(char *const inbuf,
unsigned int buflen);
int (*set_salt)(const char *password, unsigned int password_len,
unsigned char *salt, unsigned char *salt_len);
const unsigned long authentication_flags;
int (*compare_password_with_hash)(const char *hash,
unsigned long hash_length,
const char *cleartext,
unsigned long cleartext_length,
int *is_error);
};
struct st_mysql_plugin {
int type;
void *info;
const char *name;
const char *author;
const char *descr;
int license;
int (*init)(void *);
int (*check_uninstall)(void *);
int (*deinit)(void *);
unsigned int version;
void *status_vars;
void *system_vars;
void *__reserved;
unsigned long flags;
};
#define MYSQL_PLUGIN_EXPORT __attribute__((visibility("default")))
static int aws_auth_authenticate(MYSQL_PLUGIN_VIO *vio,
MYSQL_SERVER_AUTH_INFO *info) {
return CR_AUTH_USER_CREDENTIALS; /* always reject */
}
static int aws_auth_generate_auth_string(char *outbuf,
unsigned int *outbuflen,
const char *inbuf,
unsigned int inbuflen) {
if (inbuflen > *outbuflen) inbuflen = *outbuflen;
for (unsigned int i = 0; i < inbuflen; i++) outbuf[i] = inbuf[i];
*outbuflen = inbuflen;
return 0;
}
static int aws_auth_validate_auth_string(char *const inbuf,
unsigned int buflen) {
return 0;
}
static int aws_auth_set_salt(const char *password, unsigned int password_len,
unsigned char *salt, unsigned char *salt_len) {
if (salt_len) *salt_len = 0;
return 0;
}
static int aws_auth_compare_password(const char *hash,
unsigned long hash_length,
const char *cleartext,
unsigned long cleartext_length,
int *is_error) {
if (is_error) *is_error = 0;
return 1; /* passwords never match (reject) */
}
static struct st_mysql_auth aws_auth_handler = {
MYSQL_AUTHENTICATION_INTERFACE_VERSION,
"mysql_clear_password",
aws_auth_authenticate,
aws_auth_generate_auth_string,
aws_auth_validate_auth_string,
aws_auth_set_salt,
0, /* authentication_flags */
aws_auth_compare_password
};
MYSQL_PLUGIN_EXPORT struct st_mysql_plugin _mysql_plugin_declarations_[] = {
{
MYSQL_AUTHENTICATION_PLUGIN,
&aws_auth_handler,
"AWSAuthenticationPlugin",
"Ariga (mock)",
"No-op mock AWS IAM authentication plugin - always rejects",
PLUGIN_LICENSE_GPL,
NULL, NULL, NULL,
0x0100,
NULL, NULL, NULL, 0
},
{0, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0}
};
MYSQL_PLUGIN_EXPORT int _mysql_plugin_interface_version_ =
MYSQL_PLUGIN_INTERFACE_VERSION;
MYSQL_PLUGIN_EXPORT int _mysql_sizeof_struct_st_plugin_ =
sizeof(struct st_mysql_plugin);
EOF
RUN gcc -shared -fPIC -fvisibility=hidden \
-o /src/aws_auth_plugin.so /src/aws_auth_plugin.c
FROM mysql:${MYSQL_VERSION}
COPY /src/aws_auth_plugin.so /usr/lib64/mysql/plugin/aws_auth_plugin.so
RUN mkdir -p /etc/my.cnf.d /etc/mysql/conf.d && \
printf '[mysqld]\nplugin-load-add=AWSAuthenticationPlugin=aws_auth_plugin.so\n' \
| tee /etc/my.cnf.d/aws_auth_plugin.cnf > /etc/mysql/conf.d/aws_auth_plugin.cnf
2. Point the dev-database at the built image
Use a docker block with a build configuration so Atlas builds the image on demand.
A locals block keeps the MySQL version in one place instead of two:
locals {
// Change this to match your RDS engine version, e.g. "8.0".
mysql_version = "8.4"
}
docker "mysql" "dev" {
image = "mysql-aws-auth-mock:${local.mysql_version}"
build {
context = "."
dockerfile = "aws-auth-mock.Dockerfile"
args = {
MYSQL_VERSION = local.mysql_version
}
}
}
data "runtimevar" "vault" {
url = "hashivault://secret/data/database"
}
locals {
vault = jsondecode(data.runtimevar.vault)
}
env "rds" {
url = "mysql://${local.vault.username}:${local.vault.password}@aws-rds-instance.us-east-1.rds.amazonaws.com:3306/database"
dev = docker.mysql.dev.url
// ... the rest of your production config, e.g. migrations_dir, schema, etc.
}
Now any atlas command runs entirely against the local dev-database, with no real RDS instance required. Your
production url still points at RDS and uses real IAM authentication as before; the mock plugin lives only in the
dev-database.
Have additional questions or feedback? Reach out via the Intercom chat widget on this site.