First-time deploy hook for setup
Sometimes it is necessary to perform some setup steps (enabling some service, pulling some Docker images, writing some configuration file, generation of some random key) on deploy, but only necessary the first time a project is deployed. Imagine a cloud-based setup with ephemeral servers, where deployments to a blank slate are relatively common, and manually configuration is ideally avoided.
Capistrano creates the :deploy_to directory if it does not exist, and knows whether the current release is the first. It would be nice to have a task added to the flow which is invoked only if this is the first release, which the user can hook other setup tasks into.
You can easily do that yourself by capture() and the release directory variable to count the number of releases. I don't think this belongs in the core, as most of what you're describing falls into the category of "provisioning" (admittedly the line between "deployment" and "provisioning" moves back and forth depending on the team, wind direction, technology in use, etc)
I'd be glad to help you get a task written that knows if you are on the first deploy or not, help you tie that into at the very start of your deploy process, and set a variable which you can then use in later tasks.
The servers I'm deploying to are provisioned in such a way to be fairly agnostic to what projects are going to be deployed to them. A solid example is deploying server software that needs to generate a keystore, but that generation only needs to happen once, the first time the server is deployed. It would not make sense for the configuration management software to do this, as it might need to be generated by the server itself, and the directory structure might not even exist or be known before deployment.
I do see how I could do this myself, but having found myself having to do a handful of steps to set things up after the first deployment (another example is telling some monitoring service to monitor the new service I just deployed), and that the change is relatively simple, noninvasive (an empty Rake task), and possibly generally useful, I figured I'd suggest it.
Sure, a lot of these things are idempotent anyway, especially enabling and starting services, in systemd at least enabling an already enabled service is harmless, and starting an already started one is a noop.
Regarding generating files, etc - we always tend towards Makefiles/Rakefiles for builds (Even in Ruby projects) because they can create things that don't exists, then kick off these "build the things I need" tasks remotely on every deployment.
I'd be open to taking a patch for Capistrano which gave us a "cold_start" or "first_deploy" kind of variable, but i'm not sure how widely useful it is, as I said the line between app and infrastructure is blurry at best, and a moving target in any case.
To explain my usecase a bit more specifically, we have a 15-minute-ish Maven build process which generates some .jar's which are deployed to a certain directory and run in Docker containers. A separate project deploys a docker-compose configuration. All configuration files, scripts (such as a backup script), etc. are kept as part of this other project.
The backup script, for example, is called once each day by cron. It's therefore necessary to add a cron entry to call this script, but the provisioning software cannot add it (since the script doesn't exist on that host yet), nor should it presume to know that it will be there; the 'role' of a server, and what services it provides is what the Docker framework project seeks to abstract. Or, in other words, we make our provisioning as minimal as possible, keeping services and configuration as 'code'.
I would think the deployment tooling should be responsible for prepping the deployed software. But if I'm the only one for whom a "first_deploy" task/variable would be useful, then I agree that it shouldn't be part of core Capistrano. I'm not certain, either way; perhaps people will weigh in on this issue with similar usecases, perhaps not.
I don't have a strong opinion one way or the other, but I will add the "discuss!" label here to encourage other contributors and users to join the conversation.
For my projects, I just make sure my first-time tasks are idempotent and run them on every deploy.
Another thing to consider is that "first time" may mean different things to different people. For example, is it the first time the app is being deployed anywhere? Or the first time being deployed in this particular data center/geography? Or the first time on this specific server? Seems like it could depend on the task, too.
Another ticket which revolves around this issue: https://github.com/capistrano/rails/issues/118
The trouble you can see in the other ticket is that defining "first time" is really tricky. I might have a task that only runs the first time an app is deployed. It might not run if a new server is added to the cluster, even though other tasks might run in that scenario. If a :cold_boot task were added, I fear that people would start using it and be surprised by the cases when it was misleading.
Per @DylanFrese's use case, I have a number of applications that manage their crontab on deploy (a good practice, I think). I've used the Whenever Gem for most cases. Whenever will update the crontab on every deploy, overwriting the existing config for the app, but not affecting other existing crontab entries. I also have a large Perl application which uses Template Toolkit to generate the correct crontab for the environment and machine which Capistrano calls and overwrites the entire crontab on deploy. Either way, the operation happens on every deploy.
I'd suggest looking into a variant of that approach, although I understand if it doesn't work for your use case.
I think you would be better of writing the setup-tasks to either be idempotent or to check if the change has already been applied. That's how I do it anyway.
For cron, I just have a separate crontab file per app symlinked into cron.d:
desc "Install crontab"
task :cron do
on roles(:app) do
application = fetch(:application)
execute :sudo, "chown root:root #{current_path}/config/crontab"
execute :sudo, "chmod 0600 #{current_path}/config/crontab"
execute :sudo, "[ -e /etc/cron.d/#{application} ] && sudo rm -f /etc/cron.d/#{application} ; true"
execute :sudo, "ln -s #{current_path}/config/crontab /etc/cron.d/#{application}"
end
end
+1