Archivo del Autor: Gustavo Pilla
Docker Swarm + Traefik 2.2
Configuración de Docker Swarm, el archivo traefik-stack.yaml tiene la configuaración básica necesaria para hacer funcionar al proxy reverso, y un ejemplo con un servicio sonso en whoami-stack.yaml.
version: '3' | |
services: | |
reverse-proxy: | |
image: traefik:v2.2 | |
command: | |
- --api.insecure=true | |
- --api.dashboard=true | |
- --providers.docker=true | |
- --providers.docker.swarmMode=true | |
- --providers.docker.exposedbydefault=false | |
- --providers.docker.network=proxy | |
- --entrypoints.http.address=:80 | |
- --entrypoints.https.address=:443 | |
ports: | |
# The HTTP port | |
- "80:80" | |
- "443:443" | |
volumes: | |
# So that Traefik can listen to the Docker events | |
- /var/run/docker.sock:/var/run/docker.sock | |
networks: | |
- proxy | |
deploy: | |
placement: | |
constraints: | |
- node.role == manager | |
labels: | |
- "traefik.enable=true" | |
- "traefik.http.routers.api.rule=Host(`dashboard.traefik.localhost`)" | |
- "traefik.http.routers.api.service=api@internal" | |
- "traefik.http.routers.api.entrypoints=http" | |
- "traefik.http.services.api.loadbalancer.server.port=80" | |
networks: | |
proxy: | |
external: true |
version: '3' | |
services: | |
whoami: | |
# A container that exposes an API to show its IP address | |
image: containous/whoami | |
networks: | |
- proxy | |
deploy: | |
labels: | |
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)" | |
- "traefik.enable=true" | |
- "traefik.http.routers.whoami.entrypoints=http" | |
- "traefik.http.services.whoami.loadbalancer.server.port=80" | |
networks: | |
proxy: | |
external: true |
Teniendo los archivos de configuración listos, debemos ejecutar los siguientes comandos para levantar el cluster.
1 2 3 |
docker network create --driver=overlay proxy docker stack deploy -c traefik-stack.yaml traefik docker stack deploy -c whoami-stack.yaml whoami |
Ahora expliquemos un poco…
Como habran notado la magia para publicar servicios realmente esta en la siguiente sección, en los labels que le pongo al servicio dentro de deploy:
1 2 3 4 5 6 |
deploy: labels: - "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)" - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=http" - "traefik.http.services.whoami.loadbalancer.server.port=80" |
En la linea "traefik.http.routers.whoami.rule=Host(whoami.localhost)"
lo que hacemos decirle a que URL va a responder.
En la linea "traefik.enable=true"
le informamos a Traefik que nos interesa publicar este servicio. Esto es necesario ya que en la configuración de Traefik pusimos de que no queremos que tome todos los servicios publicados por default.
Ahora cuando dice "traefik.http.routers.whoami.entrypoints=http"
le estamos diciendo que debe escuchar el puerto 80 (lo configuramos en el archivo traefik-stack.yaml:16) en el host y por último "traefik.http.services.whoami.loadbalancer.server.port=80"
nos dice que puerto debemos mapear del contenedor/servicio.
Presentación SIU-Guaraní Dockerizado en UNJU 2019
Buenas… recibi muchos pedidos de la presentación, y varios llegaron desde mi pobre abandonado blog… entonces la comparto por aca.
Como implementar Doctrine Filter en Symfony 4
Esto es un poco de mi experiencia. En este caso vamos a poder filtrar entidades por una Cuenta.
Cada usuario va a pertencer a una Cuenta (Account.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// src/App/Entity/User.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="fos_user") */ class User extends BaseUser { /** * @ORM\ManyToOne(targetEntity="Account", inversedBy="users", cascade={"persist"}) * @ORM\JoinColumn(name="account_id", referencedColumnName="id") */ protected $account; /** * Set account * * @param \App\Entity\Account $account * * @return User */ public function setAccount(\App\Entity\Account $account = null) { $this->account = $account; return $this; } /** * Get account * * @return \App\Entity\Account */ public function getAccount() { return $this->account; } } |
Aca definimos la cuenta, de vago no copie todos los setters y getters… usen la imaginacion… o el bin/console make:entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * Account * * @ORM\Table(name="account") * @ORM\Entity */ class Account { // ... } |
Luego, si queremos filtrar los clientes por cuenta… para no repetir código utilizamos un Trait…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/Entity/Client.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use App\Entity\Traits\AccountAwareEntity; /** * @ORM\Entity */ class Client { use AccountAwareEntity; // ... } |
Aquí el Trait… como ven muy simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// src/Entity/Traits/AccountAwareEntity.php namespace App\Entity\Traits; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; trait AccountAwareEntity { /** * @ORM\ManyToOne(targetEntity="App\Entity\Account") * @ORM\JoinColumn(name="account_id", referencedColumnName="id") */ private $account; /** * Set account * * @param \AppBundle\Entity\Account $account * * @return Venta */ public function setAccount(\App\Entity\Account $account = null) { $this->account = $account; return $this; } /** * Get account * * @return \AppBundle\Entity\Account */ public function getAccount() { return $this->account; } public function shouldBeFilteredByAccount() { return true; } } |
Ahora arrancamos con la magia… creamos un SQLFilter donde se agrega la logica.. de como vamos a filtrar y tambien importante cuando… el cuando lo sabemos con un Helper que definimos más abajo…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// src/Filter/AccountFilter.php namespace App\Filter; use Doctrine\ORM\Mapping\ClassMetaData; use Doctrine\ORM\Query\Filter\SQLFilter; use App\Helpers\AccountAwareHelper; class AccountFilter extends SQLFilter { public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ('cli' === PHP_SAPI && is_null($this->getParameter('id'))) { return '1 = 1'; } $class = $targetEntity->reflClass->name; if (AccountAwareHelper::classIsAccountAware($class)) { $fieldName = 'account_id'; $accountId = $this->getParameter('id'); $query = sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $accountId); return $query; } return '1 = 1'; } |
Para que esto funcione, debemos definir el «Id» a filtrar en el código anterior… esto se hace con un Configurator, el cual no es otra cosa que un EventListener al que se le hacemos un inject de Doctrine y el manejo de la sesión de usuarios. Tomando el usuario conectado, le preguntamos a que cuenta pertenece, tomamos el ID y lo seteamos al filtro de Doctrine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// src/Filter/Configurator.php namespace App\Filter; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Doctrine\Common\Persistence\ObjectManager; class Configurator { protected $em; protected $tokenStorage; public function __construct(ObjectManager $em, TokenStorageInterface $tokenStorage) { $this->em = $em; $this->tokenStorage = $tokenStorage; } public function onKernelRequest() { if ($user = $this->getUser()) { if (!in_array('ROLE_SUPER_ADMIN', $user->getRoles())) { $filter = $this->em->getFilters()->enable('account_filter'); $filter->setParameter('id', $this->getUser()->getAccount()->getId()); } else { try { // $this->em->getFilters()->enable('account_filter'); // $this->em->getFilters()->disable('account_filter'); } catch (\Expcetion $e) { } } } else { $this->em->getFilters()->enable('account_filter'); $this->em->getFilters()->disable('account_filter'); } } private function getUser() { $token = $this->tokenStorage->getToken(); if (!$token) { return null; } $user = $token->getUser(); if (!($user instanceof UserInterface)) { return null; } return $user; } } |
Este Helper es para reconocer si la entidad debe ser filtrada por el AccountFilter o solo dejarla pasar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// src/Helpers/AccountAwareHelper.php namespace App\Helpers; class AccountAwareHelper { public static function isAccountAware($object) { $class = get_class($object); return self::classIsAccountAware($class); } public static function classIsAccountAware($class) { if (in_array('App\Entity\Traits\AccountAwareEntity', self::classUses($class))) { return true; } if (\is_subclass_of($class, 'App\Entity\AccountAware')) { return true; } return false; } private function classUses($class, $autoload = true) { $traits = []; do { $traits = array_merge(class_uses($class, $autoload), $traits); } while ($class = get_parent_class($class)); foreach ($traits as $trait => $same) { $traits = array_merge(class_uses($trait, $autoload), $traits); } return array_unique($traits); } } |
Y aca las configuraciones… primero para que lo reconozca Doctrine al Filter.
1 2 3 4 5 6 7 |
# config/packages/doctrine.yaml doctrine: orm: account_filter: class: App\Filter\AccountFilter enabled: true |
Y ahora la definición para que se reconozca el EventListener.
1 2 3 4 5 6 7 8 |
# config/services.yaml app.filter.account.configurator: class: 'App\Filter\Configurator' arguments: - "@doctrine.orm.entity_manager" - "@security.token_storage" tags: - { 'name': kernel.event_listener, 'event': kernel.request, priority: 5 } |
That’s all folks.
PHP 7.2 + mcrypt en Mac OSX Sierra
Como putee por encontrar esto, ahora lo comparto, y me lo guardo… como siempre.
Supongo que tienen instalado PHP con Homebrew, ahora tenemos que hacer funcionar mcrypt por alguna dependencia de un paquete no actualizado.
Bueno, PHP no lo tiene como paquete oficial, pero esta en PECL. El otro problema es que hay que tener instalada la libreria mcrypt para que cuando PECL lo compile encuentre las cabeceras.
Tampoco es dificil, solo no esta muy bien documentado que digamos.
1 |
brew install mcrypt |
luego… hacemos la magia de PECL
1 |
pecl install mcrypt-1.0.1 |
Somos felices…
PHP 7+ en Debian Jessie y Stretch
Es fácil no tengo ganas de escribir, aca esta la papa:
Por lo que leí es un maintener de Debian, así que tiene un buen nivel de confiabilidad (no dejen de chequearlo ustedes).
Para sacar andando los repos… todo lo que necesitan es el siguiente README:
https://packages.sury.org/php/README.txt
Ok, me compadeci y agregue una lineas extras…
PHP CLI ya levanto todo bien desde el apt-get install.
1 2 3 4 |
sudo apt-get install php7.1 php7.1 php7.1-mysql php7.1-soap php7.1-cli php7.1-curl php7.1-xml php7.1-zip php7.1-mcrypt php7.1-intl php7.1-mbstring php-memcache sudo a2dismod php7.0 sudo a2enmod php7.1 sudo systemctl restart apache2 |
Nos vemos…
Instalar Redmine usando Docker
La instalación de Redmine suele ser medio un dolor de cabeza si no estas en el palo de Ruby.
No quise sufrir está vez y lo levante desde un container de Docker especificamente de Redmine.
Basándome en la documentación del link anterior, mejore algunas configuraciones para extraer los datos que maneja el sistema de adentro del contenedor (no esta bueno dejar datos que uno quiere persistir dentro del contenedor).
¿Como lo instale?
Primero necesito levantar una base de datos:
1 |
docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234567890 -e MYSQL_DATABASE=redmine -v /usr/src/redmine/mysql:/var/lib/mysql mysql |
¿Que tiene diferente el comando con el del tutorial original?
- Saco el directorio /var/lib/mysql para persistirlo si borro el contenedor.
- Público el puerto 3066 para poder conectarme desde afuera de los contenedores al MySQL.
Sobre este ultimo bullet, es necesario hacer un cambio para poder conectarse desde afuera:
1 2 |
docker exec -it redmine bash mysql -u root |
Accedo al contenedor, abro el cliente MySQL local y ejecuto el siguiente comando:
1 |
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '1234567890'; |
En nuestro caso teníamos una base vieja, por lo que cargue el backup dentro del MySQL antes de arrancar el contenedor de Redmine para que en lugar de crear una base nueva, intente migrarla, no voy a explicar el como, supongo que lo saben hacer.
Sigo con la instalación levantando el contenedor de Docker.
1 |
docker run -d --name redmine -v /usr/src/redmine/config/configuration.yml:/usr/src/redmine/config/configuration.yml -v /usr/src/redmine/config/initializers/saml.rb:/usr/src/redmine/config/initializers/saml.rb -v /usr/src/redmine/files:/usr/src/redmine/files -v /usr/src/redmine/plugins:/usr/src/redmine/plugins -v /etc/localtime:/etc/localtime:ro -p 3000:3000 --link mysql:mysql redmine:latest |
¿Que tiene diferente el comando con el del tutorial original?
- Cargo la configuración desde afuera /usr/src/redmine/config/configuration.yml
- Utilizamos SAML, por lo que cargo una configuración especifica /usr/src/redmine/config/initializers/saml.rb
- Extraigo el directorio plugins por fuera del container, para poder persitir
- Extraigo el directorio files por fuera del container, para poder persitir
- Público el puerto 3000 para poder conectarme desde afuera.
Luego con un apache en modo proxy, transformo el puerto 80 en 3000. (Ahora que lo pienso podría no ser necesario ya que con el mismo Docker lo puedo transformar de 3000 a 80)
¿Como actualizar?
Los pasos básicamente son:
- Actualizo la imagen
- Paro el contenedor
- Borro el contenedor
- Creo nuevamente el contenedor (pero está vez lo hace con la imagen nueva)
La versión en comandos es:
1 2 3 4 |
docker pull redmine docker stop redmine docker rm redmine docker run -d --name redmine -v /usr/src/redmine/config/configuration.yml:/usr/src/redmine/config/configuration.yml -v /usr/src/redmine/config/initializers/saml.rb:/usr/src/redmine/config/initializers/saml.rb -v /usr/src/redmine/files:/usr/src/redmine/files -v /usr/src/redmine/plugins:/usr/src/redmine/plugins -v /etc/localtime:/etc/localtime:ro -p 3000:3000 --link mysql:mysql redmine:latest |
¿Si está caído?
Proba si realmente está caído.
1 |
docker stats |
Y si lo está:
1 |
docker start redmine |
¿Y si quiero ver algo dentro del docker?
1 |
docker exec -it redmine bash |
Con esto nos da una consola bash dentro del container
Implementando g3w3 (SIU-Guaraní para alumnos) y que se la banque!
Me toco una difícil, y que no sabia realmente como solucionar. Fue un trabajo muy fuerte en equipo:
- Daniel Palazzo (DBA) @palazzo_d http://danielpalazzo.com.ar/
- Lucas Szczuczko (sysadmin)
- Nicolas Samus (Sysadmin) @nikosamus
Lo que implementamos fue una solución con un frontend que balance carga y varios nodos backend con la aplicación. La aplicación se conecta a una base de datos (DB1) y por seguridad generamos dos replicas, en el mismo cluster y otro cluster (para no tener dependencia de ningun tipo). Como al empezar a tener mucho trafico se generaban demasiadas conexiones contra la base instalamos un pgPool para manejarlo de una manera mas eficiente.
Por ahora ahora la solución que buscamos es solo performance y seguridad de datos (¡que no se pierda nada!), próximamente buscamos alta disponibilidad.
Y que la fuerza nos acompañe, después les cuento como nos fue…
No se la banco!… la base volo por los aires. Y miren que si salio mal que nos dedicaron un meme.
Mejorando performance de Apache
Espero que esto sea la «parte 1», esto es una primera aproximación, claramente despues hay que ajustar las configuraciones a cada sitio, pero por ahora con esto mejora sustancialmente.
Lo que propongo es agregar un par de configuraciones para reducir la cantidad de contenido que se trafica después de la primera visita.
Logstash filter para PHP errors
Las aplicaciones tienen errores, algunos mas graves otros menos graves. Algunos mas visibles, otros irreproducibles. Los distintos lenguajes y servidores de aplicación hacen logs de los errores que se encuentran. Pero seamos sinceros, a menos que nos llamen para decirnos de que algo anda mal, el 90% de las veces no nos entereamos y probablemente ni nos interese (hay cosas mas divertidas para hacer que casar bugs).
Para ser un poco mas pro activo en la búsqueda de problemas, o para hacernos la vida un poco mas facil a la hora de buscar la causa del error cuando ya no nos lo reportaron, es necesario revisar los logs de la aplicación. Pero es un dolor de cabeza!!!
Por todo esto estoy trabajando un poco para poner en funcionamiento una instancia del stack ELK (intentando sin exito hacer algunos dashboards útiles) y no pude encontrar un parsing decente de los logs de Apache para los errores PHP, y así tener datos mas útiles sobre los errores, por lo que arme uno propio.
Aquí lo comparto en un gist la configuración que arme (próximamente mas info).