Here is how I solve basically the same issue:
I am using a standard bootstrap process. It includes exactly one cookbook that I call “main”.
The main::default recipe contains the business logic that decides which recipes, roles, etc. to add to the node - in my case, simply a big case statement that keys off the node name (my environment is small enough to do that). In your case, this recipe would parse out the node name (or the FQDN - which may or may not be the same) or whatever other values are appropriate.
The main::default recipe also contains some standard code that applies to all nodes, in my case things like making sure SELinux is enabled, SSH is locked down, etc.
So on the first chef run (during bootstrap), your system will only be prepped with some basic functionality. On the second chef run, the “real” run list (which still includes the “main” cookbook) will be executed.
Our values: Privacy, Liberty, Justice