{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/misc/heredoc-headache/",
  "description": "Avoid here-doc pitfalls when running remote commands via SSH. Learn variable expansion gotchas and simpler alternatives for deployment scripts.",
  "path": "/misc/heredoc-headache/",
  "publishedAt": "2024-07-19T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Shell",
    "Unix",
    "DevOps",
    "GitHub"
  ],
  "textContent": "I was working on the deployment pipeline for a service that launches an app in a dedicated\nVM using GitHub Actions. In the last step of the workflow, the CI SSHs into the VM and runs\nseveral commands using a [here document] in bash. The simplified version looks like this:\n\nThe fully working version can be found in the [serve-init repo with here-doc].\n\nHere, environment variables like SSH_USER, SSH_HOST, and APP_AUTH_TOKEN are defined in\nthe surrounding local scope of the CI. The variables then get propagated to the remote\nmachine when we run the commands via here-doc.\n\nHowever, I couldn't figure out why the Docker containers weren't able to access the value of\nthe AUTH_TOKEN variable. The other variables were getting through just fine.\n\nIt turns out, export AUTH_TOKEN=$AUTH_TOKEN within the here-doc block, doesn't export the\nvariable in the remote shell. So this doesn't do what I thought it would:\n\nI was expecting it to print:\n\nBut instead, it just prints:\n\nSo export FOO=bar in the here-doc block doesn't set the variable in the remote shell. One\nsolution is to set it before the block like this:\n\nThis prints:\n\nSo, in the CI pipeline, we could do the following to propagate the environment variable from\nlocal to the remote machine:\n\nThis will print the value of the environment variable on the remote machine correctly.\nHowever, this doesn't set the value in the remote shell's environment. If you SSH into the\nremote machine and try to print the variable's value, you'll see nothing gets printed. The\nprevious command only passes the value to the remote machine temporarily and doesn't set it\npermanently in the remote shell.\n\nTo fix it, you could pipe the value into a file and load it in the remote shell like this:\n\nHere, echo \\$FOO instead of echo $FOO ensures that the shell expansion is done on the\nremote machine, not on the local. This allows us to know that the environment variable has\nbeen set in the remote shell correctly.\n\nMaybe the behavior makes sense, but it still broke my mental model.\n\nSo I decided to get rid of here-doc in the pipeline altogether and went with this:\n\nIt [works without here-doc]!\n\nOne thing to keep in mind with the second approach is that if you need to run any expanded\ncommands, you'll need to defer it with a backslash so that it's run on the remote machine,\nnot on the local:\n\nWithout the backslash, the $(...) will be expanded on the local machine, which is not\ndesirable here. The backslash defers it so that it runs on the remote instead.\n\n\n\n\n[here document]:\n    https://tldp.org/LDP/abs/html/here-docs.html\n\n[serve-init repo with here-doc]:\n    https://github.com/rednafi/serve-init/blob/7232c55c9aa3a6c34c5da6aeb9d14afc88d9aa0e/.github/workflows/ci.yml#L86-L115\n\n[works without here-doc]:\n    https://github.com/rednafi/serve-init/blob/54b9b0fc94030eb4b9749fd4a5823a8867545f6a/.github/workflows/ci.yml#L86-L113",
  "title": "Here-doc headache"
}