0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PostgreSQLのユーザ構成管理(IaC) -Anasible編-

Last updated at Posted at 2022-06-24

PostgreSQLのユーザ構成管理(IaC) -Anasible編-

まえがき

PostgreSQLのユーザ管理、構成管理をしようと思い色々調べてみたのですが自分の中でこれといったものがなかなか見つからなかったのでここに書き記しておきます。

  • ユーザ認証方式検討
  • PostgreSQLのユーザ構成管理(IaC) -Anasible編- (本記事)
  • PostgreSQLのユーザ構成管理(IaC) -Terraform編- (記事がかけたらリンク張る)
  • MySQLのユーザ構成管理(IaC) -Anasible編- (記事がかけたらリンク張る)
  • MySQLのユーザ構成管理(IaC) -Terraform編- (記事がかけたらリンク張る)

今回はAnsibleで以下を実装しました。また、今回は 組み込みユーザ認証方式 でユーザを作成しています。

  • ユーザロールの作成
  • グループロールの作成
  • グループロールへの権限付与
  • ユーザロールのグループロールの継承
  • パスワードの送付

構成

ディレクトリ構成

ansible
├── staging.yml
├── roles
│   └── send-mail
│       └── tasks
│           └── main.yaml      # credentialsに書き込まれたファイルを送信するロールです
│   └── db-account
│       └── tasks
│           └── test_db.yaml   # ユーザロール、グループロール、各種権限付与を行うロールです。今回はDB毎にyamlファイルを作成する方式にしました
├── group_vars
│   ├── staging.yaml           # DB(instance)に作成するデータベース情報やユーザロール、グループロールを記述します
│   ├── staging.sops.yaml      # DBの認証情報を記述します
│   ├── production.yaml
│   └── production.sops.yaml
├── hosts_staging              # db-account roleの実行先環境を記述します
├── hosts_production
├── hosts_sendmail             # send-mail role の実行先環境を記述します
├── credentials                # こちらのディレクトリにパスワードが書き込まれたファイルが生成されます
├── ansible.cfg                # ansibleでsopsを使用するために必要です
└── README.md

コード

  • staging.yaml
test_db:
  database:
    - database1
    - database2
  group_role:
    - read_only
    - read_write
  group_role_priv:
    - type: table
      role: read_only
      priv: SELECT
    - type: table
      role: read_write
      priv: SELECT,INSERT,UPDATE,DELETE
    - type: sequence
      role: read_only
      priv: USAGE,SELECT
    - type: sequence
      role: read_write
      priv: USAGE,SELECT,UPDATE

test_db_user:
  - name: taro.yamada
    email: taro.yama@example.com
    group: [read_write]
    state: present
    group_state: present
    no_password_changes: yes
  - name: hanako.yamada
    email: hanako.yamada@example.com
    group: [read_write]
    state: present
    group_state: present
    no_password_changes: yes
  • staging.sops.yaml
test_db_info:
    login_host: xxx.xxx.xxx.xxx
    login_user: postgres
    login_password: postgres
  • test_db.yaml
- name: Create group roles
  community.postgresql.postgresql_user:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    name: "{{ item }}"
    no_password_changes: yes
    role_attr_flags: NOLOGIN
    fail_on_user: no
    state: present
  loop: "{{ test_db.group_role }}"

- name: Attach priv to group roles on public schema
  community.postgresql.postgresql_privs:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    database: "{{ item.0 }}"
    privs: "{{ item.1.priv }}"
    type:  "{{ item.1.type }}"
    objs: ALL_IN_SCHEMA
    schema: public
    roles: "{{ item.1.role }}"
    grant_option: no
    fail_on_role: no
    state: present
  loop: "{{ test_db.database | product(test_db.group_role_priv) }}"

- name: Collect all roles
  community.postgresql.postgresql_info:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    filter: roles
  register: roles

- name: Create user roles
  community.postgresql.postgresql_user:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    name: "{{ item.name }}"
    no_password_changes: yes
    encrypted: yes
    password: >-
      {%- if item.state == 'present' -%}
        {%- if ansible_check_mode == false -%}
          {%- if item.name in roles.roles | list -%}
          {%- else -%}
            {{ lookup('password', 'credentials/' + item.email + '.txt length=32 chars=ascii_uppercase,ascii_lowercase,digits') }}
          {%- endif -%}
        {%- endif -%}
      {%- endif -%}
    state: "{{ item.state }}"
  loop: "{{ test_db_user }}"

- name: Collect all roles after Create user roles
  community.postgresql.postgresql_info:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    filter: roles
  register: roles

- name: Reset priv to user roles
  community.postgresql.postgresql_membership:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    groups: >-
      {%- if not roles.roles[item.name].member_of -%}
        {{ item.group }}
      {%- else -%}
        {%- if roles.roles[item.name].member_of == item.group -%}
          {{ roles.roles[item.name].member_of }}
        {%- else -%}
          {{ roles.roles[item.name].member_of }}
        {%- endif -%}
      {%- endif -%}
    target_role: "{{ item.name }}"
    fail_on_role: no
    state: >-
      {%- if not roles.roles[item.name].member_of -%}
        present
      {%- else -%}
        {%- if roles.roles[item.name].member_of == item.group -%}
          present
        {%- else -%}
          absent
        {%- endif -%}
      {%- endif -%}
  with_items: "{{ test_db_user }}"
  when: item.name in roles.roles | list

- name: Attach priv to user roles
  community.postgresql.postgresql_membership:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    groups: "{{ item.group }}"
    target_role: "{{ item.name }}"
    fail_on_role: no
    state: "{{ item.group_state }}"
  loop: "{{ test_db_user }}"
  when: item.name in roles.roles | list
  • main.yaml(ansible/send-mail/tasks/main.yaml)
- name: Copy password file from local to remote
  copy:
    src: credentials/
    dest: /tmp/credentials
    remote_src: no
  check_mode: no

- name: get files
  find:
    paths: /tmp/credentials
    file_type: "file"
  register: find_result
  check_mode: no

- name: Send an email to a single recipient that the deployment was successful
  community.general.sendgrid:
    api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    attachments:  /tmp/credentials/{{ item.path | basename }}
    from_address: "test@example.com"
    to_addresses: "{{ item.path | basename | splitext | first }}"
    subject: "Database Password"
    body: "test"
  #delegate_to: localhost
  with_items: "{{ find_result.files }}"

- file: path=/tmp/credentials/
        state=absent
  • ansible.cfg
[defaults]
vars_plugins_enabled = host_group_vars,community.sops.sops

解説

Collect all roles が何故必要なのか

community.postgresql.postgresql_user moduleではユーザが存在する場合はスキップしてくれますし、 no_password_changes を設定すればパスワードも変更されないので一見 Collect all roles タスクは不要に感じます。しかしながら、↓のパスワード作成部分がやっかいでこのチェック機構を挟まないと存在するユーザ分もパスワードが作成されてしまうので、それを回避するため敢えて入れています。
パスワードをファイルに出力せずその場でランダムに作成してしまうと DBを複数作成した場合「DBごとに初期パスワードが異なってしまい面倒であることメールでパスワードを送信したいこと を理由にこういった形でパスワードを生成するようにしています。なお、 lookup関数 を使うとファイルが存在しない場合はファイルを作成し、存在する場合はそのファイルからパスワードを読み取ってくれます。

{{ lookup('password', 'credentials/' + item.email + '.txt length=32 chars=ascii_uppercase,ascii_lowercase,digits') }}

当該箇所

- name: Collect all roles
  community.postgresql.postgresql_info:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    filter: roles
  register: roles

- name: Create user roles
  community.postgresql.postgresql_user:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    name: "{{ item.name }}"
    no_password_changes: yes
    encrypted: yes
    password: >-
      {%- if item.state == 'present' -%}
        {%- if ansible_check_mode == false -%}
          {%- if item.name in roles.roles | list -%}
          {%- else -%}
            {{ lookup('password', 'credentials/' + item.email + '.txt length=32 chars=ascii_uppercase,ascii_lowercase,digits') }}
          {%- endif -%}
        {%- endif -%}
      {%- endif -%}
    state: "{{ item.state }}"
  loop: "{{ test_db_user }}"

{%- if not roles.roles[item.name].member_of -%}

こちらは解説というほどのものではないのですが結構ハマったところなので書いています。前述のパスワード作成部分で item.email のような変数を + でくっつける方法の記事はいくつかでてくるのですが、その方法ではうまく取り出せなかったので備忘録を兼ねて記載しておきました。

{{ lookup('password', 'credentials/' + item.email + '.txt length=32 chars=ascii_uppercase,ascii_lowercase,digits') }}

ちなみにregisterに登録したrolesにはこういった構造で格納されます。

TASK [debug] ***************************************************************************************************************************************************************
ok: [xxx.xxx.xxx.xxx => {
    "msg": [
        {
            "changed": false,
            "databases": {},
            "failed": false,
            "in_recovery": null,
            "pending_restart_settings": [],
            "repl_slots": {},
            "replications": {},
            "roles": {
                "read_only": {
                    "canlogin": false,
                    "member_of": [],
                    "superuser": false,
                    "valid_until": "9999-12-31T23:59:59.999999+00:00"
                },
                "read_write": {
                    "canlogin": false,
                    "member_of": [],
                    "superuser": false,
                    "valid_until": "9999-12-31T23:59:59.999999+00:00"
                },
                "hanako.yamada": {
                    "canlogin": true,
                    "member_of": [
                        "read_only"
                    ],
                    "superuser": false,
                    "valid_until": ""
                },
                "taro.yamada": {
                    "canlogin": true,
                    "member_of": [
                        "read_write"
                    ],
                    "superuser": false,
                    "valid_until": "9999-12-31T23:59:59.999999+00:00"
                },
            },
            "settings": {},
            "tablespaces": {},
            "version": {}
        }
    ]
}

PLAY RECAP *****************************************************************************************************************************************************************
xxx.xxx.xxx.xxx              : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

当該コード

{%- if not roles.roles[item.name].member_of -%}

Reset priv to user roles が何故存在するのか

community.postgresql.postgresql_membership はユーザロールをグループロールを継承させるmoduleです。一見、継承させるだけなので 最下部のAttach priv to user roles のタスクだけでも良さそうですが、こちらの community.postgresql.postgresql_membership はAnsibleの仕様で state: present で付与、 state: absent で削除しかできません。つまり仮に taro.yamadaread_write ロールを現状継承しているが、それを read_only に付け替えたいというケースで単純に Attach priv to user roles を実行しただけでは、 read_only に所属はしますが、 state: absent は実行されていないのでもともと継承していた read_write ロールにも所属したままになります。それを回避するために一度こちらのタスクで不要なロールをリセットしてあげる機構を取り入れています。(正直このmoduleを使っている他の方がどうやって運用を回しているのかとても気になります...毎回今の権限を確認して absent してから present しているのでしょうか...)また、 roles.roles[item.name].member_of が空の状態で state: absent を実行するとエラーでAnsibleは実行をやめてしまいます。それを回避するために以下の条件分岐を入れています。

{%- if not roles.roles[item.name].member_of -%}

その後は、item.groupが roles.roles[item.name].member_of と一致するかしないかで absent させるか present させるかを分岐させています。つまりタスク名上はResetとしていますが、厳密にはリセットだけではなく登録もすることになります。

{%- if roles.roles[item.name].member_of == item.group -%}

当該コード


- name: Reset priv to user roles
  community.postgresql.postgresql_membership:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    groups: >-
      {%- if not roles.roles[item.name].member_of -%}
        {{ item.group }}
      {%- else -%}
        {%- if roles.roles[item.name].member_of == item.group -%}
          {{ roles.roles[item.name].member_of }}
        {%- else -%}
          {{ roles.roles[item.name].member_of }}
        {%- endif -%}
      {%- endif -%}
    target_role: "{{ item.name }}"
    fail_on_role: no
    state: >-
      {%- if not roles.roles[item.name].member_of -%}
        present
      {%- else -%}
        {%- if roles.roles[item.name].member_of == item.group -%}
          present
        {%- else -%}
          absent
        {%- endif -%}
      {%- endif -%}
  with_items: "{{ test_db_user }}"
  when: item.name in roles.roles | list

- name: Attach priv to user roles
  community.postgresql.postgresql_membership:
    login_host: "{{ test_db_info.login_host }}"
    login_user: "{{ test_db_info.login_user }}"
    login_password: "{{ test_db_info.login_password }}"
    groups: "{{ item.group }}"
    target_role: "{{ item.name }}"
    fail_on_role: no
    state: "{{ item.group_state }}"
  loop: "{{ test_db_user }}"
  when: item.name in roles.roles | list

sendmail

こちらはオマケだと思っていただければと思います。sendgridでcredentialsに格納されたファイルを当該ユーザに送信します。ファイル名をemailアドレスにしておいたのはこちらが理由です。ファイルはローカルにしか作成されないので、sendgridが動作するリモートサーバにデータをコピーし最後に削除しています。

- name: Copy password file from local to remote
  copy:
    src: credentials/
    dest: /tmp/credentials
    remote_src: no
  check_mode: no

- name: get files
  find:
    paths: /tmp/credentials
    file_type: "file"
  register: find_result
  check_mode: no

- name: Send an email to a single recipient that the deployment was successful
  community.general.sendgrid:
    api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    attachments:  /tmp/credentials/{{ item.path | basename }}
    from_address: "test@example.com"
    to_addresses: "{{ item.path | basename | splitext | first }}"
    subject: "Database Password"
    body: "The most recent Ansible deployment was successful."
  #delegate_to: localhost
  with_items: "{{ find_result.files }}"

- file: path=/tmp/credentials/
        state=absent

オマケ

今回はわかりやすさ、取り回しやすさを重視して書きましたが、以下のような書き方でファイル数を削減したり環境をTaskで吸収したりもできますね。より深いネストになります。
2重までのネストはよく記事で出てくるのですが3重のネストはあまり出てこなかったのでご紹介です。

  • /ansible/group_vars/staging.sops.yaml
env: "staging"
staging:
  - db_name: test-db
    db_info:
      login_host: xxx.xxx.xxx.xxx
      login_user: postgres
      login_password: xxxxxxxxxxxxxxxxxxxxxxxx
    db_list:
      - database1
      - database2
    user_list:
      - name: yaro.yamada
        email: taro.yamada@example.com
        group: [read_only]
        state: present
        group_state: absent
  • ansible/roles/db-account/tasks/test_db.yaml
    loop のmainelement lookup('vars', env) がポイントです。
- name: Create read_only role
  community.postgresql.postgresql_user:
    login_host: "{{ item.0.db_info.login_host }}"
    login_user: "{{ item.0.db_info.login_user }}"
    login_password: "{{ item.0.db_info.login_password }}"
    db: "{{ item.1 }}"
    name: read_only
    no_password_changes: yes
    role_attr_flags: NOLOGIN
    fail_on_user: no
    state: present
  loop: "{{ lookup('vars', env) | subelements('db_list') }}"
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?