オートスケールで起動する際にIPアドレスごとの処理を実行してIP固定っぽい構成を作る。

# はじめに
クラウド環境では、特にオートスケールを行うような場合、サーバごとの色を出さず、使い捨てできるように設計するのがベストプラクティスとなります。

しかし、例えばIPアドレス固定で特定の認証処理を行っていたり、どうしてもIPアドレスごとに個別の処理を行う必要があったり、そのような制約によりオートスケールの構成にすることを断念するといったことが、特にオンプレミス環境からクラウド環境へリフト＆シフトするシステムで起こりがちな問題かと思います。

今回の構成はクラウド環境においてバッドプラクティス、アンチパターンの構成となるため、このような構成は極力避けるのが鉄則ですが、世の中そんなに簡単に変えられない場合も多いため、一つの案として紹介しようと思います。

# ホストごとに個別処理が必要だけどオートスケールしたい
今回記事を書いた背景として私が遭遇した問題の概要を紹介します。

とある案件で、仮想基盤環境からAWS環境へのリファクタリングを行った際、以前の環境から使用しているソフトウェアの制約でホストごとに個別のIDを割り振る必要があるということが発覚しました。

本来、クラウド環境に適した設計・構成へ変更するべきではありますが、変更した際のアプリケーションへの影響や残りのスケジュール等考えると、根本的な設計変更するのは難しく、なんとかオートスケールでスケールイン・スケールアウトだけはできるようにしたいということで実装したのが今回の背景です。

## ホストごとに個別処理が必要な際のオートスケールの問題
オートスケールは基となるテンプレートイメージからコンテナやEC2インスタンス等を作成し、負荷に応じて台数を増減させる機能となりますが、AWSで提供されているオートスケールの機能をそのまま使用する場合、テンプレートを別々にすることはできないため、事前にIPを固定させたイメージを用意しておくことができません。

`CloudWatch Alarm`を組み合わせることでオートスケールっぽい仕組みを作ることもできなくは無いと思いますが、できる限りAWSに実装されている機能をそのまま利用できるようにしたほうがAWSサービス側のサービスレベルに合わせられるので、今回はサーバ起動時にサーバ側でできる限りどうにかする方法を選択しました。

# IP固定っぽい構成の構築
今回の構成について以下より順に説明・構築していきます。

## 要件・制約の整理
まず、今回の要件と制約についてまとめてみます。

- **今回のシステム的な要件**
    - IPアドレスごとに個別IDを付与する必要がある
    - IPアドレスごとに個別処理を行いたい

- **AWSの機能的な制約**
    - オートスケールで利用できるテンプレートは１つのみ
    - EC2インスタンスへのIPアドレス割り振りはDHCPで割り振る必要がある
    - オートスケールで起動する際に指定のIPを割り振るといったことはできない

今回ベースとするシステム的な要件としては、例えば`10.1.20.4`といったIPアドレスを持ったサーバは`AAAA`というIDを付与し、`10.1.20.5`といったIPアドレスを持ったサーバは`BBBB`というIDを付与したいといった要件がありますが、AWSの機能的な制約として要件をそのまま実現することは難しいといった状況となりますのでその点を踏まえて以下より構成を考えてみます。

## 今回の構成
今回の構成としては以下のような構成となります。

オートスケールでの起動時、IPアドレスを固定で割り振れないので、固定で割り当てられないなら任意のIPアドレスしか割り振られないようにしてしまえという方針で、割り振られたIPアドレスに従って起動時に個別処理を実行するような構成としています。

![autoscale.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/7f89d7af-f50f-3331-a5d1-71782231d446.png)

ポイントはEC2インスタンスに割り当てられるIPアドレスを制限するため、オートスケール用のセグメントを用意し、「**サブネットCIDR予約**」機能で払い出されるIPアドレスを制限しているところと、サーバ起動時に割り振られたIPアドレスを判定するため、Route53の逆引きゾーンを指定したところになります。

## AWS側の設定

### オートスケール用サブネット作成
後述するサブネットCIDR予約にも関連しますが、EC2インスタンスに振り分けるIPアドレスを制限するため、オートスケールのインスタンスを配置するサブネットは専用のサブネットを用意したほうが良いでしょう。

そのため今回はオートスケール用のサブネットを作成するようにします。

3台程度であれば、本来`/29`のサブネットでも十分ですが、AWSでサブネットを作成する際の最小マスク値は`/28`なので、`/28`以上で設定を行ってください。

### サブネットCIDR予約
先ほど作成したサブネットに「**サブネットCIDR予約**」の設定を行っていきます。

登録は、EC2インスタンスにDHCPで割り振りたいIPアドレスを登録するのではなく、**EC2インスタンスにDHCPで割り振らないIPアドレスを登録**するので注意してください。

また、サブネットCIDR予約する際、以下のようにいくつかAWS側やIPネットワークの仕様的に予約されているアドレス等がありますが、そもそも予約アドレスはDHCPで割り当てられることはないため登録しなくて問題ありません。

- サブネットの１つ目のIPはネットワークアドレスのためDHCPで割り当てられない
- サブネットの２つ目〜4つ目のIPはAWS側で予約されているためDHCPで割り当てられない
- サブネットの最後のIPはブロードキャストアドレスのためDHCPで割り当てられない

設定方法など詳細を知りたい方は以下を参照してください。

- [EC2のプライベートIPアドレスを任意で割り当てる方法とサブネットCIDR予約](https://qiita.com/sakai00kou/items/83b08529e6b07cb06598)

なお、今回は以下のようにCIDR予約を設定しました。

![Monosnap_20241110_141834.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/4b7fd110-3e19-1f95-3a0f-eb033340f8d5.png)

### 逆引き用Route53ホストゾーン作成
私が担当した環境ではIPアドレスごとに個別のホスト名の登録等も行っていたため、EC2インスタンスに割り振られたIPアドレスを逆引きしてホスト名の設定等も行っておりました。

単純にEC2インスタンスに割り振られたIPアドレスだけわかれば良いのであれば、逆引き用Route53ホストゾーンの作成は行わなくても問題ありません。

逆引き用Route53ホストゾーンの作成の詳細を知りたい方は以下を参照してください、

- [AWSのRoute53のプライベートホストゾーンでDNSの逆引き設定をする。](https://qiita.com/sakai00kou/items/3c023d0307635eabeef9)

なお、今回は「**[今回の構成](#今回の構成)**」で記載した３台分の逆引きレコードを設定しました。

![Monosnap_20241110_142041.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/e62b1d65-bc27-28e0-8782-35ed03add376.png)

## サーバ側の設定
EC2のLinuxサーバで起動時に処理を行う場合、`cloud-init`でも行うことができますが、あまり柔軟性は高くないので、Amazon Linux 2023やRHEL系のLinuxサーバを使うのであれば、OS標準の`Systemd`で行うのが良いでしょう。

今回はオートスケール用の`Systemd`のサービスを作成して起動時に実行させるようにします。

### テンプレートサーバ作成
オートスケールでEC2インスタンスを起動する際の基となるテンプレートサーバを作成します。

今回は`Amazon Linux 2023`のサーバをテンプレートサーバとして作成して、後述の設定を仕込んでいきます。

### 起動時に実行するシェル
オートスケールで起動する際に実行するシェルを準備します。

実際に実装したシェルはもっと複雑ですが、今回はオートスケールで起動する際に割り振られたIPアドレスからホスト名を逆引きして、逆引きしたホスト名をEC2インスタンスのホスト名に設定する部分だけ抜き出してみます。

前述の逆引き用Route53ホストゾーンとレコード作成が前提となりますが、メタデータで取得したIPから逆引きして、取得したホスト名を設定するようにしています。

他にも起動時に設定したい処理があれば、追記してください。

```bash:/usr/local/sbin/AutoScaling.sh
#!/bin/bash
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
LOCAL_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-ipv4)
LOCAL_HOSTNAME=$(dig -x ${LOCAL_IP} +short)
hostnamectl set-hostname ${LOCAL_HOSTNAME}
```

最近のEC2はトークンを発行しないと`http://169.254.169.254/`でメタデータを取得できないため、トークンアクセスが必要な`IMDSv2`に対応した内容で記載します。

`IMDSv1`、`IMDSv2`の詳細を知りたい方は以下を参照してください。

- [インスタンスメタデータサービスを使用してインスタンスメタデータにアクセスする](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)

シェルはどこに置いてもいいですが、今回は`/usr/local/sbin`に作成し、シェル作成後、起動時に実行できるように権限を付与しておきます。

```bash:オートスケール起動時実行シェルの権限付与
chmod 755 /usr/local/sbin/AutoScaling.sh
```

### Systemd設定
先ほど作成したシェルを起動時に実行させるため、`Systemd`のサービスファイルを作成し、実行時にシェルを実行するようにします。

私が担当したシステムでは、Webサービスが起動する前にWebサービスで使用する設定ファイルの配置やホスト名設定を行う必要があったため、サービスの起動順も考える必要がありました。

そのため、今回は以下のような設定を行いました。

```bash:/etc/systemd/system/autoscaling.service
[Unit]
Description=AutoScaling setup service
After=network-online.target
Before=httpd.service

[Service]
Restart=no
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/AutoScaling.sh

[Install]
WantedBy=network-online.target
```

以下各設定項目の内容概要。

|項目|設定|備考|
|:--|:--|:--|
|Description|AutoScaling setup service|適当に分かりやすい名前を指定|
|After|network-online.target|ネットワーク起動後にサービス起動させたいので設定|
|Before|httpd.service|本サービスの後に起動したいサービスを設定|
|Restart|no|待ち受けサービスではないのでno|
|Type|oneshot|起動時1回のみ実行のためoneshotで設定|
|RemainAfterExit|yes|oneshotで状態追跡しなくていいのでyesで設定|
|ExecStart|/usr/local/sbin/AutoScaling.sh|起動時に実行したいシェルを設定|
|WantedBy|network-online.target|先にネットワーク起動していないと通信できていないため設定|

`oneshot`で設定することで、起動時に1回だけ実行する設定となり、`Before`で上記サービスが起動した後に実行したいサービスを指定しています。

今回の場合、`httpd.service`を指定していますが、特に`httpd.service`側の`Systemd`ファイルで`After`の指定をしなくても問題ありません。

なお、オートスケール停止時にも何かしらの処理を行いたい場合は、`ExecStop`で停止時に実行したいシェル等を登録すると停止時にも処理を行うことができます。

サービスを作成したら以下コマンドで起動時に実行できるように設定しておきましょう。

```bash:作成したautoscaling.serviceの有効化
systemctl enable autoscaling.service
```

### テンプレートサーバのAMI作成
EC2インスタンス起動時に処理を実行する設定を加えたテンプレートサーバのAMIを作成します。

EC2ダッシュボードからテンプレートサーバを選択し、「**アクション**」→「**イメージとテンプレート**」→「**イメージを作成**」からAMIを作成していきます。

![Monosnap_20241110_133039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/bf52e6d1-eca3-5e79-2c77-0bc4f538b60f.png)

### 起動確認
EC2オートスケールの設定を行うためには「**Auto Scalingグループ**」で設定する必要がありますが、オートスケールの設定は本来の目的と異なるため、今回は、作成したAMIを使って適当なEC2インスタンスを起動して、先程テンプレートサーバに設定した起動時処理が動くかどうかだけ確認しようと思います。

EC2ダッシュボードの「**インスタンス**」→「**インスタンスを起動**」から先ほど作成したAMIを指定して起動してみます。

起動後`hostnamectl`コマンドで確認してみたところ、今回は`testinstance1`で指定した`10.1.20.4`がDHCPで割り振られて、シェルに記載したホスト名設定の処理が動き、逆引きレコードに設定したホスト名が想定通り設定されました。

```bash:起動後のホスト名確認
[ec2-user@testinstance1 ~]$ hostnamectl
 Static hostname: testinstance1.example.local
       Icon name: computer-vm
         Chassis: vm 🖴
      Machine ID: ec2d53c3610a11b486cedcf5608693a6
         Boot ID: 4c27fc819d5045398e2e95f855100472
  Virtualization: xen
Operating System: Amazon Linux 2023.6.20241031
     CPE OS Name: cpe:2.3:o:amazon:amazon_linux:2023
          Kernel: Linux 6.1.112-124.190.amzn2023.x86_64
    Architecture: x86-64
 Hardware Vendor: Xen
  Hardware Model: HVM domU
Firmware Version: 4.11.amazon
```

また、`autoscaling.service`で指定した起動順を確認するため、EC2起動時の各サービス起動までの時間とタイミングを確認してみると、先ほど`autoscaling.service`内で指定した通り、`Before`で指定した`httpd.service`が`autoscaling.service`の後に実行されていることが確認できます。

![Monosnap_20241110_143545.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/3d353267-ef40-6095-31d4-c84aff832172.png)

今回は見た目分かりやすくするため、先ほど記載したシェルの最後に`sleep 10`を追加して、`autoscaling.service`の後に`httpd.service`が実行されていることが分かりやすいようにしております。

ちなみに上記のようにサービス起動時間、タイミングをグラフ形式で確認するには以下コマンドを実行して出力されたHTMLファイルを適当なWebブラウザで見ると確認することができます。

```bash:サービス起動時間とタイミングのグラフ出力
systemd-analyze plot > service_start.html
```

`Systemd`に対応しているOSであればどれでも出力でき、どのサービスが起動に時間がかかっているかなど確認できるので調査する際に便利です。

# おわりに
いわゆるオンプレミスのサーバに固定でIPを設定する構成とは異なりますが、AWSのサービスとOS側の設定を組み合わせることで、IPアドレス等の制約があるサーバも何とかオートスケールできるようにすることもできるという例を紹介させて頂きました。

もちろんシステムごとに事情は異なることから、今回のような仕組みを導入しても実現できない例はたくさんあると思うので、このような方法もあるという一例として参考にしてもらえればと思います。

今回はEC2オートスケールで設定する方法を紹介しましたが、イメージに起動シェルを仕込んでおけばECS・Fargateでもできるかと思いますので、ECSでやりたい方は挑戦してみてください。