If you've ever used a graphics API like OpenGL before, you're probably familiar with the idea of shader resources. The Vulkan spec defines resources as "views of memory with associated formatting and dimensionality". OpenGL was a global state machine where the shader resources were bound to specified bind points using API calls. We did not have to worry about any of the low level mechanisms in OpenGL, but we do for Vulkan.
Vulkan introduces the concept of descriptors which are "opaque data structures used to access shader resources such as buffers, images, or samplers. Rather than existing as distinct objects, descriptors are handled as opaque data, which can be accessed by a shader through descriptor heaps, descriptor buffers, or descriptor sets". So to summarize, descriptors are pointers to some shader resource and can be accessed from the shader using one of the following: descriptor heaps, descriptor buffers, or descriptor sets.
Additionally, as their name would imply, descriptors describe where to find a resource. This could include things like the usage type of a resource, offsets into a buffer, metadata, etc.
As of the 1.4.360 Vulkan spec, Vulkan supports three primary resources types: buffers, images, and tensors. Here's how it differentiates between the three types: "Buffers provide access to raw arrays of bytes, whereas images can be multidimensional and may have associated metadata. Tensors can be multidimensional, contain format information like images and may have associated metadata." The difference between images and tensors has to do with their use cases. Images are usually 1/2/3D graphical textures and as such are optimized for sampling and pixel operations. Tensors were introduced quite recently for machine learning workloads.
Vulkan also defines other resource types such as acceleration structures and micromaps, though they ultimately use buffers as the backing store for opaque data structures.
Resources will be discussed in much more detail in another post, so for now we will shift our attention to descriptors and descriptor sets.
A single descriptor will describe a single resource. However, we can't just use a descriptor by itself. Descriptors are always grouped into descriptor sets, and we use the entire set at a time. Each set can contain one or more descriptors, and the programmer has full control over this. For this reaosn, it's in your best interest to put descriptors that are going to be used at the same time in the same set.
One important thing to note is that descriptor state is tracked at the command buffer level, so when we bind a descriptor set it will be local to the command buffer we bound it to. Individual commands within the command buffer will likely make use of different resources, which means our descriptor sets will likely be bound and unbound at various indices. The descriptor sets that are necessary for the largest number of commands should be bound to lower indices. The following graphic from Johannes Unterguggenberger's Vulkan slides show this very well:

We can see that Descriptor Set A is used for all four commands in the command buffer, and as such it's bound to the lowest index (0). Descriptor Set C is used for three commands (cmd2, cmd3, cmd4) and so it's bound to the next lowest index (1). You'll notice that cmd1 doesn't make use of Descriptor Set C, but it does use Descriptor Set B which takes the next lowest index available for that command, which also happens to be 1.
Taking a closer look at vkCmdBindDescriptorSets(), we can see that we have to provide a VkPipelineBindPoint argument. This will most commonly be either VK_PIPELINE_BIND_POINT_COMPUTE for the compute pipeline, VK_PIPELINE_BIND_POINT_GRAPHICS for the graphics pipeline, or VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR for the ray tracing pipeline, though there do exist other possible values.
This means that we can assign different descriptor sets to the different pipeline bind points, which exist in parallel. Based on what command is performed, it will get the resources from the descriptor set at the associated pipeline. The following graphic from Johannes Unterguggenberger's Vulkan slides show this very well:

We can see that the first command in the command buffer is a TraceRays call. Since that's a ray tracing command, we will use the descriptor sets bound to it in the ray tracing pipeline, marked in green. Similarly, the second command is a graphics command, so we use the descriptor sets bound in the graphics pipeline.
The following image from RawSourceCode.io shows an example of what the descriptor bindings for a descriptor set might look like:
You can see various resources bound at different indices within the descriptor set. This is done with what's called a Descriptor Set Layout.
Once the command buffer is submitted to the queue, with the necessary descriptor sets bound before recording the commands, the GPU begins processing the commands in submission order. It then creates the necessary resource bindings between the descriptor sets and the commands, ensuring that the commands have access to the resources they need before execution. Finally, commands start getting processed.
To actually allocate a descriptor set, we must first create a descriptor pool of sufficient size. According to the VkDescriptorPool ref page, "A descriptor pool maintains a pool of descriptors, from which descriptor sets are allocated. Descriptor pools are externally synchronized, meaning that the application must not allocate and/or free descriptor sets from the same pool in multiple threads simultaneously."
Once we have a pool, we need to create the Descriptor Set Layouts for each descriptor set that we want to allocate from the pool. The resource bindings are specified within the descriptor set layouts, as we saw in the image above.
Only once we've created a descriptor pool and a descriptor set layout can we allocate a descriptor set from the pool.\. But allocating a descriptor set isn't enough, we also need to populate it with information that references the actual resources we need. This is done by creating a VkWriteDescriptorSet for each resource in the descriptor set, and then writing those VkWriteDescriptorSets to the GPU with a call to VkUpdateDescriptorSets().
Additional resources: