README.rdoc in deplomat-0.1.13 vs README.rdoc in deplomat-0.2.0

- old
+ new

@@ -1,3 +1,262 @@ -= deplomat +# Deplomat - a stack agnostic deployment system +Deplomat is a stack agnostic deployment system that uses bash and ssh commands. +The purpose of Deplomat is to be a suitable deployment system for all and easily scale from +a one-man operation to big teams and multiple servers. -Stack agnostic deployment system that uses bash and ssh commands. +How does it work? +----------------- + * It uses SSH to send commands to remote servers and can also execute scripts on a local machine. + * The SSH connection is opened in such a way that it is persistent, so it's fast to execute multiple commands over it; + * The deployment script is a simple ruby script. You just create your own methods and then call them. + +Let's take a look at an example script, step by step. For the purposes of this tutorial, we'll simplify things. +The process will resemble a typical web-app deployment, but won't be too specific or complicated. + + #!/usr/bin/env ruby + require 'rubygems' + require 'deplomat' + + $env = ARGV[0] || "staging" + $local = Deplomat::LocalNode.new(path: "/home/user/myproject") + $server = Deplomat::RemoteNode.new host: "myproject.com", port: 22, user: "deploy" + $branch = ARGV[1] || $env + $app_name = "myproject" + $project_dir = "/var/www/#{$app_name}" + $release_dir = Time.now.to_i + +We've defined a bunch of global variables here. They're global to easily distinguish them from unimportant things, but they might as well +have been regular local variables. We have also created a `LocalNode` object - it is used to run commands on the local machine; and a `RemoteNode` +object, which, you guessed it, is used to run commands on a remote machine. You can theoretically have as many different remote nodes +as you want, but in our example we'll just have one. + +Ok, now let's write the actual deployment code. + + # Create the release dir + $server.create_dir("#{$project_dir}/#$env/releases/#{$release_dir}") + + # Upload to the remote dir + $server.upload("#{$local.current_path}/*", "#{$project_dir}/#$env/releases/#{$release_dir}/") + + # cd to the release dir we've just created and uploaded, we'll do things inside it. + $server.cd("#{$project_dir}/#$env/releases/#{$release_dir}") + +Here, we used standard `Deplomat::RemoteNode` methods. First, we used `Deplomat::RemoteNode#create_dir` which +ran a `"mkdir -p /home/user/myproject/staging/releases/[timestamp]"` command on the server for us. Then we used the +`Deplomat::RemoteNode#upload` which uploaded all the files from our project directory to the server. And, finally, +we've changed current directory on the server to the one we've just created. So far so good, but we now need +to do a few things on the server before we can restart our webapp: + + $server.create_symlink("#{$project_dir}/#$env/shared/config/database.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/config/secrets.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/config/cable.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/config/redis.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/log", "./") + $server.create_symlink("#{$project_dir}/#$env/shared/public/uploads", "public/") + +Here, we created symlinks to the files and directories that persist across deployments. For example, the files users +upload should not evaporate with each new release and so we put them in the `/var/www/myproject/staging/shared/public/uploads` +directory and then symlink them to the release directory. + +For the final steps, we need to migrate the database, instruct the server to restart and symlink the release directory: + + # Migrate DB. Our migration script requires a login shell to work properly, + # so we instruct deplomat to run it using a login shell. + $server.execute("bin/migrate_db #{$env}", login_shell: true) + + # Restart the server + $server.execute("mkdir -p tmp") + $server.touch("tmp/restart.txt") + + if $server.file_exists?("#{$project_dir}/#$env/current") + $server.mv("#{$project_dir}/#$env/current", "#{$project_dir}/#$env/previous") + end + $server.create_symlink("#{$project_dir}/#$env/releases/#{$release_dir}", "#{$project_dir}/#$env/current") + +You can see how we used `#file_exists?`, `#touch`, `#mv` methods here. Those are also standard methods of `Deplomat::Node`. +Notice how we checked if `"#{$project_dir}/#$env/current"` exists first, because it might not exist on our first deployment. +However if it does exist, it's wise to rename this symlink into `previous` so we can later undo the deployment easily by renaming that +symlink back to `current`. + +Our script is ready, now you can run `./deploy` (assuming it's in the root dir of your project and you've made it executable). + + +Adding more structure with methods +---------------------------------- +Our script above is ok, however as your project grows you'll discover you'd want to add more structure to it. It +makes sense to group some actions into methods, so you can have something like this: + + #!/usr/bin/env ruby + require 'rubygems' + require 'deplomat' + require 'deployment_steps' # << THIS IS WHERE WE PUT OUR METHODS THAT WE USE BELOW + + $env = ARGV[0] || "staging" + $local = Deplomat::LocalNode.new(path: "/home/user/myproject") + $server = Deplomat::RemoteNode.new host: "myproject.com", port: 22, user: "deploy" + $branch = ARGV[1] || $env + $app_name = "myproject" + $project_dir = "/var/www/#{$app_name}" + $release_dir = Time.now.to_i + + create_release_dir_and_upload! + create_symlinks! + migrate_db! + restart_server! + +This script looks much nicer. We moved deployment code into separate methods into a file we called +`deployment_steps.rb` and it looks like this: + + def create_release_dir_and_upload! + # Create the release dir + $server.create_dir("#{$project_dir}/#$env/releases/#{$release_dir}") + # Upload to the remote dir + $server.upload("#{$local.current_path}/*", "#{$project_dir}/#$env/releases/#{$release_dir}/") + # cd to the release dir we've just created and uploaded, we'll do things inside it. + $server.cd("#{$project_dir}/#$env/releases/#{$release_dir}") + end + + def create_symlinks! + $server.create_symlink("#{$project_dir}/#$env/shared/config/database.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/config/secrets.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/config/cable.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/config/redis.yml", "config/") + $server.create_symlink("#{$project_dir}/#$env/shared/log", "./") + $server.create_symlink("#{$project_dir}/#$env/shared/public/uploads", "public/") + end + + def migrate_db! + # Migrate DB. Our migration script requires a login shell to work properly, + # so we instruct deplomat to run it using a login shell. + $server.execute("bin/migrate_db #{$env}", login_shell: true) + end + + def restart_server! + # Restart the server + $server.execute("mkdir -p tmp") + $server.touch("tmp/restart.txt") + + if $server.file_exists?("#{$project_dir}/#$env/current") + $server.mv("#{$project_dir}/#$env/current", "#{$project_dir}/#$env/previous") + end + $server.create_symlink("#{$project_dir}/#$env/releases/#{$release_dir}", "#{$project_dir}/#$env/current") + end + +Notice, we were able to use the same `$server`, `$release_dir`, `$env` and some other variables inside that file precisely +because we made them global. While global vars are not great for large systems, deployment scripts such as this +one can take advantage of them without complicating things too much. + +Deployment requisites +--------------------- +Sometimes you need to run a piece of code while deploying the project, but you only need to run it once - that is, +on one deployment after which it will never be run again. Much like Ruby On Rails runs migrations once and then only runs +new migrations when necessary. + +An example would be when you need to add something to the config file. While you can do it manually by logging into your server +and editing the config file, it's much more desirable to automate that process, because then you won't forget to do it. + +Deplomat has a special feature called Deployment requisites, which allow you to + + * Write special ruby scripts called "requisites", which have access to all the variables and features your script has access to; + * Enumarate the tasks that are being run on each deployment and keep track of them by using a special requisite counter (a file created on the server); + * Assign each task to be run before or after a particular deployment method in your script; + +Let's see how we do that. The first step would be to replace method calls with a call to `#add_task` in your deployment script: + + #!/usr/bin/env ruby + require 'rubygems' + require 'deplomat' + require 'deployment_steps' # << THIS IS WHERE WE PUT OUR METHODS THAT WE USE BELOW + + $env = ARGV[0] || "staging" + $local = Deplomat::LocalNode.new(path: "/home/user/myproject") + $server = Deplomat::RemoteNode.new host: "myproject.com", port: 22, user: "deploy" + $branch = ARGV[1] || $env + $app_name = "myproject" + $project_dir = "/var/www/#{$app_name}" + $release_dir = Time.now.to_i + + add_task :create_release_dir_and_upload! + add_task :create_symlinks! + add_task :migrate_db! + add_task :restart_server! + + execute_tasks! + +This script's behavior is equivalent to the one we had previously. We can make it shorter though by writing: + + # --- top part with requires and global var settings ommited --- + + add_task :create_release_dir_and_upload!, :create_symlinks!, :migrate_db!, :restart_server! + execute_tasks! + + +We'll add two lines of code that will read the current requisite number from the server and then load the requisites from +a local directory called `./deployment_requisites/`. You can change the defaults by passing an additional `path:` argument +to the `load_requisites!` method, but we're not going to do it here. + + # --- top part with requires and global var settings ommited --- + + # This method is kind of a callback, it's called automatically after every #before_task + # or #after_task call. We need to define it manually here, + # otherwise requisite number will be stuck on the same number + # and never updated. + def update_requisite_number!(n) + $server.update_requisite_number!(n) + end + + # read current requisite number from the server + req_n = $server.current_requisite_number + + # load requisites + load_requisites!(req_n) + + add_task :create_release_dir_and_upload!, :create_symlinks!, :migrate_db!, :restart_server! + execute_tasks! + +The `#execute_tasks!` method will now not only run your methods, but also run the requisites associated with each task. Now +you might ask, where's the actual code for the requisites? Let's create two files in the `./deployment_requisites/` dir on +your local machine: + + # ./deployment_requisites/1_add_var1_to_config_file.rb + before_task(:migrate_db, 1) do + $server.execute("cat 'var1: true' >> config/secrets.yml") + end + + # ./deployment_requisites/2_add_var2_to_config_file.rb + after_task(:create_release_dir_and_upload!, 2) do + $server.execute("cat 'var2: true' >> config/secrets.yml") + end + +Notice two things here: + + * Filenames start with a number. That's very important: each new requisite file should get a number that's larger than the previous number; + * When calling `before_task` or `after_task` the second argument should always be the number that equals that + consecutive number in the file name; + * You can have only one call to `before_task` or `after_task` per requisite file. + +When you deploy, that's what's going to happen: + + * Deplomat will check for the requisite number in the counter file at `"#{@current_path}/.deployment_requisites_counter"` (that's + the default location, can be changed by passing an additional argument to `Deplomat::Node#current_requisite_number` and + `Deplomat::Node#update_requisite_number`, see sources); + + * Use that fetched counter number to run only requisites with the numbers that are higher; + + * Update that number upon each requisite script completion (so if there's an error somehwere, we're still left at the exact requisite number + that ran successfully). The update is done by calling the `#update_requisite_number!` we defined in our deployment script. + + +Where can I find the list of all methods? +---------------------------------------- +For now, because this is a relatively small library, you're better off browsing the sources, specifically: + + * [Deplomat::Node](lib/deplomat/node.rb) + * [Deplomat::LocalNode](lib/deplomat/local_node.rb) + * [Deplomat::RemoteNode](lib/deplomat/remotenode.rb) + +TODO +----- + + * `Deplomat::Node` methods to read and update .yml files easily (usecase: update config files in requisite scripts). + * Include `git-archive-all` script + * Include scripts for default Ruby On Rails deployments (perhaps as a separate repo)