{"id":165,"date":"2018-11-27T15:36:45","date_gmt":"2018-11-27T15:36:45","guid":{"rendered":"https:\/\/patryk.rzski.com\/?p=165"},"modified":"2018-11-29T14:08:33","modified_gmt":"2018-11-29T14:08:33","slug":"puppet-git-docker-in-devops-a-simple-yet-powerful-workflow","status":"publish","type":"post","link":"https:\/\/patryk.rzski.com\/?p=165","title":{"rendered":"Puppet, Git, Docker in DevOps &#8211; a simple yet powerful workflow"},"content":{"rendered":"<p>In this article I&#8217;ll briefly describe how I&#8217;m managing my code (configs, scripts, etc.) between my workstation and my virtual private server playground. I will try to point out where I&#8217;m using simple solutions instead of enterprise-appropriate ones.<\/p>\n<p>To automate the workflow, I am using:<\/p>\n<li><strong>Docker<\/strong> &#8211; to run services in sandboxed networks, without their dependencies<\/li>\n<li><strong>Git<\/strong> &#8211; for proper version control of my code<\/li>\n<li><strong>Cronie<\/strong> &#8211; for simple (cronie is a light weight cron implementation) scheduling (with enterprise alternatives)<\/li>\n<li><strong>Puppet<\/strong> &#8211; for file orchestration and integrity monitoring<\/li>\n<p>First of all, I need a code repository with the ability to control versions and to review commits. Git seems the most appropriate as it is easy to configure and is available by default in my Linux distribution (<a href=\"http:\/\/gentoo.org\" rel=\"noopener\" target=\"_blank\">Gentoo<\/a>). It is also available in more common enterprise Linux choices, like RHEL, SLES or Debian. <br \/>\nIt is highly recommended to generate a key pair to use key-based authentication with the Git server, or to be precise &#8211; with the ssh daemon running there. Use <em>ssh-keygen<\/em> to generate the key pair (comes with the openssh package). From there, copy the public key (the one ending in .pub) and place it in Git user&#8217;s ~\/.ssh\/authorized_keys.<br \/>\n<br \/>\nSince there&#8217;s plenty of guides available about setting up a Git repository, I will not describe this in detail here. What I did briefly was a <em>&#8211;bare init<\/em> for configs.git and scripts.git on the server, while on the client side, I&#8217;ve added two remotes over ssh with a custom port:<br \/>\n<code><br \/>\ngit remote add ssh:\/\/git@rzski.com:9999\/path\/to\/configs.git<br \/>\ngit remote add ssh:\/\/git@rzski.com:9999\/path\/to\/scripts.git<br \/>\n<\/code><br \/>\nThis allowed me to push all my files after aggregating their copies in one directory, and then pull it on my workstation. Now I can edit files locally and push them to the central repository on the server:<br \/>\n<code><br \/>\n% echo \"# End of file\" >> configs\/ntpd\/ntp.conf<br \/>\n% git add configs\/ntpd\/ntp.conf<br \/>\n% git commit -m \"configs: for the purpose of the article\"<br \/>\n[master 3a26138] configs: for the purpose of the article<br \/>\n 1 file changed, 1 insertion(+)<br \/>\n% git push configs master<br \/>\nEnumerating objects: 16, done.<br \/>\nCounting objects: 100% (16\/16), done.<br \/>\nDelta compression using up to 8 threads<br \/>\nCompressing objects: 100% (9\/9), done.<br \/>\nWriting objects: 100% (11\/11), 1.29 KiB | 1.29 MiB\/s, done.<br \/>\nTotal 11 (delta 3), reused 0 (delta 0)<br \/>\nTo ssh:\/\/rzski.com:9999\/path\/to\/configs.git<br \/>\n   c5858d0..3a26138  master -> configs<\/code><br \/>\n<br \/>\nTime to set up Puppet to grab the files from the Git repository and push them to chosen environments. For simplicity, I&#8217;m using a single environment (production) here. Puppet needs a server (master) and an agent to provide the file orchestration and integrity monitoring functionality. It also needs a connector to grab files from Git and use them as source for modules. There are many ways to integrate Git and Puppet, such as:<\/p>\n<li>Puppet Enterprise (PE) + PE Code Manager (obsoleting or r10k)<\/li>\n<li>Puppet Enterprise (PE) + PE Bolt running scripts<\/li>\n<li><em>git pull<\/em> command scheduled to run in the Puppet external mount every minute (or so) by cronie<\/li>\n<p>I went with the last approach, but it is probably least appropriate for enterprise or production environments. For me this was suitable, since I&#8217;ve decided to install the puppet-agent (no dependencies) from portage (Gentoo&#8217;s package manager), but have the master and the pdk run as Docker containers:<br \/>\n<code><br \/>\ndocker pull puppet\/puppetserver-standalone<br \/>\ndocker pull terzom\/pdk<\/code><\/p>\n<p>Running the Puppet master from a Docker container is very convenient. The images are tiny. I have created a \/30 network for just the master and the agent to operate in:<br \/>\n<code><br \/>\ndocker network create --internal --subnet=192.168.123.0\/30 --gateway 192.168.123.1 puppet-nw<br \/>\ndocker run --name puppetmstr --hostname puppetmstr --network puppet-nw -d -v \/work\/puppetlabs:\/etc\/puppetlabs puppet\/puppetserver-standalone<\/code><\/p>\n<p>Gladly, the pdk can be run in &#8220;disposable&#8221; mode (like the ansible container, if you decide to use it) and bind storage to the same config path as the master:<br \/>\n<code><br \/>\ndocker run --rm -it -v \/work\/puppetlabs:\/etc\/puppetlabs terzom\/pdk<br \/>\n<\/code><br \/>\nThen run pdk to generate templates for a new module. A module is the recommended organizational unit of which files Puppet should control. In most cases people seem to start with NTP as a good, simple example. I&#8217;m skipping the interview since I&#8217;m not planning to open-source my configs \ud83d\ude09<br \/>\n<code><br \/>\npdk new module ntp --skip-interview<br \/>\n<\/code><\/p>\n<p>This generates the schema for the ntp module. Meanwhile, the server needs to be configured to accept connections from the agent (plenty of guides online, I&#8217;ll jump over this part) and become authorized to grab files from a clone of the central repo.<\/p>\n<p>To set up the repo:<br \/>\n<code><br \/>\ncd \/work\/puppetlabs\/files;<br \/>\ngit init;<br \/>\ngit clone ssh:\/\/git@rzski.com:9999\/path\/to\/configs.git<br \/>\nchown -R puppet:puppet configs\/<br \/>\n<\/code><br \/>\nGOTCHA: all files and folders in the Puppet config as well as cloned from the Git repository need to match the UID and GID used by the <strong>Puppet master in the container<\/strong>. Matching the username and group name is not enough, since these are mapped in \/etc\/passwd and \/etc\/group respectively and can have varying octals. Matching them between the host and the container is apparently the recommended approach.<br \/>\n<br \/>\nThe Puppet master needs to know the location of the central repository pulled from Git. In the below example, I am configuring this directory as a Puppet mount point:<br \/>\n<code><br \/>\n# cat \/work\/puppetlabs\/puppet\/fileserver.conf<br \/>\n[files]<br \/>\n    path \/etc\/puppetlabs\/files<br \/>\n    allow 192.168.123.0\/30<\/code><br \/>\n<code><br \/>\n[configs]<br \/>\n    path \/etc\/puppetlabs\/files\/configs<br \/>\n    allow 192.168.123.0\/30<br \/>\n<\/code><br \/>\nNote the paths refer to \/etc rather than \/work, since that&#8217;s how the master running from within the container will see them.<br \/>\nTo configure authorizing the server to access this mount (although it should be enabled by default, I&#8217;d rather cut it down to just the participants of my puppet-nw network), the following regexp stanzas are needed (use <em>docker attach puppetmstr<\/em> to hook into the running Puppet master and see how it handles these paths as agents try to connect to it):<br \/>\n<code><br \/>\n# cat \/work\/puppetlabs\/puppet\/auth.conf<br \/>\n(...)<br \/>\n# Authorization for serving files from the Git repo<br \/>\npath ~ ^\/file_(metadata|content)s?\/configs\/<br \/>\nmethod find<br \/>\nallow 192.168.123.0\/30<br \/>\npath ~ ^\/file_(metadata|content)s?\/files\/configs\/<br \/>\nmethod find<br \/>\nallow 192.168.123.0\/30<br \/>\n(...)<br \/>\n<\/code><\/p>\n<p>Now each time I want to link a file in the central repo with a file in production, be it a script, config, or source code, I need to generate a module for it. Inside each such module, I&#8217;m defining a config class to manage the actual config file. The rest of the module code has been pre-populated by <em>pdk<\/em>.<br \/>\n<code><br \/>\n# cat \/work\/puppetlabs\/code\/modules\/ntp\/manifests\/config.pp <\/p>\n<p># ntp::config<br \/>\n#<br \/>\n# A description of what this class does<br \/>\n#<br \/>\n# @summary A short summary of the purpose of this class<br \/>\n#<br \/>\n# @example<br \/>\n#   include ntp::config<br \/>\nclass ntp::config {<br \/>\n    file { \"ntp.conf\" :<br \/>\n        path => \"\/etc\/ntp.conf\",<br \/>\n        owner => \"root\",<br \/>\n        group => \"root\",<br \/>\n        mode => \"0644\",<br \/>\n        source => \"puppet:\/\/\/configs\/ntpd\/ntp.conf\",<br \/>\n    }<br \/>\n}<\/code><br \/>\nIn the above example, the source is defined as Puppet&#8217;s fileserver relative location, mount name equals &#8220;configs&#8221;, and the path is set to ntpd\/ntp.conf to match the config file location in the central config repository pulled from Git.<br \/>\n<br \/>\nThe Puppet master also needs to know on which nodes (servers) it should manage these config files. In this case, the management happens in the production environment, on the VPS hosting the <strong>Puppet master container only &#8211; rzski.com<\/strong> (hostname is picked from the host&#8217;s \/etc\/hosts):<br \/>\n<code><br \/>\n# cat \/work\/puppetlabs\/code\/environments\/production\/manifests\/site.pp<br \/>\nnode \"rzski.com\" {<br \/>\n  include ntp::config<br \/>\n}<\/code><br \/>\nThe above two steps (class definition with the correct path and the site.pp class reference per node) would have to be repeated for every module (set of config files or single files or scripts).<\/p>\n<p>To verify the configs are shared, run the local Puppet binary testing the agent mode:<br \/>\n<code><br \/>\n# puppet agent -t<br \/>\nInfo: Using configured environment 'production'<br \/>\nInfo: Retrieving pluginfacts<br \/>\nInfo: Retrieving plugin<br \/>\nInfo: Retrieving locales<br \/>\nInfo: Caching catalog for rzski.com<br \/>\nInfo: Applying configuration version '1543335030'<br \/>\nNotice: Applied catalog in 0.05 seconds<br \/>\n<\/code><br \/>\nAnd to observe the change commited by appending a comment to the end of the ntp.conf file:<br \/>\n<code><br \/>\n# tail \/var\/log\/puppetlabs\/puppet\/puppet.log<br \/>\npuppet-agent[18560]: Applied catalog in 0.05 seconds<br \/>\npuppet-agent[18728]: (\/Stage[main]\/Ntp::Config\/File[ntp.conf]\/content) content changed '{md5}96db7670882085ea77ce9b5fa14dc46f' to '{md5}06f8cea8589b23e43dcea88cce5ac8ea<br \/>\n<\/code><\/p>\n<p>Finally, to &#8220;sync&#8221; between the Git repo and the Puppet repo, cron can call <em>git pull<\/em> every minute (as described above, optionally that can land in a small shell script). Either switch user to &#8216;puppet&#8217; and clone (<em>su &#8211; puppet -c &#8220;git clone&#8230;&#8221;<\/em>, but this requires giving the puppet user a valid shell, which is not ideal), or just pull &#038;&#038; chown.<\/p>\n<p>To go further from here, I can also execute a command (using Bolt for agentless approach, or a shell script) to, for example, push my Solidity code into my Docker container running Geth Ethereum node. Let&#8217;s say I&#8217;ve pushed the file from the workstation and accepted the commit. Once the new version gets cloned into the Puppet repo, run <em>docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH<\/em>. Then, to register the ABI and, if required, compile, I can chain another task calling <em>docker exec container_name command<\/em> and collect the outputs. But that&#8217;s material for another article \ud83d\ude09<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this article I&#8217;ll briefly describe how I&#8217;m managing my code (configs, scripts, etc.) between my workstation and my virtual private server playground. I will try to point out where I&#8217;m using simple solutions instead of enterprise-appropriate ones. To automate the workflow, I am using: Docker &#8211; to run services in sandboxed networks, without their [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"_links":{"self":[{"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=\/wp\/v2\/posts\/165"}],"collection":[{"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=165"}],"version-history":[{"count":18,"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=\/wp\/v2\/posts\/165\/revisions"}],"predecessor-version":[{"id":183,"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=\/wp\/v2\/posts\/165\/revisions\/183"}],"wp:attachment":[{"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=165"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=165"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/patryk.rzski.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=165"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}