Puppet Enterprise 2017.2

ロールとプロファイルの概要詳細な例では、ロールとプロファイルの手法の仕組みについて説明しました。次に、より高度なプロファイルを記述していきます。

上から下に記述する例で示したプロファイルは非常にシンプルで、複雑なサービスを管理する方法は示していませんでした。 この高度な例では、コード例を反復的にリファクタリングし、実際の懸念事項に対処します。 最終的には(若干の相違点はあるものの)Puppetの本稼働で使用するJenkinsプロファイルが作成されます。

その中で、選択肢について説明するとともに、独自のプロファイルを設計するにあたって直面する一般的なトレードオフについても指摘します。

注意

初期のプロファイルコード

以前の例で作成したJenkinsプロファイルは以下のとおりです。

# /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
class profile::jenkins::master (
  String $jenkins_port = '9091',
  String $java_dist    = 'jdk',
  String $java_version = 'latest',
) {

  class { 'jenkins':
    configure_firewall => true,
    install_java       => false,
    port               => $jenkins_port,
    config_hash        => {
      'HTTP_PORT'    => { 'value' => $jenkins_port },
      'JENKINS_PORT' => { 'value' => $jenkins_port },
    },
  }

  class { 'java':
    distribution => $java_dist,
    version      => $java_version,
    before       => Class['jenkins'],
  }
}

プロファイル記述のルール

プロファイルクラスを記述するためのルールは以下のとおりです。

  1. include関数を使用して、プロファイルを安全に動作するようにします。プロファイルにリソースライクな宣言を使用しないでください。
  2. プロファイルには、他のプロファイルをincludeできます。
  3. プロファイルには、コンポーネントクラスのすべてのクラスパラメータがあります。プロファイルでいずれかのクラスパラメータが省略されている場合、必ずデフォルト値が必要です。コンポーネントクラスはHieraデータの値を使用してはなりません。以前に省略されたクラスパラメータを設定する必要がある場合、プロファイルをリファクタリングします。
  4. コンポーネントクラスを設定するにあたり、プロファイルに必要な情報を入手するには3通りの方法があります。
    • 指定のパラメータに対して常に同じ値を使用する場合は、ハードコード化します。
    • ハードコード化できない場合は、すでにわかっている情報をもとに計算します。
    • 最後に、計算できない場合は、データの中で ルックアップを実行します。ルックアップを減らすため、1つの質問に対する回答から複数のパラメータを得られるケースを特定します。

1回目のリファクタリング: Javaを分割する

Jenkins masterおよびJenkins agentノードを管理します。agentプロファイルについては詳しく網羅しませんが、最初に直面する問題点は、いずれもJavaが必要であるということです。

Javaクラス宣言をコピーし、貼り付けることができます。Javaクラス宣言は小さいため、複数のコピーを最新の状態に保つことは、それほど大きな負担ではありません。 今回は、Javaを個別のプロファイルに分割することにしました。 そうすると、管理を一度に行い、Javaプロファイルをagentプロファイルとmasterプロファイルの両方に含めることができます。

注意: これは一般的なトレードオフです。 コード片を1か所にまとめておくと(「DRY(don't repeat yourself: 繰り返しを避ける)」原則と呼ばれます)、メンテナンスがしやすくなり、脆弱性を回避することができます。ただし、その代償として個々のプロファイルクラスが読みづらくなり、プロファイルの実際の動作を確認するには、より多くのファイルを確認する必要があります。

読みやすさが損なわれないように、意味がある単位にコードを分割します。 このようにすると、Javaプロファイルのジョブはシンプルなので、名前から推測することができます。つまり、コードを読まなくても、Java 8を管理していることがわかります。 コメントも役に立ちます。

まず、JenkinsマシンでJavaをどれだけ構成するかを決定します。 過去の用途を見ると、2つのオプションしか使用していないことがわかります。そのうち1つはOracleのJava 8配布をインストールすること、もう1つはJenkinsモジュールが管理するOpenJDK 7をデフォルトで設定することです。 つまり、以下のことが可能です。

  • 新しいJavaプロファイルはできるだけシンプルにする。Java 8をハードコード化し、設定は行わない。
  • profile::jenkins::masterの2つのJavaパラメータを、1つのブールパラメータに置換する(JenkinsでJavaを処理するかどうか)。

注意: これはルール4を実際に行ったものです。 複数の質問を1つに集約し、プロファイルの設定面を削減します。

新しいパラメータリストは以下のとおりです。

class profile::jenkins::master (
  String  $jenkins_port = '9091',
  Boolean $install_jenkins_java = true,
) { # ...

また、使用するJavaは以下のように選択します。

  class { 'jenkins':
    configure_firewall => true,
    install_java       => $install_jenkins_java,    # <--- here
    port               => $jenkins_port,
    config_hash        => {
      'HTTP_PORT'    => { 'value' => $jenkins_port },
      'JENKINS_PORT' => { 'value' => $jenkins_port },
    },
  }

  # When not using the jenkins module's java version, install java8.
  unless $install_jenkins_java  { include profile::jenkins::usage::java8 }

新しいJavaプロファイルは以下のとおりです。

# Class: profile::jenkins::usage::java8
# Sets up java8 for Jenkins on Debian
#
class profile::jenkins::usage::java8 {
  motd::register { 'Java usage profile (profile::jenkins::usage::java8)': }

  # OpenJDK 7 is already managed by the Jenkins module.
  # ::jenkins::install_java or ::jenkins::agent::install_java should be false to use this profile
  # this can be set through the class parameter $install_jenkins_java
  case $::osfamily {
    'debian': {
      class { 'java':
        distribution => 'oracle-jdk8',
        version      => '8u92',
      }

      package { 'tzdata-java':
        ensure => latest,
      }
    }
    default: {
      notify { "profile::jenkins::usage::java8 cannot set up JDK on ${::osfamily}": }
    }
  }
}

1回目のリファクタリングの差分

@@ -1,13 +1,12 @@
 # /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
 class profile::jenkins::master (
-  String $jenkins_port = '9091',
-  String $java_dist    = 'jdk',
-  String $java_version = 'latest',
+  String  $jenkins_port = '9091',
+  Boolean $install_jenkins_java = true,
 ) {

   class { 'jenkins':
     configure_firewall => true,
-    install_java       => false,
+    install_java       => $install_jenkins_java,
     port               => $jenkins_port,
     config_hash        => {
       'HTTP_PORT'    => { 'value' => $jenkins_port },
@@ -15,9 +14,6 @@ class profile::jenkins::master (
     },
   }

-  class { 'java':
-    distribution => $java_dist,
-    version      => $java_version,
-    before       => Class['jenkins'],
-  }
+  # When not using the jenkins module's java version, install java8.
+  unless $install_jenkins_java  { include profile::jenkins::usage::java8 }
 }

2回目のリファクタリング: ヒープを管理する

Puppetでは、Jenkinsアプリ用にJavaのヒープサイズを管理します。本稼働サーバーには十分なメモリが搭載されておらず、ヘビーユースには対応していませんでした。

Jenkinsモジュールには、システムプロパティを管理するためのユーザ定義のタイプjenkins::sysconfigがあるため、それを使用します。

  # Manage the heap size on the master, in MB.
  if($::memorysize_mb =~ Number and $::memorysize_mb > 8192)
  {
    # anything over 8GB we should keep max 4GB for OS and others
    $heap = sprintf('%.0f', $::memorysize_mb - 4096)
  } else {
    # This is calculated as 50% of the total memory.
    $heap = sprintf('%.0f', $::memorysize_mb * 0.5)
  }
  # Set java params, like heap min and max sizes. See
  # https://wiki.jenkins-ci.org/display/JENKINS/Features+controlled+by+system+properties
  jenkins::sysconfig { 'JAVA_ARGS':
    value => "-Xms${heap}m -Xmx${heap}m -Djava.awt.headless=true -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -Dhudson.model.DirectoryBrowserSupport.CSP=\\\"default-src 'self'; img-src 'self'; style-src 'self';\\\"",
  }

注意: ここでもルール4が適用されます。余分なメモリを必要とする小さいJenkins masterがあり、ハードコード化できないからです。 ただし、本稼働のmasterは必ずパワフルなマシンにあるため、マシンのメモリサイズに基づいてヒープを計算できます(factとしてアクセス可能)。 これにより、余分な設定を行わずに済みます。

2回目のリファクタリングの差分

@@ -16,4 +16,20 @@ class profile::jenkins::master (

   # When not using the jenkins module's java version, install java8.
   unless $install_jenkins_java  { include profile::jenkins::usage::java8 }
+
+  # Manage the heap size on the master, in MB.
+  if($::memorysize_mb =~ Number and $::memorysize_mb > 8192)
+  {
+    # anything over 8GB we should keep max 4GB for OS and others
+    $heap = sprintf('%.0f', $::memorysize_mb - 4096)
+  } else {
+    # This is calculated as 50% of the total memory.
+    $heap = sprintf('%.0f', $::memorysize_mb * 0.5)
+  }
+  # Set java params, like heap min and max sizes. See
+  # https://wiki.jenkins-ci.org/display/JENKINS/Features+controlled+by+system+properties
+  jenkins::sysconfig { 'JAVA_ARGS':
+    value => "-Xms${heap}m -Xmx${heap}m -Djava.awt.headless=true -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -Dhudson.model.DirectoryBrowserSupport.CSP=\\\"default-src 'self'; img-src 'self'; style-src 'self';\\\"",
+  }
+
 }

3回目のリファクタリング: バージョンを固定する

予期しないアップグレードを回避するため、Jenkinsを特定のバージョンに固定します。 そのためには、Jenkinsを内部パッケージリポジトリに追加するのではなく、直接パッケージURLを使用します。 組織によっては、やり方が異なることもあります。

まず、アップグレードをコントロールするパラメータを追加します。 これで、.../data/groups/ci.yamlをそのままにしておきながら.../data/groups/ci/dev.yamlに新しい値を設定できるようになります。開発マシンは最初に新しいJenkinsバージョンを取得するため、本稼働マシンをアップグレードする前に、すべてを予期しているとおりに動作させることができます。

class profile::jenkins::master (
  Variant[String[1], Boolean] $direct_download = 'http://pkg.jenkins-ci.org/debian-stable/binary/jenkins_1.642.2_all.deb',
  # ...
) { # ...

次に、Jenkinsクラスに必須のパラメータを設定します。

  class { 'jenkins':
    lts                => true,                    # <-- here
    repo               => true,                    # <-- here
    direct_download    => $direct_download,        # <-- here
    version            => 'latest',                # <-- here
    service_enable     => true,
    service_ensure     => running,
    configure_firewall => true,
    install_java       => $install_jenkins_java,
    port               => $jenkins_port,
    config_hash        => {
      'HTTP_PORT'    => { 'value' => $jenkins_port },
      'JENKINS_PORT' => { 'value' => $jenkins_port },
    },
  }

今回は、Jenkinsサービスの明示的な管理も追加しました。

3回目のリファクタリングの差分

@@ -1,10 +1,17 @@
 # /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
 class profile::jenkins::master (
-  String  $jenkins_port = '9091',
-  Boolean $install_jenkins_java = true,
+  String                      $jenkins_port = '9091',
+  Variant[String[1], Boolean] $direct_download = 'http://pkg.jenkins-ci.org/debian-stable/binary/jenkins_1.642.2_all.deb',
+  Boolean                     $install_jenkins_java = true,
 ) {

   class { 'jenkins':
+    lts                => true,
+    repo               => true,
+    direct_download    => $direct_download,
+    version            => 'latest',
+    service_enable     => true,
+    service_ensure     => running,
     configure_firewall => true,
     install_java       => $install_jenkins_java,
     port               => $jenkins_port,

4回目のリファクタリング: ユーザアカウントを手動で管理する

インフラでは多数のユーザアカウントを管理するため、一元的に処理します。 profile::serverクラスはvirtual::usersをプルします。ここには多数の仮想リソースが含まれており、ユーザが指定マシンにログインする場合は必要に応じて個別に提供できます。

注意: これには代償が伴います。この処理は離れた場所から行われるため、特定のプロファイルに対してどのユーザを有効にするか確認する場合に、多くのファイルを読む必要があります。 ただし、ここでは代償を払う価値があると判断しました。すべてのユーザアカウントを記述したファイルは1つまたは2つしかないため、存在する可能性があるすべてのユーザを確認し、一貫して管理することは難しくないからです。

今回の選択によって、(問題なく対処できる)ある部分については手間が増えますが、その代わりに別の部分(そこで問題に直面すると手に負えなくなる可能性がある)の問題を回避することができます。 このような判断を行うには、同僚の能力と、既存のコードベースおよびサポートされるサービスの限度を把握しておく必要があります。

そのため、この例では、Jenkinsプロファイルが同様に動作するように変更します。他のユーザアカウントに合わせて、jenkinsユーザアカウントを管理します。 同時に、Jenkinsのパッケージ化の方法によっては問題となり得るディレクトリも管理します。

必要な値の中には、Jenkins agentとJenkins masterの両方によって使用されるものもあるため、これらをparamsクラスに格納します。これは共有変数を設定するクラスで、リソースの管理は行いません。 ただし、これは負荷の大きいソリューションであるため、使用する前に実際の値が提供されるまで待機する必要があります。 今回のケースでは、OS固有のagentプロファイルが多数あるため(この例には含まれていません)、paramクラスの使用が効果的です。

注意: 前回と同様、「don't repeat yourself(繰り返しを避ける)」と「keep it readable(読みやすくする)」は相反する原則であるため、適度なバランスを見出す必要があります。

  # We rely on virtual resources that are ultimately declared by profile::server.
  include profile::server

  # Some default values that vary by OS:
  include profile::jenkins::params
  $jenkins_owner          = $profile::jenkins::params::jenkins_owner
  $jenkins_group          = $profile::jenkins::params::jenkins_group
  $master_config_dir      = $profile::jenkins::params::master_config_dir

  file { '/var/run/jenkins': ensure => 'directory' }

  # Because our account::user class manages the '${master_config_dir}' directory
  # as the 'jenkins' user's homedir (as it should), we need to manage
  # `${master_config_dir}/plugins` here to prevent the upstream
  # rtyler-jenkins module from trying to manage the homedir as the config
  # dir. For more info, see the upstream module's `manifests/plugin.pp`
  # manifest.
  file { "${master_config_dir}/plugins":
    ensure  => directory,
    owner   => $jenkins_owner,
    group   => $jenkins_group,
    mode    => '0755',
    require => [Group[$jenkins_group], User[$jenkins_owner]],
  }

  Account::User <| tag == 'jenkins' |>

  class { 'jenkins':
    lts                => true,
    repo               => true,
    direct_download    => $direct_download,
    version            => 'latest',
    service_enable     => true,
    service_ensure     => running,
    configure_firewall => true,
    install_java       => $install_jenkins_java,
    manage_user        => false,                    # <-- here
    manage_group       => false,                    # <-- here
    manage_datadirs    => false,                    # <-- here
    port               => $jenkins_port,
    config_hash        => {
      'HTTP_PORT'    => { 'value' => $jenkins_port },
      'JENKINS_PORT' => { 'value' => $jenkins_port },
    },
  }

上記のコードでは、以下の3点に注意してください。

  • ユーザは、自作のユーザ定義タイプaccount::userで管理します。ここでuserリソースの他にもいくつかの宣言を行います。
  • Account::Userリソースコレクタを使用してJenkinsユーザを作成します。これを行うには、profile::serverを宣言する必要があります。
  • Jenkinsクラスのmanage_usermanage_groupmanage_datadirsパラメータをfalseに設定します。
  • これで、pluginsディレクトリとrunディレクトリが明示的に管理されます。

4回目のリファクタリングの差分

@@ -5,6 +5,33 @@ class profile::jenkins::master (
   Boolean                     $install_jenkins_java = true,
 ) {

+  # We rely on virtual resources that are ultimately declared by profile::server.
+  include profile::server
+
+  # Some default values that vary by OS:
+  include profile::jenkins::params
+  $jenkins_owner          = $profile::jenkins::params::jenkins_owner
+  $jenkins_group          = $profile::jenkins::params::jenkins_group
+  $master_config_dir      = $profile::jenkins::params::master_config_dir
+
+  file { '/var/run/jenkins': ensure => 'directory' }
+
+  # Because our account::user class manages the '${master_config_dir}' directory
+  # as the 'jenkins' user's homedir (as it should), we need to manage
+  # `${master_config_dir}/plugins` here to prevent the upstream
+  # rtyler-jenkins module from trying to manage the homedir as the config
+  # dir. For more info, see the upstream module's `manifests/plugin.pp`
+  # manifest.
+  file { "${master_config_dir}/plugins":
+    ensure  => directory,
+    owner   => $jenkins_owner,
+    group   => $jenkins_group,
+    mode    => '0755',
+    require => [Group[$jenkins_group], User[$jenkins_owner]],
+  }
+
+  Account::User <| tag == 'jenkins' |>
+
   class { 'jenkins':
     lts                => true,
     repo               => true,
@@ -14,6 +41,9 @@ class profile::jenkins::master (
     service_ensure     => running,
     configure_firewall => true,
     install_java       => $install_jenkins_java,
+    manage_user        => false,
+    manage_group       => false,
+    manage_datadirs    => false,
     port               => $jenkins_port,
     config_hash        => {
       'HTTP_PORT'    => { 'value' => $jenkins_port },

5回目のリファクタリング: その他の依存関係を管理する

Jenkinsでは、Gitを必ずインストールする必要があります(PuppetのソースコントロールではGitが使用されるため)。また、非公開のGitリポジトリにアクセスし、Jenkins agentノードでコマンドを実行するには、SSHキーが必要です。Jenkinsプラグインの標準的なリストもあるため、これらも管理します。

Gitの管理は難しくありません。

  package { 'git':
    ensure => present,
  }

SSHキーは機密性の高いコンテンツなので、管理はやや困難です。 SSHキーは、他のPuppetコードとともにバージョン管理にチェックインできないため、特定のPuppetサーバーのカスタムマウントポイントに格納します。

このサーバーは通常のPuppetサーバーとは異なるため、アクセスのためのルールを定めます。ホスト名をハードコード化する代わりに、データからルックアップする必要があります。 これにより、セキュアサーバーが移動しても1か所で変更するだけで対応できます。

  $secure_server = lookup('puppetlabs::ssl::secure_server')

  file { "${master_config_dir}/.ssh":
    ensure => directory,
    owner  => $jenkins_owner,
    group  => $jenkins_group,
    mode   => '0700',
  }

  file { "${master_config_dir}/.ssh/id_rsa":
    ensure => file,
    owner  => $jenkins_owner,
    group  => $jenkins_group,
    mode   => '0600',
    source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins",
  }

  file { "${master_config_dir}/.ssh/id_rsa.pub":
    ensure => file,
    owner  => $jenkins_owner,
    group  => $jenkins_group,
    mode   => '0640',
    source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins.pub",
  }

プラグインもやや慎重に検討する必要があります。プラグインを手動で設定するJenkins masterもいくつか存在するからです。 そのため、ベースリストを別個のプロファイルに格納し、それを使用するかどうかはパラメータでコントロールします。

class profile::jenkins::master (
  Boolean                     $manage_plugins = false,
  # ...
) {
  # ...
  if $manage_plugins {
    include profile::jenkins::master::plugins
  }

プラグインプロファイルでは、Jenkinsモジュールが提供するjenkins::pluginリソース型を使用できます。

# /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master/plugins.pp
class profile::jenkins::master::plugins {
  jenkins::plugin { 'audit2db':          }
  jenkins::plugin { 'credentials':       }
  jenkins::plugin { 'jquery':            }
  jenkins::plugin { 'job-import-plugin': }
  jenkins::plugin { 'ldap':              }
  jenkins::plugin { 'mailer':            }
  jenkins::plugin { 'metadata':          }
  # ... and so on.
}

5回目のリファクタリングの差分

@@ -1,6 +1,7 @@
 # /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
 class profile::jenkins::master (
   String                      $jenkins_port = '9091',
+  Boolean                     $manage_plugins = false,
   Variant[String[1], Boolean] $direct_download = 'http://pkg.jenkins-ci.org/debian-stable/binary/jenkins_1.642.2_all.deb',
   Boolean                     $install_jenkins_java = true,
 ) {
@@ -14,6 +15,20 @@ class profile::jenkins::master (
   $jenkins_group          = $profile::jenkins::params::jenkins_group
   $master_config_dir      = $profile::jenkins::params::master_config_dir

+  if $manage_plugins {
+    # About 40 jenkins::plugin resources:
+    include profile::jenkins::master::plugins
+  }
+
+  # Sensitive info (like SSH keys) isn't checked into version control like the
+  # rest of our modules; instead, it's served from a custom mount point on a
+  # designated server.
+  $secure_server = lookup('puppetlabs::ssl::secure_server')
+
+  package { 'git':
+    ensure => present,
+  }
+
   file { '/var/run/jenkins': ensure => 'directory' }

   # Because our account::user class manages the '${master_config_dir}' directory
@@ -69,4 +84,29 @@ class profile::jenkins::master (
     value => "-Xms${heap}m -Xmx${heap}m -Djava.awt.headless=true -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -Dhudson.model.DirectoryBrowserSupport.CSP=\\\"default-src 'self'; img-src 'self'; style-src 'self';\\\"",
   }

+  # Deploy the SSH keys that Jenkins needs to manage its agent machines and
+  # access Git repos.
+  file { "${master_config_dir}/.ssh":
+    ensure => directory,
+    owner  => $jenkins_owner,
+    group  => $jenkins_group,
+    mode   => '0700',
+  }
+
+  file { "${master_config_dir}/.ssh/id_rsa":
+    ensure => file,
+    owner  => $jenkins_owner,
+    group  => $jenkins_group,
+    mode   => '0600',
+    source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins",
+  }
+
+  file { "${master_config_dir}/.ssh/id_rsa.pub":
+    ensure => file,
+    owner  => $jenkins_owner,
+    group  => $jenkins_group,
+    mode   => '0640',
+    source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins.pub",
+  }
+
 }

6回目のリファクタリング: ロギングとバックアップを管理する

ほとんどの場合は、バックアップを行うことをお勧めします。

自作のbackupモジュールを使用することもできます。このモジュールは、backup::jobリソース型を提供します(この前提条件については、profile::serverが対処します)。 ただし、ここではバックアップをオプションにします。そうすることで、テスト目的で1回限りのJenkinsインスタンスを設定している場合に、バックアップサーバーに不要なものを誤って送信しないようにします。

class profile::jenkins::master (
  Boolean                     $backups_enabled = false,
  # ...
) {
  # ...
  if $backups_enabled {
    backup::job { "jenkins-data-${::hostname}":
      files => $master_config_dir,
    }
  }
}

また、チームからは、Jenkinsログに対して競合するリクエストがありました。

  • 他の大半のサービスと同じように、syslogを使いたいという人もいます。
  • また、syslogにスパムが送信されないよう、個別のログファイルを使用し、ファイルをデフォルトよりも短期間で回転させたいという人もいます。

この場合は新たなパラメータが必要となります。 ここで、$jenkins_logs_to_syslogというパラメータを作成し、undefをデフォルトにします。 これを標準的なsyslogファシリティ(daemon.infoなど)に設定すると、Jenkinsは独自のファイルではなく、そのファシリティにログを格納します。

jenkins::sysconfigと自作のlogrotate::jobで処理を行います。

class profile::jenkins::master (
  Optional[String[1]]         $jenkins_logs_to_syslog = undef,
  # ...
) {
  # ...
  if $jenkins_logs_to_syslog {
    jenkins::sysconfig { 'JENKINS_LOG':
      value => "$jenkins_logs_to_syslog",
    }
  }
  # ...
  logrotate::job { 'jenkins':
    log     => '/var/log/jenkins/jenkins.log',
    options => [
      'daily',
      'copytruncate',
      'missingok',
      'rotate 7',
      'compress',
      'delaycompress',
      'notifempty'
    ],
  }
}

6回目のリファクタリングの差分

@@ -1,8 +1,10 @@
 # /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
 class profile::jenkins::master (
   String                      $jenkins_port = '9091',
+  Boolean                     $backups_enabled = false,
   Boolean                     $manage_plugins = false,
   Variant[String[1], Boolean] $direct_download = 'http://pkg.jenkins-ci.org/debian-stable/binary/jenkins_1.642.2_all.deb',
+  Optional[String[1]]         $jenkins_logs_to_syslog = undef,
   Boolean                     $install_jenkins_java = true,
 ) {

@@ -84,6 +86,15 @@ class profile::jenkins::master (
     value => "-Xms${heap}m -Xmx${heap}m -Djava.awt.headless=true -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -Dhudson.model.DirectoryBrowserSupport.CSP=\\\"default-src 'self'; img-src 'self'; style-src 'self';\\\"",
   }

+  # Forward jenkins master logs to syslog.
+  # When set to facility.level the jenkins_log will use that value instead of a
+  # separate log file eg. daemon.info
+  if $jenkins_logs_to_syslog {
+    jenkins::sysconfig { 'JENKINS_LOG':
+      value => "$jenkins_logs_to_syslog",
+    }
+  }
+
   # Deploy the SSH keys that Jenkins needs to manage its agent machines and
   # access Git repos.
   file { "${master_config_dir}/.ssh":
@@ -109,4 +120,29 @@ class profile::jenkins::master (
     source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins.pub",
   }

+  # Back up Jenkins' data.
+  if $backups_enabled {
+    backup::job { "jenkins-data-${::hostname}":
+      files => $master_config_dir,
+    }
+  }
+
+  # (QENG-1829) Logrotate rules:
+  # Jenkins' default logrotate config retains too much data: by default, it
+  # rotates jenkins.log weekly and retains the last 52 weeks of logs.
+  # Considering we almost never look at the logs, let's rotate them daily
+  # and discard after 7 days to reduce disk usage.
+  logrotate::job { 'jenkins':
+    log     => '/var/log/jenkins/jenkins.log',
+    options => [
+      'daily',
+      'copytruncate',
+      'missingok',
+      'rotate 7',
+      'compress',
+      'delaycompress',
+      'notifempty'
+    ],
+  }
+
 }

7回目のリファクタリング: HTTPSのリバースプロキシを使用する

Jenkins WebインターフェースでHTTPSを使用してもらうため、Nginxリバースプロキシを実現します。 また、ポートを標準化します。Jenkinsアプリはデフォルトポートに常にバインドされ、プロキシではHTTPSは常にポート443を、HTTPは常にポート80を使用します。

通常のHTTPを使用可能な状態にしておく場合は、$sslパラメータを提供します。 falseに設定されている場合(デフォルト)、HTTPとHTTPSの両方を経由してJenkinsにアクセスできます。 $site_aliasパラメータも追加します。こうすると、プロキシはノードのメインFQDN以外のホスト名もリッスンできます。

class profile::jenkins::master (
  Boolean                     $ssl = false,
  Optional[String[1]]         $site_alias = undef,
  # IMPORTANT: notice that $jenkins_port is removed.
  # ...

Jenkinsクラスでconfigure_firewall => falseを設定します。

  class { 'jenkins':
    lts                => true,
    repo               => true,
    direct_download    => $direct_download,
    version            => 'latest',
    service_enable     => true,
    service_ensure     => running,
    configure_firewall => false,                # <-- here
    install_java       => $install_jenkins_java,
    manage_user        => false,
    manage_group       => false,
    manage_datadirs    => false,
    # IMPORTANT: notice that port and config_hash are removed.
  }

Nginxが到達できる場所にSSL証明書をデプロイする必要があります。 HTTPS上ではさまざまなサービスを提供しているため、そのプロファイルがすでに存在します。

  # Deploy the SSL certificate/chain/key for sites on this domain.
  include profile::ssl::delivery_wildcard

ここで、その日のメッセージに情報を追加するのも良いでしょう(puppetlabs/motdが処理します)。

  motd::register { 'Jenkins CI master (profile::jenkins::master)': }

  if $site_alias {
    motd::register { 'jenkins-site-alias':
      content => @("END"),
                 profile::jenkins::master::proxy

                 Jenkins site alias: ${site_alias}
                 |-END
      order   => 25,
    }
  }

作業の大半は、profile::jenkins::master::proxyという名前の新規プロファイルによって処理されます。 簡略化するためコードは省略しますが、簡単に言うと以下の処理が行われます。

  • profile::nginxを含める。
  • jfryman/nginxのリソース型を使用してvhostを設定し、通常のHTTPを有効にしていない場合はHTTPSへのリダイレクトを強制する。
  • アクセスおよびエラーログ用に、logstash転送を設定する。
  • 必要に応じてprofile::fw::httpsを含めてファイアウォールルールを管理する。

次に、そのプロファイルをメインプロファイルで宣言します。

  class { 'profile::jenkins::master::proxy':
    site_alias  => $site_alias,
    require_ssl => $ssl,
  }

重要: ここで、ロールとプロファイル手法のルールの中でも特に重要な、ルール1が破られています。 なぜ?

profile::jenkins::master::proxyprofile::jenkins::masterのみ属する「非公開」のプロファイルであるからです。 他のロールやプロファイルによって宣言されることはありません。

読みやすさを維持する目的でのみコードを分離する場合は、これがルール1への唯一の例外になります。つまり、非公開プロファイルのコンテンツをメインプロファイルに貼り付けてまったく同じ効果を得られるのであれば、リソースライクの宣言を非公開プロファイルで使用することができます。 これにより、データルックアップをまとめ、非公開プロファイルのインプットの可視性を高めながら、メインプロファイルをすっきりと見やすくすることができます。 その場合は、非公開プロファイルが非公開であることを文書化する必要があります。

このコードが別のプロファイルによって使用される可能性がある場合は、ルール1を順守してください。

7回目のリファクタリングの差分

@@ -1,8 +1,9 @@
 # /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
 class profile::jenkins::master (
-  String                      $jenkins_port = '9091',
   Boolean                     $backups_enabled = false,
   Boolean                     $manage_plugins = false,
+  Boolean                     $ssl = false,
+  Optional[String[1]]         $site_alias = undef,
   Variant[String[1], Boolean] $direct_download = 'http://pkg.jenkins-ci.org/debian-stable/binary/jenkins_1.642.2_all.deb',
   Optional[String[1]]         $jenkins_logs_to_syslog = undef,
   Boolean                     $install_jenkins_java = true,
@@ -11,6 +12,9 @@ class profile::jenkins::master (
   # We rely on virtual resources that are ultimately declared by profile::server.
   include profile::server

+  # Deploy the SSL certificate/chain/key for sites on this domain.
+  include profile::ssl::delivery_wildcard
+
   # Some default values that vary by OS:
   include profile::jenkins::params
   $jenkins_owner          = $profile::jenkins::params::jenkins_owner
@@ -22,6 +26,31 @@ class profile::jenkins::master (
     include profile::jenkins::master::plugins
   }

+  motd::register { 'Jenkins CI master (profile::jenkins::master)': }
+
+  # This adds the site_alias to the message of the day for convenience when
+  # logging into a server via FQDN. Because of the way motd::register works, we
+  # need a sort of funny formatting to put it at the end (order => 25) and to
+  # list a class so there isn't a random "--" at the end of the message.
+  if $site_alias {
+    motd::register { 'jenkins-site-alias':
+      content => @("END"),
+                 profile::jenkins::master::proxy
+
+                 Jenkins site alias: ${site_alias}
+                 |-END
+      order   => 25,
+    }
+  }
+
+  # This is a "private" profile that sets up an Nginx proxy -- it's only ever
+  # declared in this class, and it would work identically pasted inline.
+  # But since it's long, this class reads more cleanly with it separated out.
+  class { 'profile::jenkins::master::proxy':
+    site_alias  => $site_alias,
+    require_ssl => $ssl,
+  }
+
   # Sensitive info (like SSH keys) isn't checked into version control like the
   # rest of our modules; instead, it's served from a custom mount point on a
   # designated server.
@@ -56,16 +85,11 @@ class profile::jenkins::master (
     version            => 'latest',
     service_enable     => true,
     service_ensure     => running,
-    configure_firewall => true,
+    configure_firewall => false,
     install_java       => $install_jenkins_java,
     manage_user        => false,
     manage_group       => false,
     manage_datadirs    => false,
-    port               => $jenkins_port,
-    config_hash        => {
-      'HTTP_PORT'    => { 'value' => $jenkins_port },
-      'JENKINS_PORT' => { 'value' => $jenkins_port },
-    },
   }

   # When not using the jenkins module's java version, install java8.

ファイナルコード

リファクタリング(および微調整)を行った後、profile::jenkins::masterのファイナルコードは以下のとおりです。

# /etc/puppetlabs/code/environments/production/site/profile/manifests/jenkins/master.pp
# Class: profile::jenkins::master
#
# Install a Jenkins master that meets Puppet's internal needs.
#
class profile::jenkins::master (
  Boolean                     $backups_enabled = false,
  Boolean                     $manage_plugins = false,
  Boolean                     $ssl = false,
  Optional[String[1]]         $site_alias = undef,
  Variant[String[1], Boolean] $direct_download = 'http://pkg.jenkins-ci.org/debian-stable/binary/jenkins_1.642.2_all.deb',
  Optional[String[1]]         $jenkins_logs_to_syslog = undef,
  Boolean                     $install_jenkins_java = true,
) {

  # We rely on virtual resources that are ultimately declared by profile::server.
  include profile::server

  # Deploy the SSL certificate/chain/key for sites on this domain.
  include profile::ssl::delivery_wildcard

  # Some default values that vary by OS:
  include profile::jenkins::params
  $jenkins_owner          = $profile::jenkins::params::jenkins_owner
  $jenkins_group          = $profile::jenkins::params::jenkins_group
  $master_config_dir      = $profile::jenkins::params::master_config_dir

  if $manage_plugins {
    # About 40 jenkins::plugin resources:
    include profile::jenkins::master::plugins
  }

  motd::register { 'Jenkins CI master (profile::jenkins::master)': }

  # This adds the site_alias to the message of the day for convenience when
  # logging into a server via FQDN. Because of the way motd::register works, we
  # need a sort of funny formatting to put it at the end (order => 25) and to
  # list a class so there isn't a random "--" at the end of the message.
  if $site_alias {
    motd::register { 'jenkins-site-alias':
      content => @("END"),
                 profile::jenkins::master::proxy

                 Jenkins site alias: ${site_alias}
                 |-END
      order   => 25,
    }
  }

  # This is a "private" profile that sets up an Nginx proxy -- it's only ever
  # declared in this class, and it would work identically pasted inline.
  # But since it's long, this class reads more cleanly with it separated out.
  class { 'profile::jenkins::master::proxy':
    site_alias  => $site_alias,
    require_ssl => $ssl,
  }

  # Sensitive info (like SSH keys) isn't checked into version control like the
  # rest of our modules; instead, it's served from a custom mount point on a
  # designated server.
  $secure_server = lookup('puppetlabs::ssl::secure_server')

  # Dependencies:
  #   - Pull in apt if we're on Debian.
  #   - Pull in the 'git' package, used by Jenkins for Git polling.
  #   - Manage the 'run' directory (fix for busted Jenkins packaging).
  if $::osfamily == 'Debian' { include apt }

  package { 'git':
    ensure => present,
  }

  file { '/var/run/jenkins': ensure => 'directory' }

  # Because our account::user class manages the '${master_config_dir}' directory
  # as the 'jenkins' user's homedir (as it should), we need to manage
  # `${master_config_dir}/plugins` here to prevent the upstream
  # rtyler-jenkins module from trying to manage the homedir as the config
  # dir. For more info, see the upstream module's `manifests/plugin.pp`
  # manifest.
  file { "${master_config_dir}/plugins":
    ensure  => directory,
    owner   => $jenkins_owner,
    group   => $jenkins_group,
    mode    => '0755',
    require => [Group[$jenkins_group], User[$jenkins_owner]],
  }

  Account::User <| tag == 'jenkins' |>

  class { 'jenkins':
    lts                => true,
    repo               => true,
    direct_download    => $direct_download,
    version            => 'latest',
    service_enable     => true,
    service_ensure     => running,
    configure_firewall => false,
    install_java       => $install_jenkins_java,
    manage_user        => false,
    manage_group       => false,
    manage_datadirs    => false,
  }

  # When not using the jenkins module's java version, install java8.
  unless $install_jenkins_java  { include profile::jenkins::usage::java8 }

  # Manage the heap size on the master, in MB.
  if($::memorysize_mb =~ Number and $::memorysize_mb > 8192)
  {
    # anything over 8GB we should keep max 4GB for OS and others
    $heap = sprintf('%.0f', $::memorysize_mb - 4096)
  } else {
    # This is calculated as 50% of the total memory.
    $heap = sprintf('%.0f', $::memorysize_mb * 0.5)
  }
  # Set java params, like heap min and max sizes. See
  # https://wiki.jenkins-ci.org/display/JENKINS/Features+controlled+by+system+properties
  jenkins::sysconfig { 'JAVA_ARGS':
    value => "-Xms${heap}m -Xmx${heap}m -Djava.awt.headless=true -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -Dhudson.model.DirectoryBrowserSupport.CSP=\\\"default-src 'self'; img-src 'self'; style-src 'self';\\\"",
  }

  # Forward jenkins master logs to syslog.
  # When set to facility.level the jenkins_log will use that value instead of a
  # separate log file eg. daemon.info
  if $jenkins_logs_to_syslog {
    jenkins::sysconfig { 'JENKINS_LOG':
      value => "$jenkins_logs_to_syslog",
    }
  }

  # Deploy the SSH keys that Jenkins needs to manage its agent machines and
  # access Git repos.
  file { "${master_config_dir}/.ssh":
    ensure => directory,
    owner  => $jenkins_owner,
    group  => $jenkins_group,
    mode   => '0700',
  }

  file { "${master_config_dir}/.ssh/id_rsa":
    ensure => file,
    owner  => $jenkins_owner,
    group  => $jenkins_group,
    mode   => '0600',
    source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins",
  }

  file { "${master_config_dir}/.ssh/id_rsa.pub":
    ensure => file,
    owner  => $jenkins_owner,
    group  => $jenkins_group,
    mode   => '0640',
    source => "puppet://${secure_server}/secure/delivery/id_rsa-jenkins.pub",
  }

  # Back up Jenkins' data.
  if $backups_enabled {
    backup::job { "jenkins-data-${::hostname}":
      files => $master_config_dir,
    }
  }

  # (QENG-1829) Logrotate rules:
  # Jenkins' default logrotate config retains too much data: by default, it
  # rotates jenkins.log weekly and retains the last 52 weeks of logs.
  # Considering we almost never look at the logs, let's rotate them daily
  # and discard after 7 days to reduce disk usage.
  logrotate::job { 'jenkins':
    log     => '/var/log/jenkins/jenkins.log',
    options => [
      'daily',
      'copytruncate',
      'missingok',
      'rotate 7',
      'compress',
      'delaycompress',
      'notifempty'
    ],
  }

}
Back to top