Webアプリを作って自宅のサーバーでインターネットに公開してみた（インターネット公開編）

## はじめに

普段は主にクラウドサービスを使っているのですが、「ローカル環境のマシンをインターネットに公開する場合どうするんだろう」と思ったので勉強を兼ねてやってみました。

長くなったので

- インターネット公開編
- アプリ編

の2つに分けて書きました。これは**インターネット公開編**です。
アプリ編は[こちら](https://qiita.com/nijigen_plot/items/ccd99f72b040d1683500)です。

## アプリ紹介

https://home.quark-hardcore.com/kick-preview/


<iframe width="500" height="700" src="https://www.youtube.com/embed/lsmtIWlziEE" title="ガバキックが沢山聴けるアプリを作りました #shorts" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

ボタンを押すと曲（のキック）がランダムに1秒流れるアプリです。


## 全体構成

![system_architecture_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635079/366d1b15-386f-3863-bdb2-7ac8cdf35da5.png)

全体の構成は上記のような形です。
今記事では主に黄色枠部分について記載します。

## 環境


- サーバー : [Raspberry Pi 5 8GB](https://raspberry-pi.ksyic.com/main/index/pdp.id/1015,1016,1062,1063,1064,1056,1057,1058/pdp.open/1016/)
    - OS : Debian 12
- ルーター : [TP-Link Archer AX55](https://www.tp-link.com/jp/home-networking/wifi-router/archer-ax55/)
- フロント&バックエンドライブラリ : Reflex
- DB : MariaDB
- CRUD API : Flask
- Webサーバーアプリ : Apache2
- クラウドストレージ : Amazon S3
- SSL証明書 : Let's Encrypt
- DNS : Lightsail DNS

## 設定

### ルーター設定

TP-Linkルーターの管理画面から設定を行います。

#### アドレス予約

DHCPサーバーが有効になっており、サーバーが再起動するとローカルIPアドレスが変更される可能性があるので、MACアドレスを指定しローカルIPアドレスの予約設定をします。


![20241019.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635079/c7d54634-919a-de07-6182-bb539acc7f1d.png)


#### ポートフォワーディング設定

3つのポートを使用したいので、以下のように設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635079/a4aa2d43-757d-2878-6167-34e36b85b66b.png)

これにより、グローバルIPの特定ポートにアクセスが来た時、先ほど設定したローカルIPアドレスの特定ポートに転送されます。


#### DNS設定

私は数年前からドメインをお名前.comで購入し所有しています。
それをLightsail DNSに登録して管理しています。

https://docs.aws.amazon.com/ja_jp/lightsail/latest/userguide/amazon-lightsail-domain-registration.html


グローバルIPを以下コマンドで確認し、

```bash
$ curl -4 ifconfig.co
xxx.xxx.xxx.xxx
```

AレコードにドメインとグローバルIPを記載して登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635079/b0230cf2-3f01-b588-3cc9-488121858ef5.png)

#### 動的グローバルIPアドレスへの対処

グローバルIPアドレスですが、個人契約の場合基本的には動的になっており、ある程度期間が立つと変わるようです。

https://livra.geolocation.co.jp/iplearning/872/

Lightsail DNSには[cliが提供されている](https://docs.aws.amazon.com/cli/latest/reference/lightsail/update-domain-entry.html)ので、cronでグローバルIPアドレスをフェッチし、DNSをUpdateするスクリプトを作成・実行します。

https://docs.aws.amazon.com/cli/latest/reference/lightsail/update-domain-entry.html

```bash: update_grobal_ip.sh
#!/bin/bash

dns_setting_json=`/usr/local/bin/aws lightsail get-domain --domain-name "${MY_DOMAIN}" --region us-east-1 | /usr/bin/jq '.domain.domainEntries[] | select(.name == "home.'"${MY_DOMAIN}"'" and .type == "A")'`
ip=`/usr/bin/curl -4 -s ifconfig.co`
updated_dns_setting_json=$(echo "$dns_setting_json" | /usr/bin/jq --arg new_ip "$ip" '.target = $new_ip')

/usr/local/bin/aws lightsail update-domain-entry --domain-name "${MY_DOMAIN}" --region us-east-1 --domain-entry "$updated_dns_setting_json"
```

```:crontab
*/10 * * * * /home/xxx/update_grobal_ip.sh >> /home/xxx/update_grobal_ip.log 2>&1
```

数分のダウンタイムはありますが、これでグローバルIPアドレスが変わっても大丈夫です。

これで指定のドメインからアクセスできるようになります。
```bash
# 諸々設定済みの為301が返ってきていますが、本来は200が返ってきます。
# 尚、完成してから記事を書いているので、本来はアプリサーバー側ポートもLISTENしていないと通信成功しません。
$ curl -I curl -I  http://home.quark-hardcore.com/kick-preview/
curl: (6) Could not resolve host: curl
HTTP/1.1 301 Moved Permanently
Date: Sat, 19 Oct 2024 06:34:53 GMT
Server: Apache/2.4.62 (Debian)
Location: https://home.quark-hardcore.com/kick-preview/
Content-Type: text/html; charset=iso-8859-1
```

#### TP-Link DDNS設定について

TP-LinkにはDDNS機能があります。これを使えば動的なグローバルIPアドレスであっても、TP-Linkが提供するドメインでインターネットから接続できるのですが、ルーターにSSL証明書のインポート機能がなく、**httpsの接続は対応していません**

httpsでの通信とDDNSは全く違う話ですが、TP-LinkがDDNSで提供する`*.tplinkdns.com`については

「ルーターにSSL証明書がインポートできない→TP-LinkからDDNS機能により提供されるドメインではhttps通信ができない」

という認識です。間違っていたら教えていただきたいです。

https://community.tp-link.com/us/home/forum/topic/163336

ということで、**TP-LinkのDDNSは使っていない**です。

### SSL証明書

[ルーターの設定](#ルーター設定)により特定のドメインから自宅のサーバーへアクセスさせることが可能になりました。
自宅のサーバーにSSL証明書をインポートし、https通信ができるようにします。

#### Let's Encrypt証明書を作成

無料でSSL証明書を作成できる[Let's Encrypt](https://letsencrypt.org/ja/)を使います。


手順はこちらが分かりやすいです。

https://www.inmotionhosting.com/support/website/ssl/lets-encrypt-ssl-ubuntu-with-certbot/

また、事前にnginx or apache2どちらかを先にインストールします。
Lightsailインスタンスに入っているBitnamiで使ったことがある[Apache2](https://httpd.apache.org/)を選択しました。

```bash:Certbotインストール
$ sudo apt update
$ sudo apt install apache2
$ sudo apt install python3 python3-venv libaugeas0
$ sudo python3 -m venv /opt/certbot/
$ sudo /opt/certbot/bin/pip install --upgrade pip
$ sudo /opt/certbot/bin/pip install certbot certbot-apache
$ sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot
```

```bash:CertbotでSSL証明書を作成
$ sudo certbot --apache
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Please enter the domain name(s) you would like on your certificate (comma and/or
space separated) (Enter 'c' to cancel): home.quark-hardcore.com
Requesting a certificate for home.quark-hardcore.com
                                                                                                      Successfully received certificate.
This certificate expires on 2024-12-30.
These files will be updated when the certificate renews.

Deploying certificate
Successfully deployed certificate for home.quark-hardcore.com to 
Congratulations! You have successfully enabled HTTPS on https://home.quark-hardcore.com

NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
                                                                                                      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```

Let's Encryptは有効期間が90日と短いので、定期的に更新するようcronで設定しておきます。

```crontab
0 3 1 * * /usr/bin/certbot renew --quiet
```

これでhttpsでの通信が可能になりました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635079/faf6151c-71b9-09b3-91c9-c7f7a6df14c9.png)

通信成功です！

```
$ curl -I  https://home.quark-hardcore.com/kick-preview/
HTTP/1.1 200 OK
Date: Sat, 19 Oct 2024 06:35:22 GMT
Server: Apache/2.4.62 (Debian)
Cache-Control: no-store, must-revalidate
X-Powered-By: Next.js
ETag: "14m1wwi91dj5y7"
Content-Type: text/html; charset=utf-8
Content-Length: 7745
Vary: Accept-Encoding
```