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.
composer install
can fail because:
bower install
can fail because:
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:
fab prod update
to deployfab localhost install
to install the project on our laptop# 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
node
.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
.
chef-client
on a node
, chef will
cookbook
s from the Chef Servercookbook
s (nginx
, php
) to executecookbook
is a list of recipe
s (php[default]
, php[module_gd]
, php[module_...]
)
default
recipe is executed by defaultrecipe
define how to install a software, or a moduleCreate 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:
front
/ api
/ database
/ consumer
/ ...)database
+ postgresql
: Because it's better than mysql ☺wal_e
(https://github.com/house9/wal-e-cookbook)git
nginx
+ nginx-fastcgi
php
nodejs
python
rabbitmq
varnish
elasticsearch
(http://github.com/elasticsearch/cookbook-elasticsearch)postfix
cronwrap
(https://github.com/smaftoul/cronwrap)/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
app/logs
, app/session
)vendor
.composer dump-autoload --optimize