diff --git a/src/info.rs b/src/info.rs
index 975fd36fb33bfb6d58a8bde78034f68abec2af49..f63201ff786aa318de728b62a6f97b10283d0b97 100644
--- a/src/info.rs
+++ b/src/info.rs
@@ -1,3 +1,9 @@
+use crate::instance::Grade;
+use crate::instance::InstanceId;
+use crate::replicaset::ReplicasetId;
+use crate::traft::error::Error;
+use crate::traft::node;
+use crate::traft::RaftId;
 use std::borrow::Cow;
 use tarantool::proc;
 
@@ -41,3 +47,96 @@ impl VersionInfo<'static> {
 pub fn proc_version_info() -> VersionInfo<'static> {
     VersionInfo::current()
 }
+
+////////////////////////////////////////////////////////////////////////////////
+// InstanceInfo
+////////////////////////////////////////////////////////////////////////////////
+
+/// Info returned from [`.proc_instance_info`].
+///
+/// [`.proc_instance_info`]: proc_instance_info
+#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
+pub struct InstanceInfo {
+    pub raft_id: RaftId,
+    pub advertise_address: String,
+    pub instance_id: InstanceId,
+    pub instance_uuid: String,
+    pub replicaset_id: ReplicasetId,
+    pub replicaset_uuid: String,
+    pub cluster_id: String,
+    pub current_grade: Grade,
+    pub target_grade: Grade,
+    pub tier: String,
+}
+
+impl tarantool::tuple::Encode for InstanceInfo {}
+
+impl InstanceInfo {
+    pub fn try_get(node: &node::Node, instance_id: Option<&InstanceId>) -> Result<Self, Error> {
+        let instance;
+        match instance_id {
+            None => {
+                let instance_id = node.raft_storage.instance_id()?;
+                let instance_id =
+                    instance_id.expect("should be persisted before Node is initialized");
+                instance = node.storage.instances.get(&instance_id)?;
+            }
+            Some(instance_id) => {
+                instance = node.storage.instances.get(instance_id)?;
+            }
+        }
+
+        let peer_address = node
+            .storage
+            .peer_addresses
+            .get(instance.raft_id)?
+            .unwrap_or_else(|| "<unknown>".into());
+
+        let cluster_id = node.raft_storage.cluster_id()?;
+
+        Ok(InstanceInfo {
+            raft_id: instance.raft_id,
+            advertise_address: peer_address,
+            instance_id: instance.instance_id,
+            instance_uuid: instance.instance_uuid,
+            replicaset_id: instance.replicaset_id,
+            replicaset_uuid: instance.replicaset_uuid,
+            cluster_id,
+            current_grade: instance.current_grade,
+            target_grade: instance.target_grade,
+            tier: instance.tier,
+        })
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// .proc_instance_info
+////////////////////////////////////////////////////////////////////////////////
+
+#[proc(packed_args)]
+pub fn proc_instance_info(request: InstanceInfoRequest) -> Result<InstanceInfo, Error> {
+    let node = node::global()?;
+
+    let instance_id = match &request {
+        InstanceInfoRequest::CurrentInstance(_) => None,
+        InstanceInfoRequest::ByInstanceId([instance_id]) => Some(instance_id),
+    };
+    InstanceInfo::try_get(node, instance_id)
+}
+
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize)]
+#[serde(untagged)]
+enum InstanceInfoRequest {
+    // FIXME: this is the simplest way I found to support a single optional
+    // parameter to the stored procedure. We should probably do something about
+    // it in our custom `Encode`/`Decode` traits.
+    CurrentInstance([(); 0]),
+    ByInstanceId([InstanceId; 1]),
+}
+
+impl ::tarantool::tuple::Encode for InstanceInfoRequest {}
+
+impl crate::rpc::RequestArgs for InstanceInfoRequest {
+    const PROC_NAME: &'static str = crate::stringify_cfunc!(proc_instance_info);
+    type Response = InstanceInfo;
+}
diff --git a/src/lib.rs b/src/lib.rs
index 1a386726ea359f32e4a3b6763460abfa6bdf17a7..55b7c8778bdbfec9f49f8dffca9ee7ca8cfaa1fe 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@
 #![allow(clippy::too_many_arguments)]
 #![allow(clippy::let_and_return)]
 #![allow(clippy::needless_return)]
+#![allow(clippy::needless_late_init)]
 #![allow(clippy::unwrap_or_default)]
 #![allow(clippy::redundant_static_lifetimes)]
 use serde::{Deserialize, Serialize};
diff --git a/src/luamod.rs b/src/luamod.rs
index 238f4ad49b6ed925315ecfd07de235b7eb696db6..baee8b54446c5806696d0507d4ba06b0df25a8d8 100644
--- a/src/luamod.rs
+++ b/src/luamod.rs
@@ -152,13 +152,13 @@ pub(crate) fn setup(args: &args::Run) {
         "},
         tlua::function0(|| -> traft::Result<_> {
             let node = traft::node::global()?;
-            let raft_storage = &node.raft_storage;
+            let info = crate::info::InstanceInfo::try_get(node, None)?;
 
             Ok(tlua::AsTable((
-                ("raft_id", raft_storage.raft_id()?),
-                ("cluster_id", raft_storage.cluster_id()?),
-                ("instance_id", raft_storage.instance_id()?),
-                ("tier", raft_storage.tier()?),
+                ("raft_id", info.raft_id),
+                ("cluster_id", info.cluster_id),
+                ("instance_id", info.instance_id),
+                ("tier", info.tier),
             )))
         }),
     );
@@ -219,24 +219,18 @@ pub(crate) fn setup(args: &args::Run) {
         "},
         tlua::function1(|iid: Option<InstanceId>| -> traft::Result<_> {
             let node = traft::node::global()?;
-            let iid = iid.unwrap_or(node.raft_storage.instance_id()?.unwrap());
-            let instance = node.storage.instances.get(&iid)?;
-            let peer_address = node
-                .storage
-                .peer_addresses
-                .get(instance.raft_id)?
-                .unwrap_or_else(|| "<unknown>".into());
+            let info = crate::info::InstanceInfo::try_get(node, iid.as_ref())?;
 
             Ok(tlua::AsTable((
-                ("raft_id", instance.raft_id),
-                ("advertise_address", peer_address),
-                ("instance_id", instance.instance_id.0),
-                ("instance_uuid", instance.instance_uuid),
-                ("replicaset_id", instance.replicaset_id),
-                ("replicaset_uuid", instance.replicaset_uuid),
-                ("current_grade", instance.current_grade),
-                ("target_grade", instance.target_grade),
-                ("tier", instance.tier),
+                ("raft_id", info.raft_id),
+                ("advertise_address", info.advertise_address),
+                ("instance_id", info.instance_id.0),
+                ("instance_uuid", info.instance_uuid),
+                ("replicaset_id", info.replicaset_id),
+                ("replicaset_uuid", info.replicaset_uuid),
+                ("current_grade", info.current_grade),
+                ("target_grade", info.target_grade),
+                ("tier", info.tier),
             )))
         }),
     );
diff --git a/src/traft/raft_storage.rs b/src/traft/raft_storage.rs
index 212a4a03a9c63595861e2b939240b492d9e18956..4ddc8214d6ad721bf33c59600384856bd662ef77 100644
--- a/src/traft/raft_storage.rs
+++ b/src/traft/raft_storage.rs
@@ -94,6 +94,10 @@ impl RaftSpaceAccess {
         Ok(res)
     }
 
+    /// Returns the persisted `InstanceId` of the current instance.
+    /// This should be persisted before the global [`Node`] is initialized.
+    ///
+    /// [`Node`]: crate::traft::node::Node
     #[inline(always)]
     pub fn instance_id(&self) -> tarantool::Result<Option<InstanceId>> {
         let res = self.try_get_raft_state("instance_id")?;
diff --git a/test/int/test_basics.py b/test/int/test_basics.py
index 281301a48144fdbbf52e2ae43823ca01d24d16bf..0c50a7d8096989f5eb75220f4f9f35b6f8717a17 100644
--- a/test/int/test_basics.py
+++ b/test/int/test_basics.py
@@ -325,3 +325,51 @@ def test_governor_notices_restarts(instance: Instance):
 def test_proc_version_info(instance: Instance):
     info = instance.call(".proc_version_info")
     assert info.keys() == set(["picodata_version", "proc_api_version"])  # type: ignore
+
+
+def test_proc_instance_info(cluster: Cluster):
+    cfg = {
+        "tier": {
+            "storage": {"replication_factor": 1},
+            "router": {"replication_factor": 2},
+        }
+    }
+    cluster.set_init_cfg(cfg)
+
+    i1 = cluster.add_instance(tier="storage")
+    i2 = cluster.add_instance(tier="router")
+
+    i1_info = i1.call(".proc_instance_info")
+    assert i1_info == dict(
+        raft_id=1,
+        advertise_address=f"{i1.host}:{i1.port}",
+        instance_id="i1",
+        instance_uuid=i1.instance_uuid(),
+        replicaset_id="r1",
+        replicaset_uuid=i1.replicaset_uuid(),
+        cluster_id=i1.cluster_id,
+        current_grade=dict(variant="Online", incarnation=1),
+        target_grade=dict(variant="Online", incarnation=1),
+        tier="storage",
+    )
+
+    info = i1.call(".proc_instance_info", "i1")
+    assert i1_info == info
+
+    i2_info = i1.call(".proc_instance_info", "i2")
+    assert i2_info == dict(
+        raft_id=2,
+        advertise_address=f"{i2.host}:{i2.port}",
+        instance_id="i2",
+        instance_uuid=i2.instance_uuid(),
+        replicaset_id="r2",
+        replicaset_uuid=i2.replicaset_uuid(),
+        cluster_id=i1.cluster_id,
+        current_grade=dict(variant="Online", incarnation=1),
+        target_grade=dict(variant="Online", incarnation=1),
+        tier="router",
+    )
+
+    with pytest.raises(TarantoolError) as e:
+        i1.call(".proc_instance_info", "i3")
+    assert 'instance with id "i3" not found' in str(e)