diff --git a/changelogs/unreleased/gh-9078-roles.md b/changelogs/unreleased/gh-9078-roles.md
new file mode 100644
index 0000000000000000000000000000000000000000..00ae8694fbe5d642b33bc089c5c6c60a7e401384
--- /dev/null
+++ b/changelogs/unreleased/gh-9078-roles.md
@@ -0,0 +1,4 @@
+## feature/config
+
+* Introduced the initial support for roles - programs that run when
+  a configuration is loaded or reloaded (gh-9078).
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index e108f3b1f434d01d94bdf2cd1540e5ca872251ca..616d5aef395dd2ab51f2b0cb66765b3ba63ec685 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -44,6 +44,7 @@ lua_source(lua_sources lua/config/applier/console.lua     config_applier_console
 lua_source(lua_sources lua/config/applier/credentials.lua config_applier_credentials_lua)
 lua_source(lua_sources lua/config/applier/fiber.lua       config_applier_fiber_lua)
 lua_source(lua_sources lua/config/applier/mkdir.lua       config_applier_mkdir_lua)
+lua_source(lua_sources lua/config/applier/roles.lua       config_applier_roles_lua)
 lua_source(lua_sources lua/config/applier/sharding.lua    config_applier_sharding_lua)
 lua_source(lua_sources lua/config/cluster_config.lua      config_cluster_config_lua)
 lua_source(lua_sources lua/config/configdata.lua          config_configdata_lua)
diff --git a/src/box/lua/config/applier/roles.lua b/src/box/lua/config/applier/roles.lua
new file mode 100644
index 0000000000000000000000000000000000000000..9e343794c5dba82d8a537fe33483a6dc2d193e3b
--- /dev/null
+++ b/src/box/lua/config/applier/roles.lua
@@ -0,0 +1,87 @@
+local log = require('internal.config.utils.log')
+
+local last_loaded = {}
+local last_loaded_names_ordered = {}
+
+local function stop_roles(roles_to_skip)
+    for id = #last_loaded_names_ordered, 1, -1 do
+        local role_name = last_loaded_names_ordered[id]
+        if roles_to_skip == nil or roles_to_skip[role_name] == nil then
+            log.verbose('roles.apply: stop role ' .. role_name)
+            local ok, err = pcall(last_loaded[role_name].stop)
+            if not ok then
+                error(('Error stopping role %s: %s'):format(role_name, err), 0)
+            end
+        end
+    end
+end
+
+local function apply(config)
+    local configdata = config._configdata
+    local role_names = configdata:get('roles', {use_default = true})
+    if role_names == nil or next(role_names) == nil then
+        stop_roles()
+        return
+    end
+
+    -- Remove duplicates.
+    local roles = {}
+    local roles_ordered = {}
+    for _, role_name in pairs(role_names) do
+        if roles[role_name] == nil then
+            table.insert(roles_ordered, role_name)
+        end
+        roles[role_name] = true
+    end
+
+    -- Stop removed roles.
+    stop_roles(roles)
+
+    -- Run roles.
+    local roles_cfg = configdata:get('roles_cfg', {use_default = true}) or {}
+    local loaded = {}
+    local loaded_names_ordered = {}
+
+    -- Load roles.
+    for _, role_name in ipairs(roles_ordered) do
+        local role = last_loaded[role_name]
+        if not role then
+            log.verbose('roles.apply: load role ' .. role_name)
+            role = require(role_name)
+            local funcs = {'validate', 'apply', 'stop'}
+            for _, func_name in pairs(funcs) do
+                if type(role[func_name]) ~= 'function' then
+                    local err = 'Role %s does not contain function %s'
+                    error(err:format(role_name, func_name), 0)
+                end
+            end
+        end
+        loaded[role_name] = role
+        table.insert(loaded_names_ordered, role_name)
+    end
+
+    -- Validate configs for all roles.
+    for _, role_name in ipairs(roles_ordered) do
+        local ok, err = pcall(loaded[role_name].validate, roles_cfg[role_name])
+        if not ok then
+            error(('Wrong config for role %s: %s'):format(role_name, err), 0)
+        end
+    end
+
+    -- Apply configs for all roles.
+    for _, role_name in ipairs(roles_ordered) do
+        log.verbose('roles.apply: apply config for role ' .. role_name)
+        local ok, err = pcall(loaded[role_name].apply, roles_cfg[role_name])
+        if not ok then
+            error(('Error applying role %s: %s'):format(role_name, err), 0)
+        end
+    end
+
+    last_loaded = loaded
+    last_loaded_names_ordered = loaded_names_ordered
+end
+
+return {
+    name = 'roles',
+    apply = apply,
+}
diff --git a/src/box/lua/config/init.lua b/src/box/lua/config/init.lua
index 3f45bf11a1a7c9f70557a4837a91d216b64f765b..2bd0cd5102296c77d52630f14abe4c93f9312a63 100644
--- a/src/box/lua/config/init.lua
+++ b/src/box/lua/config/init.lua
@@ -150,6 +150,7 @@ function methods._initialize(self)
     self:_register_applier(require('internal.config.applier.console'))
     self:_register_applier(require('internal.config.applier.fiber'))
     self:_register_applier(require('internal.config.applier.sharding'))
+    self:_register_applier(require('internal.config.applier.roles'))
     self:_register_applier(require('internal.config.applier.app'))
 
     if extras ~= nil then
diff --git a/src/box/lua/config/instance_config.lua b/src/box/lua/config/instance_config.lua
index e3881119ce2318f50cf1cf687bff666afb526006..af259943b49117a492a25a5be263079e7908e458 100644
--- a/src/box/lua/config/instance_config.lua
+++ b/src/box/lua/config/instance_config.lua
@@ -1802,6 +1802,13 @@ return schema.new('instance_config', schema.record({
             "compatibility",
         }),
     })),
+    roles_cfg = schema.map({
+        key = schema.scalar({type = 'string'}),
+        value = schema.scalar({type = 'any'}),
+    }),
+    roles = schema.array({
+        items = schema.scalar({type = 'string'})
+    }),
 }, {
     -- This kind of validation cannot be implemented as the
     -- 'validate' annotation of a particular schema node. There
diff --git a/src/box/lua/init.c b/src/box/lua/init.c
index feab7f57fdfa9bdd94fcad42fc7b831f1da69193..36d7e794aac3f810c8628aaf702708a3277c72e7 100644
--- a/src/box/lua/init.c
+++ b/src/box/lua/init.c
@@ -144,6 +144,7 @@ extern char session_lua[],
 	config_applier_credentials_lua[],
 	config_applier_fiber_lua[],
 	config_applier_mkdir_lua[],
+	config_applier_roles_lua[],
 	config_applier_sharding_lua[],
 	config_cluster_config_lua[],
 	config_configdata_lua[],
@@ -387,6 +388,10 @@ static const char *lua_sources[] = {
 	"internal.config.applier.sharding",
 	config_applier_sharding_lua,
 
+	"config/applier/roles",
+	"internal.config.applier.roles",
+	config_applier_roles_lua,
+
 	"config/init",
 	"config",
 	config_init_lua,
diff --git a/test/config-luatest/helpers.lua b/test/config-luatest/helpers.lua
index f998f31db6d58badfc5391d4cd6166bc4e3e1522..f068024b7d343ec73991a7c67d9c88d00b121396 100644
--- a/test/config-luatest/helpers.lua
+++ b/test/config-luatest/helpers.lua
@@ -88,6 +88,7 @@ local simple_config = {
 
 local function prepare_case(g, opts)
     local dir = opts.dir
+    local roles = opts.roles
     local script = opts.script
     local options = opts.options
 
@@ -95,6 +96,12 @@ local function prepare_case(g, opts)
         dir = treegen.prepare_directory(g, {}, {})
     end
 
+    if roles ~= nil and next(roles) ~= nil then
+        for name, body in pairs(roles) do
+            treegen.write_script(dir, name .. '.lua', body)
+        end
+    end
+
     if script ~= nil then
         treegen.write_script(dir, 'main.lua', script)
     end
@@ -135,6 +142,10 @@ end
 -- Start a server with the given script and the given
 -- configuration, run a verification function on it.
 --
+-- * opts.roles
+--
+--   Role codes for writing into corresponding files.
+--
 -- * opts.script
 --
 --   Code write into the main.lua file.
@@ -164,6 +175,7 @@ end
 -- Start tarantool process with the given script/config and check
 -- the error.
 --
+-- * opts.roles
 -- * opts.script
 -- * opts.options
 --
@@ -185,12 +197,17 @@ end
 -- Start a server, write a new script/config, reload, run a
 -- verification function.
 --
+-- * opts.roles
 -- * opts.script
 -- * opts.options
 -- * opts.verify
 --
 --   Same as in success_case().
 --
+-- * opts.roles_2
+--
+--   A new list of roles to prepare before config:reload().
+--
 -- * opts.script_2
 --
 --   A new script to write into the main.lua file before
@@ -205,6 +222,7 @@ end
 --
 --   Verify test invariants after config:reload().
 local function reload_success_case(g, opts)
+    local roles_2 = opts.roles_2
     local script_2 = opts.script_2
     local options = assert(opts.options)
     local verify_2 = assert(opts.verify_2)
@@ -214,6 +232,7 @@ local function reload_success_case(g, opts)
 
     prepare_case(g, {
         dir = prepared.dir,
+        roles = roles_2,
         script = script_2,
         options = options_2,
     })
@@ -227,31 +246,44 @@ end
 -- Start a server, write a new script/config, reload, run a
 -- verification function.
 --
+-- * opts.roles
 -- * opts.script
 -- * opts.options
 -- * opts.verify
 --
 --   Same as in success_case().
 --
+-- * opts.roles_2
+--
+--   A new list of roles to prepare before config:reload().
+--
 -- * opts.script_2
 --
 --   A new script to write into the main.lua file before
 --   config:reload().
 --
+-- * opts.options_2
+--
+--   A new config to use for the config:reload(). It is optional,
+--   if not provided opts.options is used instead.
+--
 -- * opts.exp_err
 --
 --   An error that config:reload() must raise.
 local function reload_failure_case(g, opts)
-    local script_2 = assert(opts.script_2)
+    local script_2 = opts.script_2
+    local roles_2 = opts.roles_2
     local options = assert(opts.options)
+    local options_2 = opts.options_2 or options
     local exp_err = assert(opts.exp_err)
 
     local prepared = success_case(g, opts)
 
     prepare_case(g, {
         dir = prepared.dir,
+        roles = roles_2,
         script = script_2,
-        options = options,
+        options = options_2,
     })
     t.assert_error_msg_equals(exp_err, g.server.exec, g.server, function()
         local config = require('config')
diff --git a/test/config-luatest/roles_test.lua b/test/config-luatest/roles_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..c637af8cea8f02c18faa9408925adb84ea8d10bc
--- /dev/null
+++ b/test/config-luatest/roles_test.lua
@@ -0,0 +1,355 @@
+local t = require('luatest')
+local helpers = require('test.config-luatest.helpers')
+
+local g = helpers.group()
+
+-- Make sure the role is properly loaded.
+g.test_single_role_success = function(g)
+    local one = [[
+        local function apply(cfg)
+            _G.bar = cfg
+        end
+
+        _G.foo = 42
+        _G.bar = nil
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+    local verify = function()
+        local config = require('config')
+        t.assert_equals(_G.foo, 42)
+        local roles_cfg = config:get('roles_cfg')
+        t.assert_equals(roles_cfg['one'], 12345)
+        t.assert_equals(roles_cfg['one'], _G.bar)
+    end
+
+    helpers.success_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 12345},
+            ['roles'] = {'one'}
+        },
+        verify = verify,
+    })
+end
+
+-- Make sure the role is loaded only once during each run.
+g.test_role_repeat_success = function(g)
+    local one = [[
+        local function apply(cfg)
+            _G.foo = _G.foo * cfg
+        end
+
+        _G.foo = 42
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+    local verify = function()
+        local config = require('config')
+        local roles_cfg = config:get('roles_cfg')
+        t.assert_equals(roles_cfg['one'], 3)
+        t.assert_equals(_G.foo, 42 * 3)
+    end
+
+    helpers.success_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 3},
+            ['roles'] = {'one', 'one', 'one', 'one', 'one', 'one', 'one'}
+        },
+        verify = verify,
+    })
+end
+
+-- Make sure all roles are loaded correctly and in correct order.
+g.test_multiple_role_success = function(g)
+    local one = [[
+        local function apply(cfg)
+            _G.foo = tonumber(cfg)
+        end
+
+        _G.foo = nil
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+
+    local two = [[
+        local function apply(cfg)
+            _G.foo = _G.foo + tonumber(cfg)
+        end
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+
+    local three = [[
+        local function apply(cfg)
+            _G.foo = foo / tonumber(cfg)
+        end
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+
+    local verify = function()
+        local config = require('config')
+        t.assert_equals(config:get('roles'), {'one', 'two', 'three'})
+        local roles_cfg = config:get('roles_cfg')
+        local one = roles_cfg['one']
+        local two = roles_cfg['two']
+        local three = roles_cfg['three']
+        t.assert_equals(one, 42)
+        t.assert_equals(two, '24')
+        t.assert_equals(three, 3)
+        t.assert_equals(_G.foo, 22)
+    end
+
+    helpers.success_case(g, {
+        roles = {one = one, two = two, three = three},
+        options = {
+            ['roles_cfg'] = {one = 42, two = '24', three = 3},
+            ['roles'] = {'one', 'two', 'three'}
+        },
+        verify = verify,
+    })
+end
+
+-- Make sure the roles call apply() during a reload.
+g.test_role_reload_success = function(g)
+    local one = [[
+        local function apply(cfg)
+            _G.foo = _G.foo * cfg
+        end
+
+        _G.foo = 42
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+
+    local verify = function()
+        local config = require('config')
+        local roles_cfg = config:get('roles_cfg')
+        t.assert_equals(roles_cfg['one'], 2)
+        t.assert_equals(_G.foo, 42 * 2)
+    end
+
+    local verify_2 = function()
+        local config = require('config')
+        local roles_cfg = config:get('roles_cfg')
+        t.assert_equals(roles_cfg['one'], 3)
+        t.assert_equals(_G.foo, 42 * 2 * 3)
+    end
+
+    helpers.reload_success_case(g, {
+        roles = {one = one},
+        roles_2 = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 2},
+            ['roles'] = {'one'}
+        },
+        options_2 = {
+            ['roles_cfg'] = {one = 3},
+            ['roles'] = {'one'}
+        },
+        verify = verify,
+        verify_2 = verify_2,
+    })
+end
+
+-- Make sure the roles are stopped after reload if they were removed from the
+-- configuration.
+g.test_role_stop_success = function(g)
+    local one = [[
+        local function apply(cfg)
+            _G.foo = _G.foo * cfg
+        end
+
+        local function stop()
+            _G.foo = _G.foo - 100
+        end
+
+        _G.foo = 42
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = stop,
+        }
+    ]]
+
+    local verify = function()
+        local config = require('config')
+        t.assert_equals(config:get('roles'), {'one'})
+        local roles_cfg = config:get('roles_cfg')
+        t.assert_equals(roles_cfg['one'], 2)
+        t.assert_equals(_G.foo, 42 * 2)
+    end
+
+    local verify_2 = function()
+        local config = require('config')
+        t.assert_equals(config:get('roles'), nil)
+        local roles_cfg = config:get('roles_cfg')
+        t.assert_equals(roles_cfg['one'], 1)
+        t.assert_equals(_G.foo, 42 * 2 - 100)
+    end
+
+    helpers.reload_success_case(g, {
+        roles = {one = one},
+        roles_2 = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 2},
+            ['roles'] = {'one'}
+        },
+        options_2 = {
+            ['roles_cfg'] = {one = 1},
+        },
+        verify = verify,
+        verify_2 = verify_2,
+    })
+end
+
+-- Ensure that errors during config validation are handled correctly.
+g.test_role_validate_error = function(g)
+    local one = [[
+        local function validate(cfg)
+            error('something wrong', 0)
+        end
+
+        return {
+            validate = validate,
+            apply = function() end,
+            stop = function() end,
+        }
+    ]]
+
+    helpers.failure_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 1},
+            ['roles'] = {'one'}
+        },
+        exp_err = 'Wrong config for role one: something wrong'
+    })
+end
+
+-- Ensure that errors during role application are handled correctly.
+g.test_role_apply_error = function(g)
+    local one = [[
+        local function apply(cfg)
+            error('something wrong', 0)
+        end
+
+        return {
+            validate = function() end,
+            apply = apply,
+            stop = function() end,
+        }
+    ]]
+
+    helpers.failure_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 1},
+            ['roles'] = {'one'}
+        },
+        exp_err = 'Error applying role one: something wrong'
+    })
+end
+
+-- Make sure an error is raised if not all methods are present.
+g.test_role_no_method_error = function(g)
+    local one = [[
+        return {
+            apply = function() end,
+            stop = function() end,
+        }
+    ]]
+    helpers.failure_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 1},
+            ['roles'] = {'one'}
+        },
+        exp_err = 'Role one does not contain function validate'
+    })
+
+    one = [[
+        return {
+            validate = function() end,
+            stop = function() end,
+        }
+    ]]
+    helpers.failure_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 1},
+            ['roles'] = {'one'}
+        },
+        exp_err = 'Role one does not contain function apply'
+    })
+
+    one = [[
+        return {
+            validate = function() end,
+            apply = function() end,
+        }
+    ]]
+    helpers.failure_case(g, {
+        roles = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 1},
+            ['roles'] = {'one'}
+        },
+        exp_err = 'Role one does not contain function stop'
+    })
+end
+
+-- Ensure that errors during role stopping are handled correctly.
+--
+-- Also verify that the error is raised by config:reload() and the same error
+-- appears in the alerts.
+g.test_role_reload_error = function(g)
+    local one = [[
+        return {
+            validate = function() end,
+            apply = function() end,
+            stop = function() error('Wrongly stopped', 0) end,
+        }
+    ]]
+
+    helpers.reload_failure_case(g, {
+        roles = {one = one},
+        roles_2 = {one = one},
+        options = {
+            ['roles_cfg'] = {one = 1},
+            ['roles'] = {'one'}
+        },
+        options_2 = {
+            ['roles_cfg'] = {one = 1},
+        },
+        verify = function() end,
+        exp_err = 'Error stopping role one: Wrongly stopped'
+    })
+end