It turns out that (with reasonably new versions of OpenSSL) generating a private key and PKCS#10 Certificate Signing Request is not as difficult as it first appears.
All in one
Both key and CSR generation can be done in one command. There are no prompts, and you don't need to provide input via a config file.
$ openssl req -new -newkey rsa:3072 -keyout foo.rsa -nodes -subj /CN=foo.example.com -out foo.req
or
$ openssl req -new -newkey ed25519 -keyout foo.rsa -nodes -subj /CN=foo.example.com -out foo.req
Separate commands
Private Key
To generate an RSA private key:
$ openssl genpkey -out foo.key -algorithm RSA
To specify the key size, add -pkeyopt rsa_keygen_bits:4096.
or for an ED25519 private key:
$ openssl genpkey -out foo.key -algorithm ED25519
Certificate Signing Request
To generate a CSR:
$ openssl req -new -key foo.key -subj /CN=foo.example.com -out foo.req
There are no prompts, and you don't need to provide input via a config file.
To add a DNS-ID to the certificate, add -addext 'subjectAltName = DNS:foo.example.com'.
Add -text if you want a human-readable form of the CSR to be printed to stdout. Use this while experimenting with other options to check that they are adding the correct bits to the CSR.