ansible でEC2 (Amazon Linux2) の SSH ポート番号を冪等性を担保しつつ変更する

# はじめに
冪等性を担保しつつ、ansibleでec2のSSH port番号を22から50001する手順をまとめました。

# 環境

- ローカル環境
    - M1 Mac
    - MacOS Monterey 12.6
- サーバ
    - Amazon Linux2


# ansibleインストール
ローカルにansibleをインストールします。
Homebrewでもインストールできますが、pipだと細かいバージョン指定ができるため、pipでのインストールを採用しました。
```terminal
最新バージョンをインストール場合
$ pip install ansible

バージョン指定
$ pip install ansible==6.5.0
```

インストールできたか確認するため、バージョンを確認します。
```terminal
$ pip list
Package      Version
------------ -------
ansible      6.5.0
ansible-core 2.13.6
```

# EC2を起動
EC2を起動しますが、起動方法は、省略します。
セキュリティーグループは、SSHポート番号の22とカスタムポート番号50001のインバウンドルールを許可します。
また、パブリック IP の自動割り当ては、有効化します。

![スクリーンショット 2022-11-22 23.24.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/57540429-58fc-7694-3e54-02964d2cee65.png)

# ansibleのプロジェクトを作成
playbookというディレクトリを作成し、その中に、hostsファイル、playbook.ymlファイル、sshdのrolesディレクトリを作成します。
```terminal
$ mkdir playbook
$ cd playbook
$ touch hosts playbook.yml
$ ansible-galaxy init --init-path="roles" sshd

$ tree     
.
├── hosts
├── playbook.yml
└── roles
    └── sshd
        ├── README.md
        ├── defaults
        │   └── main.yml
        ├── files
        ├── handlers
        │   └── main.yml
        ├── meta
        │   └── main.yml
        ├── tasks
        │   └── main.yml
        ├── templates
        ├── tests
        │   ├── inventory
        │   └── test.yml
        └── vars
            └── main.yml
```
使わないディレクトリを削除し、以下のようにします。
```terminal
$ tree
.
├── hosts
├── playbook.yml
└── roles
    └── sshd
        ├── handlers
        │   └── main.yml
        └── tasks
            └── main.yml

4 directories, 4 files
```
## hostsファイル作成
以下のように記載します。
本来なら、rolesのvarsのmain.ymlに`custom_ssh_port=50001`などを書くのがよいですが、面倒なので、hostsファイルにまとめました。
IPは、サーバーのパブリックIPを記載してください。
```hosts:hosts
[web] # 記載されたIPに対して接続
11.11.11.11

[web:vars] #[web]で使う変数を定義
ansible_ssh_port=22
ansible_user=ec2-user
__working_user=ec2-user
ansible_python_interpreter=/usr/bin/python2

# custom ssh port （49152～65535）
custom_ssh_port=50001

# pem key path
ansible_ssh_private_key_file=~/.ssh/test.pem
```

## playbook.yml作成

```yml:playbool.yml
- name: Change port
  hosts: web
  become: yes
  gather_facts: false
  roles:
    - sshd
```
次の節で`local_action`モジュールを使用するため、`gather_facts`は、`false`にする必要があります。

- gather_facts
    - 対象ホストの情報をansible_facts変数というものに格納し、tasks内の変数で使用することができます。
 


## rolesのsshdディレクトリ配下を作成
tasksとhandlersのmain.ymlのみ記載します。ほかは使いません。
```yml:roles/sshd/tasks
# port22 でSSH接続する。タイムアウトは５秒で、接続失敗しても次に進む
- name: Connect default ssh port
  local_action: wait_for port={{ansible_ssh_port}} timeout=5 host={{inventory_hostname}}
  register: default_port
  ignore_errors: true
  become: False

# port22 で失敗した場合、port 50001でSSH接続する。タイムアウトは５秒
- name: Connect custom ssh port
  local_action: wait_for port={{custom_ssh_port}} timeout=5 host={{inventory_hostname}}
  register: custom_port
  when: default_port.elapsed >= 5
  become: False

# port22 でタイムアウトし、port50001でSSH接続成功した場合、ansible_ssh_portを50001に変える
- name: set ansible_ssh_port custom_ssh_port
  set_fact: ansible_ssh_port={{custom_ssh_port}}
  when: default_port.elapsed >= 5 and custom_port.elapsed < 5
  become: False

# 現在port22でSSH接続している場合、/etc/ssh/sshd_configのPortを50001に書き換える
- name: Rewrite custom_ssh_port
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: '^#Port'
    line: 'Port {{custom_ssh_port}}'
  notify: Restart sshd
  when: ansible_ssh_port == 22

# ssh接続時のパスワード認証を無効
- name: Disabling password authentication
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: '^PasswordAuthentication'
    insertafter: '^#PasswordAuthentication'
    line: PasswordAuthentication no
  notify: Restart sshd
# ssh接続時のチャレンジ/レスポンス認証を無効
- name: Disabling Challenge-Response Authentication
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: '^ChallengeResponseAuthentication'
    insertafter: '^#ChallengeResponseAuthentication'
    line: ChallengeResponseAuthentication no
  notify: Restart sshd

# rootユーザーのログイン無効
- name: Disabling root user login
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    insertafter: '^#PermitRootLogin'
    line: PermitRootLogin no
  notify: Restart sshd
```


```yml:roles/sshd/handlers
# sshdの設定が変更された場合、再起動する
- name: Restart sshd
  service:
    name: sshd
    state: restarted
```

## モジュールの説明

- local_action
    - リモートホストではなくローカルで実行したい場合に使用します。今回であれば、ローカルからSSH接続できます。
- wait_for
    - SSH接続のアクション時、ポートが応答あるまで待つために使用します
        - timeoutやhost先を指定できます。
- register
    - 処理結果を変数の中に一時的に保管できます
    - elapsed
        - 経過時間を示します
- become
    - そのモジュールを root 権限で実行できます
- set_fact
    - 新たな変数を定義できます。
        - 今回は、ansible_ssh_portの値が元々22だったのをcustom_ssh_port（50001）に上書きしました
- lineinfile
    - ファイルを行単位で編集できます
    - dest
        - 編集するファイルのパスを指定
    - line
        - 置換または挿入する文字列を指定
    - regexp
        - 指定した正規表現にマッチした行を、lineで指定した文字列に置き換えます
        - lineで指定した文字列と、同じ場合は何もしません
    - insertafter
        - regexp にマッチする行が無かった場合、ここに指定した正規表現にマッチした次の行に、lineで指定した文字列を挿入する。
            - それにもマッチしない場合はファイル末尾に、lineで指定した文字列が挿入されます。
- notify
    - notifyを付与したタスクがchangedになった場合、handlers内に定義されたタスクが実行されます。
        - 同じhandlersの呼び出しは1回にまとめらます。notifyを付与している複数のタスクがchangedになっても１度しか実行されません。
# ansible実行

ルートディレクトリ配下で、下記コマンドにより、ansibleを実行できます。
```terminal
$ ansible-playbook -i hosts playbook.yml
```
詳細なログを出力したい場合、`-vv`をつけます
```terminal
$ ansible-playbook -i hosts playbook.yml -vv
```

１回目を実行します。
```terminal
$ ansible-playbook -i hosts playbook.yml -vv

PLAY [Change port] ************************************************************************************************************

TASK [sshd : Connect default ssh port] ****************************************************************************************
ok: [13.231.201.67 -> localhost]

TASK [sshd : Connect custom ssh port] *****************************************************************************************
skipping: [13.231.201.67]

TASK [sshd : set ansible_ssh_port custom_ssh_port] ****************************************************************************
skipping: [13.231.201.67]

TASK [sshd : Rewrite custom_ssh_port] *****************************************************************************************
The authenticity of host '13.231.201.67 (13.231.201.67)' can't be established.
ED25519 key fingerprint is SHA256:SgTQf176TUJ/wSqlWH4inIAZ7NWU+eAQrewxa+aqEns.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
changed: [13.231.201.67]

TASK [sshd : Disabling password authentication] *******************************************************************************
ok: [13.231.201.67]

TASK [sshd : Disabling Challenge-Response Authentication] *********************************************************************
ok: [13.231.201.67]

TASK [sshd : Disabling root user login] ***************************************************************************************
changed: [13.231.201.67]

RUNNING HANDLER [sshd : Restart sshd] *****************************************************************************************
changed: [13.231.201.67]

PLAY RECAP ********************************************************************************************************************
13.231.201.67              : ok=6    changed=3    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
```
１回目は、port22が使用されていることが分かります。
２回目を実行します。
```terminal
$ ansible-playbook -i hosts playbook.yml

PLAY [Change port] ************************************************************************************************************

TASK [sshd : Connect default ssh port] ****************************************************************************************
fatal: [13.231.201.67 -> localhost]: FAILED! => {"changed": false, "elapsed": 5, "msg": "Timeout when waiting for 13.231.201.67:22"}
...ignoring

TASK [sshd : Connect custom ssh port] *****************************************************************************************
ok: [13.231.201.67 -> localhost]

TASK [sshd : set ansible_ssh_port custom_ssh_port] ****************************************************************************
ok: [13.231.201.67]

TASK [sshd : Rewrite custom_ssh_port] *****************************************************************************************
skipping: [13.231.201.67]

TASK [sshd : Disabling password authentication] *******************************************************************************
ok: [13.231.201.67]

TASK [sshd : Disabling Challenge-Response Authentication] *********************************************************************
ok: [13.231.201.67]

TASK [sshd : Disabling root user login] ***************************************************************************************
ok: [13.231.201.67]

PLAY RECAP ********************************************************************************************************************
13.231.201.67              : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=1
```

２回目は、custom ssh portである50001ポートが使用されている事がわかります。

３回目を実行しましょう。
```terminal
ansible-playbook -i hosts playbook.yml

PLAY [Change port] ************************************************************************************************************

TASK [sshd : Connect default ssh port] ****************************************************************************************
fatal: [13.231.201.67 -> localhost]: FAILED! => {"changed": false, "elapsed": 5, "msg": "Timeout when waiting for 13.231.201.67:22"}
...ignoring

TASK [sshd : Connect custom ssh port] *****************************************************************************************
ok: [13.231.201.67 -> localhost]

TASK [sshd : set ansible_ssh_port custom_ssh_port] ****************************************************************************
ok: [13.231.201.67]

TASK [sshd : Rewrite custom_ssh_port] *****************************************************************************************
skipping: [13.231.201.67]

TASK [sshd : Disabling password authentication] *******************************************************************************
ok: [13.231.201.67]

TASK [sshd : Disabling Challenge-Response Authentication] *********************************************************************
ok: [13.231.201.67]

TASK [sshd : Disabling root user login] ***************************************************************************************
ok: [13.231.201.67]

PLAY RECAP ********************************************************************************************************************
13.231.201.67              : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=1
```
３回目もport 50001が使用されていますね。
また、`changed=0`であり、冪等性があることが分かります。

次回、ansibleのansible-lintを使用してコードをリファクタリング致します。

https://qiita.com/hirai-11/items/bb6c233da826b6bed04c

# 参考

https://qiita.com/satken2/items/0182d37fc71a7f240ad1

https://qiita.com/tiibun/items/02897ef788d21f3f05e0

https://dekitakotono.blogspot.com/2019/04/file.html

https://koh-sh.hatenablog.com/entry/2019/07/04/202824

https://qiita.com/ArimaRyunosuke/items/1f9d840311584d8160bc

https://densan-hoshigumi.com/server/playbook-maintainability

https://qiita.com/park-jh/items/8676ec76b8313357194f

https://qiita.com/yumenomatayume/items/56134b44a1244603e3a4