On Github lyrixx / SFCon-Warsaw2013-Automation
Grégoire Pineau - SymfonyCon - Warsaw 2013
Install
$ git clone git@mycompany.com:project
Or update
$ git fetch -t
Then
git checkout -q -f v1.0.0 php symfony build:all --all --no-confirmation php symfony plugin:install php symfony projects:fix-perms php symfony clear:cache rsync -azCcv --delete --dry-run . www-data@project.com:/var/www/project
git checkout -q -f v1.0.0 php composer.phar install --optimize-autoloader bowser install grunt build php app/console assetic:dump rsync -azCcv --delete --dry-run . prod:/var/www/project ssh prod php /var/www/project/app/console --env="prod" clear:cache ssh prod php /var/www/project/app/console --env="prod" doctrine:migrations:migrate
Actually, all theses issue are not related to manual deployment. But theses can be easily catch and treat with automation.
At some point you have to deal with reality. You can postpone automation for a long time and make your life really, really difficult. But at some point your life goes from difficult to impossible.
Phil Dibowitz, Production Engineer at Facebook
Hide features before they are totally ready.
In our templates:
{% if is_granted('FEATURE_SECRET') } <a href="#..."></a> {% endif %}
In our controllers:
public function secretAction() { if (!$this->get('security.context')->isGranted('FEATURE_SECRET')) { throw new AccessDeniedException('You are not allowed to see this feature.'); } }
<!-- service.xml --> <service id="awesome.feature_hierarchy.voter" class="%security.access.role_hierarchy_voter.class%"> <argument type="service" id="security.role_hierarchy" /> <argument>FEATURE_</argument> <tag name="security.voter" /> </service>
// class User implement UserInterface public function getRoles() { if ($this->isAdmin) { return array('ROLE_ADMIN', 'FEATURE_BETA'); } return array('ROLE_USER', 'FEATURE_PROD'); }
# security.yml role_hierarchy: ROLE_ADMIN: ROLE_USER FEATURE_BETA: FEATURE_PROD, FEATURE_SECRET FEATURE_PROD: FEATURE_FOO, FEATURE_BAR
# security.yml role_hierarchy: ROLE_ADMIN: ROLE_USER - FEATURE_BETA: FEATURE_PROD, FEATURE_SECRET - FEATURE_PROD: FEATURE_FOO, FEATURE_BAR + FEATURE_BETA: FEATURE_PROD + FEATURE_PROD: FEATURE_FOO, FEATURE_BAR, FEATURE_SECRET
More information: Feature Flags With Symfony2
Just copy what you used to do to deploy inside a shell script
Fabric is a Python (2.5 or higher) library and command-line tool for streamlining the use of SSH for application deployment or systems administration tasks.
So it is:
# fabfile.py # ... def install(): sudo('mkdir -p ' + path) with cd(path): sudo('git clone ' + repo + ' .') sudo('composer install --dev') sudo('php app/console doctrine:database:create') sudo('php app/console doctrine:migrations:migrate --no-interaction') def update(): with cd(path): sudo('git fetch') sudo('git reset --hard origin/prod') sudo('composer install') sudo('php app/console doctrine:migrations:migrate --no-interaction')
Then:
# deploy.rb set :application, "My App" set :deploy_to, "/var/www/my-app.com" set :domain, "my-app.com" set :scm, :git set :repository, "ssh-gitrepo-domain.com:/path/to/repo.git" role :web, domain role :app, domain, :primary => true set :use_sudo, false set :keep_releases, 3
Then run:
cap deploy
Chef is built to address the hardest infrastructure challenges on the planet. By modeling IT infrastructure and application delivery as code, Chef provides the power and flexibility to compete in the digital economy.
Create and configure lightweight, reproducible, and portable development environments.
Try it with:
$ vagrant box add base http://files.vagrantup.com/lucid32.box $ vagrant init $ vagrant up
Chef (chef-client) need to be installed on the target machine. (Prod, preprod, vm, ...)
$curl -L https://www.opscode.com/chef/install.sh | bash
Then run chef-client on the node you want to update
But chef can also work in a standalone mode with chef-solo. So in this case, chef-solo doest not need a chef server. Every cookbook should be inside the node.
Create a new VM with vagrant
$ vagrant box add saucy64 http://cloud-images.ubuntu.com/vagrant/saucy/current/saucy-server-cloudimg-amd64-vagrant-disk1.box $ vagrant init $ sed -i 's/"base"/"saucy64"/' Vagrantfile
If our host is a 32bit plateform and the guest is a 64bits plateform, add this to the Vagrantfile:
config.vm.provider :virtualbox do |vb| vb.customize ["modifyvm", :id, "--ostype", "Ubuntu_64"] end
$ vagrant up $ vagrant ssh
To make things easier, we will use chef-solo. So we will not use a Chef Server.
$ sudo su $ cd $ curl -L https://www.opscode.com/chef/install.sh | bash
Note: Chef is already installed in this box.
Download opscode's skeleton:
$ wget http://github.com/opscode/chef-repo/tarball/master $ tar -zxf master && mv opscode-chef-repo* chef-repo && rm master $ cd chef-repo
It looks like:
chef-repo ├── certificates/ ├── chefignore ├── config/ ├── cookbooks/ <--- most important folder ├── data_bags/ ├── environments/ ├── LICENSE ├── Rakefile ├── README.md └── roles/
$ knife cookbook create fortune
fortune ├── attributes ├── definitions ├── files │ └── default ├── libraries ├── metadata.rb ├── providers ├── README.md ├── recipes │ └── default.rb <--- most important file ├── resources └── templates └── default
# recipes/default.rb include_recipe "apache2" include_recipe "mysql::client" include_recipe "mysql::server" include_recipe "mysql::ruby" include_recipe "php" include_recipe "php::module_mysql" include_recipe "apache2::mod_php5" apache_site "default" do enable true end mysql_database fortune do connection ({:host => 'localhost', :username => 'root', :password => node['mysql']['server_root_password']}) action :create end
-mysql_database 'fortune' do +mysql_database node['fortune']['database'] do connection ({:host => 'localhost', :username => 'root', :password => node['mysql']['server_root_password']}) action :create end
Let's create an attribute file:
# attributes/default.rb default["fortune"]["database"] = "fortune" default["fortune"]["ga"] = "GA_123456789"
Now, you can override attributes:
/var/www/insight/ ├── current -> /var/www/insight/releases/d3fd36569dffda711a2770ea1ccae28d54fb9c11 ├── releases │ ├── 43d7d8f9aae517d45c8ca57d96d11e0648171cf9 │ ├── 52c1593bd2e6ed9496ea063d1d94aa6621b39c37 │ ├── 981a27a9932767947353d2d8567ca1b0a3f87b13 │ ├── 9d2f7873d2263f44da730efae9d6dcdbe0ffd430 │ └── d3fd36569dffda711a2770ea1ccae28d54fb9c11 └── shared ├── app ├── cached-copy └── vendor
We use the application cookbook.
application node[cookbook_name]['app_name'] do revision node[cookbook_name]['deploy_revision'] env_vars_composer = {} env_vars_composer["DATABASE_NAME"] = node[cookbook_name]['dbname'] env_vars_composer["DATABASE_USER"] = node[cookbook_name]['dbuser'] env_vars_composer["DATABASE_PASSWORD"] = node[cookbook_name]['dbpassword'] # ... # A this point, the code is not yet checkouted before_deploy do %w(app/sessions app/logs vendor).each do |dir| directory "#{shared_path}/#{dir}" do owner new_resource.owner action :create recursive true end end end # A this point, the code is checkouted, but not yet deployed before_migrate do template "#{release_path}/web/maintenance-dist.html" do source "maintenance.html.erb" user new_resource.owner mode 00644 variables( 'sitename' => node[cookbook_name]['app_name'].capitalize ) end file "#{release_path}/web/app_dev.php" do action :delete end execute "bower install" do environment({ 'HOME' => node['etc']['passwd']['insight']['dir'], 'GIT_SSH' => "#{node[cookbook_name]['app_path']}/deploy-ssh-wrapper" }) cwd release_path user new_resource.owner end bash "copy shared vendors into current release" do code <<-EOH cp -pa #{new_resource.shared_path}/vendor #{new_resource.release_path} EOH only_if { ::File.directory?("#{new_resource.shared_path}/vendor") } user new_resource.owner end execute "php /opt/composer.phar install --dev --prefer-source --no-interaction --optimize-autoloader" do environment env_vars_composer cwd release_path user new_resource.owner end execute "php app/console assetic:dump --env=prod --no-debug" do cwd release_path user new_resource.owner end bash "migrate database if needed" do user new_resource.owner cwd release_path code <<-EOH MIGRATION_NEEDED=0 DEFAULT_CONNECTION=$(app/console doctrine:migrations:status --show-versions | grep "not migrated" | wc -l) if [ "$DEFAULT_CONNECTION" -ne "0" ]; then MIGRATION_NEEDED="1" fi if [ "$MIGRATION_NEEDED" -ne "0" ]; then cp web/maintenance-dist.html #{node[cookbook_name]['app_path']}/current/web/maintenance.html app/console doctrine:migrations:migrate --no-interaction --env=prod --no-debug EXIT_CODE=$? rm #{node[cookbook_name]['app_path']}/current/web/maintenance.html echo '#{node[cookbook_name]['metric_prefix']}.chef.application.db-migrated.count:1|c' | nc -w 1 -u #{statsd_host} 8125 fi exit $EXIT_CODE EOH only_if 'app/console list --raw | grep "doctrine:migrations:status"', :user => new_resource.owner, :cwd => release_path end end symlinks({ 'app/sessions' => 'app/sessions', 'app/logs' => 'app/logs', }) before_restart do service "php5-fpm" do action :restart end end after_restart do bash "Copy installed vendor to shared vendor" do code <<-EOH cp -pa #{new_resource.release_path}/vendor #{new_resource.shared_path} EOH end end end