{"contents":[{"id":"5ji_zvbxg2","createdAt":"2021-05-04T09:51:12.294Z","updatedAt":"2021-07-16T10:09:30.867Z","publishedAt":"2021-05-04T09:51:12.294Z","revisedAt":"2021-07-16T10:09:30.867Z","title":"ubuntu-20.04.20-desktop-amd64をVMware-workstation-12.5.5にインストール","category":{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},"topics":[{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\nWindows 10 Pro x64  \nVMware-workstation-12.5.5  \nubuntu-20.04.2.0-desktop-amd64.iso  \nの組み合わせで作業しています。  \n・OSのインストール  \n・IPアドレスの設定  \n・日本語キーボードの設定\n・teratermでsshでログイン  \nまで行います。  \n\n<br />\n\n# Ubuntu 20.04.2.0 インストール\n\n  \n## １\n  \nisoを選択して簡易インストールを使用します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image1.png\n\"\")  \n\n<br />\n\n  \n## ２\n  \n<blockquote class=\"info\">\n<p>ここで入力する内容は、以下の関係のようです。</p>\n<p>フル ネーム : 表示名（ログイン画面の名前）</p>\n<p>ユーザー名 : システムで管理するアカウントID(chown xxx 等に使うユーザー名。)</p>\n<p>パスワード : 後述する６の画面があったとしても採用されるログインパスワード</p>\n</blockquote>\n<blockquote class=\"alert\">\n<p style=\"color: red;\">ユーザー名を「admin」とすると、後述する６の画面が表示されます。ここでは、あえて、「admin」を指定しています。</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image2.png\n\"\")  \n\n<br />\n  \n## ３\n  \n仮想マシンの名前と保存先を決めます。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image3.png\n\"\")  \n\n<br />\n\n## ４\n  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image4.png\n\"\")  \n\n<br />\n\n  \n## ５\n  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image5.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image6.png\n\"\")  \n\n<br />\n\n<blockquote class=\"warn\">\n<p>環境によってはここでかなり時間がかかります。</p>\n</blockquote>\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image7.png\n\"\")\n\n<br />\n\n  \n## ６\n  \n<blockquote class=\"alert\">\n<p style=\"color: red;\">２でユーザー名を「admin」としたため、ユーザー名を「admin」にできないことを知らせる確認画面が表示されます。「admin」としない場合、この画面は表示されません。</p>\n<p>ここで入力する内容は、以下の関係のようです。</p>\n<p>Your name : 表示名（ログイン画面の名前）</p>\n<p>Pick a username : システムで管理するアカウントID(chown xxx 等に使うユーザー名。)</p>\n<p>Choose a password : ２で設定したパスワードが採用され、反映されないようです。</p>\n</blockquote>\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/whoareyou1.jpg\n\"\")  \n\n<br />\n\n「Require my password to log in」にチェックを入れて、ログイン時にパスワード入力を求めるようにします。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/whoareyou2.jpg\n\"\")  \n\n<br />\n\n  \n## ７\n  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image9.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image10.png\n\"\")  \n\n<br />\n\n<blockquote class=\"alert\">\n<p>VMware-workstation-12.5.5 </p>\n<p>Windows10 PRO 1909</p>\n<p>Ryzen 7 1800X</p>\n<p>Mem:16GB</p>\n<p>SSD</p>\n<p>のＰＣの場合、なぜかここで止まりました。（２回行い、２回とも）</p>\n<p>VMware Workstation 16 Pro の場合は、問題ありませんでした。ドライバか何かの兼ね合いがあるかもしれません。</p>\n</blockquote>\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image11.png\n\"\")  \n\n<br />\n\n# インストール完了からＩＰアドレス設定まで\n\n  \n## ８\n  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image12.png\n\"\")  \n\n<br />\n\n２で入力した「フル ネーム」、あるいは、６で入力した「Your name」をクリックして、２で入力したパスワードでログインします。  \n<blockquote class=\"warn\">\n<p>なお、Not listed?をクリックすると、ユーザー名の入力を求められます。ここで入力するのは、２で入力した「ユーザー名」、あるいは、６で入力した「Pick a username」です。</p>\n</blockquote>\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image13.png\n\"\")  \n\n<br />\n\n  \n## ９\n  \n後で設定できるため、Skipを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image14.png\n\"\")  \n\n<br />\n\n  \n## １０\n  \nNextを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image15.png\n\"\")  \n\n<br />\n\n  \n## １１\n  \nUbuntuに端末の情報を送るかですが、Noを選択し、Nextを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image16.png\n\"\")  \n\n<br />\n\n  \n## １２\n  \n位置情報の使用を許可するか→しないとし、Nextを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image17.png\n\"\")  \n\n<br />\n\n  \n## １３\n  \nDoneを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image18.png\n\"\")  \n\n<br />\n\n## １４\n  \nDHCPから取得したＩＰアドレスでインターネットにつながっていました。  \nそのため、最新のアップデートを適用するかのダイアログが開きました。  \n「Install Now」を選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image19.png\n\"\")  \n\n<br />\n\n  \n## １５\n  \nWired Conected -> Wired Settings を選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image20.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image21.png\n\"\")  \n\n<br />\n\n  \n## １６\n  \nConnected - 1000 Mb/s の右側にある歯車のマークをクリックします。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image22.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image23.png\n\"\")  \n\n<br />\n\n「IPv4」を選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image24.png\n\"\")  \n\n<br />\n\n「Manual」を選択し、  \nAddress : IPアドレス  \nNetmask : サブネットマスク  \nGateway : デフォルトゲートウェイ（ルーター）  \nDNS : DNSサーバー  \nを入力し、Applyを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image25.png\n\"\")  \n\n<br />\n  \n## １７\n  \n\nConnected - 1000 Mb/s の右側にあるトグルスイッチを ON->OFF->ON に切り替えて、先ほど設定したIPアドレスを有効にします。（ネットワーク設定の再読み込み）  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image26.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image27.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image28.png\n\"\")  \n\n<br />\n\n# 日本語キーボードの設定\n\n  \n## １８\n  \n画面左下を選択し、検索に「settings」と入力し、表示された「Settings」のアイコンを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image29.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image3.png\n\"\")  \n\n<br />\n  \nまたは、デスクトップを右クリックし、「Settings」を選択します。\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image4.png\n\"\")  \n\n<br />\n\n## １９\n  \n「Region & Language」を選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image5.png\n\"\")  \n\n<br />\n\n## ２０\n  \n「Input Sources」の ＋ボタンのところを選択します。\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image6.png\n\"\")  \n\n<br />\n\n## ２１\n\n「Other」を選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image7.png\n\"\")  \n\n<br />\n\n## ２２\n  \n「Japanese」を選択します。  ※検索を利用すると素早く選択できます。\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image8.png\n\"\")  \n\n<br />\n\n## ２３\n  \n「Add」ボタンを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image9.png\n\"\")  \n\n<br />\n\n## ２４\n  \nEnglish (US) の右側のゴミ箱ボタンをクリックします。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_image10.png\n\"\")  \n\n<br />\n  \nこの状態になれば、日本語キーボード入力に切り替わります。\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/kb_english_delete.jpg\n\"\")  \n\n<br />\n\n# SSHの有効化\n\n  \n## ２５\n  \n画面左下を選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image29.png\n\"\")  \n\n<br />\n  \n## ２６\n  \n検索に「ter」と入力し、表示された「Terminal」のアイコンを選択します。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image30.png\n \"\")  \n\n<br />\n  \n## ２７\n  \nコマンド入力して、ssh server をインストールします。  \n\n<br />\n\n```sh\n$ sudo passwd root\n```\n\n<blockquote class=\"info\">\n<p>rootのパスワードを設定して、この後root権限でインストールします。</p>\n</blockquote>\n\n<br />\n  \n\n```sh\n$ su\n# apt update\n# apt -y install openssh-server\n```\n\n<blockquote class=\"warn\">\n<p>１４で「Install Now」を選択した場合、apt updateは不要のようです。(「Install Now」で内部的に既に実行されていると思われます。)</p>\n</blockquote>\n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image31.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image32.png\n\"\")  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image33.png\n\"\")  \n\n<br />\n\n  \n## ２８  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image35.png\n\"\")  \n\n<br />\n\n無事ログインできました！  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image36.png\n\"\")\n","description":"Windows 10 Pro x64 VMware-workstation-12.5.5 ubuntu-20.04.2.0-desktop-amd64.iso の組み合わせで作業しています。 ・OSのインストール ・IPアドレスの設定 ・日本語キーボードの設定 ・teratermでsshでログイン まで行います。 Ubuntu 20.04.2.0 インストール １ isoを選択して簡易インストールを使用します。 ２ ここで入力する内容は、以下の関係のようです。 フル ネーム : 表示名（ログイン画面の名前） ユーザー名 : システムで管理するアカウントID(chown xxx 等に使うユーザー名。) パスワード : 後述する６の画面があったとしても採用されるログインパスワード ユーザー名を「admin」とすると、後述する６の画面が表示されます。ここでは、あえて、「admin」を指定しています。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"h5pltlsmh9","createdAt":"2021-07-16T10:05:08.747Z","updatedAt":"2021-07-16T10:05:08.747Z","publishedAt":"2021-07-16T10:05:08.747Z","revisedAt":"2021-07-16T10:05:08.747Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/ITC_Engineering_Blog.png","alt":"ubuntu-20.04.20-desktop-amd64をVMware-workstation-12.5.5にインストール","width":1200,"height":630}],"seo_authors":[]},{"id":"ubuntu-kubernetes","createdAt":"2022-01-31T12:55:33.946Z","updatedAt":"2022-11-25T02:38:58.797Z","publishedAt":"2022-01-31T12:55:33.946Z","revisedAt":"2022-11-25T02:38:58.797Z","title":"Ubuntu 20.04 LTSのオンプレにKubernetes環境構築からnginx Pod稼働まで","category":{"id":"h0khu9sd3","createdAt":"2022-01-31T12:51:23.646Z","updatedAt":"2022-01-31T12:51:23.646Z","publishedAt":"2022-01-31T12:51:23.646Z","revisedAt":"2022-01-31T12:51:23.646Z","topics":"Kubernetes","logo":"/logos/Kubernetes.png","needs_title":false},"topics":[{"id":"h0khu9sd3","createdAt":"2022-01-31T12:51:23.646Z","updatedAt":"2022-01-31T12:51:23.646Z","publishedAt":"2022-01-31T12:51:23.646Z","revisedAt":"2022-01-31T12:51:23.646Z","topics":"Kubernetes","logo":"/logos/Kubernetes.png","needs_title":false},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false},{"id":"9iy1ks71tv7n","createdAt":"2021-05-31T13:08:18.404Z","updatedAt":"2021-08-31T12:04:47.612Z","publishedAt":"2021-05-31T13:08:18.404Z","revisedAt":"2021-08-31T12:04:47.612Z","topics":"Nginx","logo":"/logos/Nginx.png","needs_title":false}],"content":"# はじめに\n\nインストールしたての Ubuntu 20.04 LTS のオンプレ環境に Kubernetes 環境（マスターノードとワーカーノード）を構築し、nginx の Pod 稼働まで確認しました。今回、その全手順を書きたいと思います。  \nUbuntu 20.04 LTS のオンプレ環境は、マスターノードとワーカーノードともに過去記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/5ji_zvbxg2\" target=\"_blank\">ubuntu-20.04.20-desktop-amd64 を VMware-workstation-12.5.5 にインストール</a>」の手順で、VMware 上にインストールしたものです。\n\n<br />\n\n完成図は、以下です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes1.png\" alt=\"Ubuntu Kubernetes 完成図\" width=\"644\" height=\"901\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n**目次**  \n[Docker インストール](#anchor1)  \n[Kubernetes インストール](#anchor2)  \n[マスターノード構築準備](#anchor3)  \n[マスターノード構築](#anchor4)  \n[ワーカーノード追加](#anchor5)  \n[Flannel インストール](#anchor6)  \n[Pod 追加](#anchor7)  \n[Service 追加](#anchor8)  \n[トラブルシュートまとめ](#anchor9)\n\n<br />\n\nなお、今回、<span style=\"color :red;\">インターネットに直接繋がる環境ではなく、Proxy サーバーを通して、インターネットに繋がります。<strong>Proxy 越しネット環境の手順</span>になります。</strong>  \n今回の手順中、HTTP プロキシ：`http://192.168.0.158:3128/`、HTTPS プロキシ：`http://192.168.0.158:3128/`とします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes2.png\" alt=\"Ubuntu Kubernetes プロキシ環境 図\" width=\"825\" height=\"82\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>【マスターノード環境】</p>\n<p><code>VMware Workstation Pro 16</code></p>\n<p>　<code>Ubuntu 20.04.2 LTS</code></p>\n<p>　　<code>Docker 20.10.12</code></p>\n<p>　　<code>Kubernetes 1.23</code></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>【ワーカーノード環境】</p>\n<p><code>VMware Workstation Pro 16</code></p>\n<p>　<code>Ubuntu 20.04.2 LTS</code></p>\n<p>　　<code>Docker 20.10.12</code></p>\n<p>　　<code>Kubernetes 1.23</code></p>\n<p>　　　<code>nginx:1.21.6-alpine</code></p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>インストールは、全てroot権限で行っていますので、sudoは省略しています。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# Docker インストール\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes3.png\" alt=\"Ubuntu Kubernetes Docker インストール 図\" width=\"599\" height=\"199\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nDocker をインストールします。\n\n<br />\n\n<span style=\"color :red;\">apt,curl が Proxy 越しにインターネットへ出られるように</span>、プロキシサーバーの環境変数をセットします。\n\n```shell-session\n# export http_proxy=\"http://192.168.0.158:3128\"\n# export https_proxy=\"http://192.168.0.158:3128\"\n```\n\n<br />\n\ndocker インストールに必要なパッケージをインストールします。\n\n```shell-session\n# apt update\n# apt install -y apt-transport-https ca-certificates curl software-properties-common\n```\n\n<br />\n\ndocker リポジトリ鍵を登録します。\n\n```shell-session\n# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n```\n\n<br />\n\ndocker リポジトリを登録します。\n\n```shell-session\n# add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\"\n# apt update\n```\n\n<br />\n\ndocker をインストールします。\n\n```shell-session\n# apt install -y docker-ce\n```\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;background-color: cornsilk;\">E: Package 'docker-ce' has no installation candidate</span></p>\n<p>エラーになる時は、stable（安定）版が無いため、docker-ce edge版かtest版が必要のようです。</p>\n<p><code># add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable edge test\"</code></p>\n<p>でやり直しが必要です。</p>\n</blockquote>\n\n```shell-session\n# docker -v\nDocker version 20.10.12, build e91ed57\n```\n\n<br />\n\ndocker が起動しているか確認します。\n\n```shell-session\n# systemctl status docker\n● docker.service - Docker Application Container Engine\n     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)\n     Active: active (running) since Sat 2022-01-29 00:50:38 PST; 4min 44s ago\n```\n\nＯＫです。\n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# Kubernetes インストール\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes4.png\" alt=\"Ubuntu Kubernetes kubelet kubeadm kubectl インストール 図\" width=\"599\" height=\"238\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nKubernetes をインストールします。「Kubernetes をインストール」と言っても、実際のところ、`kubelet`と`kubeadm`と`kubectl`をインストールになります。\n\n<blockquote class=\"info\">\n<p><strong><code>kubelet</strong></code>: Pod の起動や管理を行うサービスです。  \n<p><strong><code>kubeadm</strong></code>: Kubernetes クラスターを構築するコマンドラインツールです。  \n<p><strong><code>kubectl</strong></code>: クラスターにアクセスして各種操作を行うコマンドラインツールです。kube-apiserver の API にアクセスして、各種操作を行っています。API は、curl コマンドでもアクセスできますが、API の仕様を調べないといけなく、kubectl を使った方が楽なので、インストールします。\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>通常、マスターノードに<code>kubelet</code>は有りませんが、<code>kubeadm</code>が必要とするため、このやり方の場合、マスターノードにも存在します。</p>\n</blockquote>\n\nkubernetes リポジトリ鍵を登録します。\n\n```shell-session\n# curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg\n```\n\n<br />\n\nkubernetes リポジトリを登録します。\n\n```shell-session\n# echo \"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main\" | tee /etc/apt/sources.list.d/kubernetes.list\n# apt update\n```\n\n<br />\n\n`kubelet`と`kubeadm`と`kubectl`をインストールします。\n\n```shell-session\n# apt install -y kubelet kubeadm kubectl\n```\n\n<br />\n\n`kubelet`と`kubeadm`と`kubectl`が`apt upgrade`で更新されないようにします。\n\n```shell-session\n# apt-mark hold kubelet kubeadm kubectl\n```\n\n<br />\n\n確認します。\n\n```shell-session\n# kubelet --version\nKubernetes v1.23.3\n# kubeadm version\nkubeadm version: &version.Info{Major:\"1\", Minor:\"23\", GitVersion:\"v1.23.3\", GitCommit:\"816c97ab8cff8a1c72eccca1026f7820e93e0d25\", GitTreeState:\"clean\", BuildDate:\"2022-01-25T21:24:08Z\", GoVersion:\"go1.17.6\", Compiler:\"gc\", Platform:\"linux/amd64\"}\n# kubectl version\nClient Version: version.Info{Major:\"1\", Minor:\"23\", GitVersion:\"v1.23.3\", GitCommit:\"816c97ab8cff8a1c72eccca1026f7820e93e0d25\", GitTreeState:\"clean\", BuildDate:\"2022-01-25T21:25:17Z\", GoVersion:\"go1.17.6\", Compiler:\"gc\", Platform:\"linux/amd64\"}\nThe connection to the server localhost:8080 was refused - did you specify the right host or port?\n```\n\nヨシ！\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;background-color: cornsilk;\">The connection to the server localhost:8080 was refused - did you specify the right host or port?</span></p>\n<p>は、サーバー側（kube-apiserver）のバージョンを表示しようとして、サーバー側に接続できていないエラーですが、この段階では、準備できていないだけのため、無視します。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# マスターノード構築準備\n\n`kubeadm init`によって、マスターノードを構築しますが、いきなり行うと、エラーになりますので、その前の準備を行います。\n\n<br />\n\n**docker のプロキシを設定**します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes5.png\" alt=\"Ubuntu Kubernetes Docker Proxy設定 図\" width=\"819\" height=\"82\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<span style=\"color: red;\">プロキシサーバーを使わない場合、必要の無い手順です</span>が、`kubeadm init`の時に、`docker pull`が動きます。これのエラー回避になります。\n\n```shell-session\n# mkdir -p /etc/systemd/system/docker.service.d\n# echo -e \"[Service]\\nEnvironment=HTTP_PROXY=http://192.168.0.158:3128/ HTTPS_PROXY=http://192.168.0.158:3128/\" | tee /etc/systemd/system/docker.service.d/http-proxy.conf\n```\n\n<br />\n\n設定を反映して、docker を再起動します。\n\n```shell-session\n# systemctl daemon-reload\n# systemctl restart docker\n```\n\n<br />\n\n**docker の cgroup driver を変更**します。  \nkubelet の systemd なのに対し、docker の cgroup driver は、cgroupfs のため、systemd に合わせます。\n\n<blockquote class=\"info\">\n<p>【 cgroup 】</p>\n<p>プロセスグループのリソース(CPU、メモリ、ディスクI/Oなど)の利用を制限・隔離するLinuxカーネルの機能です。</p>\n<p>systemdはcgroupと密接に統合されており、プロセスごとにcgroupを割り当てます。</p>\n</blockquote>\n\n```shell-session\n# vi /etc/docker/daemon.json\n```\n\n```json\n{\n  \"exec-opts\": [\"native.cgroupdriver=systemd\"]\n}\n```\n\n<br />\n\n設定を反映して、docker を再起動します。\n\n```shell-session\n# systemctl daemon-reload\n# systemctl restart docker\n```\n\n<br />\n\n**swap を無効**にします。\n\n<blockquote class=\"info\">\n<p>Warningレベルで、必須ではないようですが、Kubernetesはswapがあることを前提に設計されていなく、セキュリティやパフォーマンスに問題が生じるようです。公式サイトでも必ずoffにしてくださいと書かれています。</p>\n</blockquote>\n\n```shell-session\n# vi /etc/fstab\n```\n\n<br />\n\nswap 部分をコメントアウトします。  \n注意：書かれている内容は、環境によって、異なると思います。\n\n```sh\n#/swapfile     none     swap    sw    0    0\n```\n\n<br />\n\nsystemctl の swap 利用を停止します。  \nまず、swap の UNIT 名を確認します。\n\n```shell-session\n# systemctl --type swap\n  UNIT          LOAD   ACTIVE SUB    DESCRIPTION\n  swapfile.swap loaded active active /swapfile\n```\n\n<br />\n\nswapfile.swap を使わないように（mask）します。\n\n```shell-session\n# systemctl mask \"swapfile.swap\"\nCreated symlink /etc/systemd/system/swapfile.swap → /dev/null.\n```\n\n<br />\n\nmask されたことを確認します。\n\n```shell-session\n# systemctl --type swap\n  UNIT          LOAD   ACTIVE SUB    DESCRIPTION\n● swapfile.swap masked active active /swapfile\n```\n\n<br />\n\n\"masked\"になっていることを確認出来たら、reboot します。\n\n```shell-session\n# reboot\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# マスターノード構築\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes6.png\" alt=\"Ubuntu Kubernetes kubeadm init 図\" width=\"599\" height=\"283\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n`kubeadm init`で master というノード名でマスターノードを構築します。\n\n```shell-session\n# kubeadm init --node-name master --pod-network-cidr=10.244.0.0/16\n```\n\n<blockquote class=\"info\">\n<p><code>--v=5</code>オプションを付けると、詳細が表示されます。</p>\n<p>処理中以下のようにコンテナをpullするところがあります。標準では出力されず、止まったように見えるので、付けて実行した方が良いかもしれません。\n<p><code>pulling: k8s.gcr.io/kube-apiserver:v1.23.3</code></p>\n<p><code>pulling: k8s.gcr.io/kube-controller-manager:v1.23.3</code></p>\n<p><code>pulling: k8s.gcr.io/kube-scheduler:v1.23.3</code></p>\n<p><code>pulling: k8s.gcr.io/kube-proxy:v1.23.3</code></p>\n<p><code>pulling: k8s.gcr.io/pause:3.6</code></p>\n<p><code>pulling: k8s.gcr.io/etcd:3.5.1-0</code></p>\n<p><code>pulling: k8s.gcr.io/coredns/coredns:v1.8.6</code></p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>後で出てきますが、コンテナネットワークのAPIインターフェース<code>Flannel</code>を使用する場合、<code>--pod-network-cidr=10.244.0.0/16</code>が必要です。</p>\n</blockquote>\n\n<br />\n\n構築に成功したら以下のように表示されます。\n<span style=\"color: red;\"><strong>この時表示される token、sha256:ハッシュ値は重要ですので、メモが必要です。</strong></span>  \ntoken、sha256:ハッシュ値は後で作成もできます。詳細：[トラブルシュートまとめ - Token 失効](#tokenexpire)\n\n```shell-session\nYour Kubernetes control-plane has initialized successfully!\n\nTo start using your cluster, you need to run the following as a regular user:\n\n  mkdir -p $HOME/.kube\n  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n  sudo chown $(id -u):$(id -g) $HOME/.kube/config\n\nAlternatively, if you are the root user, you can run:\n\n  export KUBECONFIG=/etc/kubernetes/admin.conf\n\nYou should now deploy a pod network to the cluster.\nRun \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:\n  https://kubernetes.io/docs/concepts/cluster-administration/addons/\n\nThen you can join any number of worker nodes by running the following on each as root:\n\nkubeadm join 192.168.12.200:6443 --token 95w8vq.89t27bukxp8dn6l7 \\\n        --discovery-token-ca-cert-hash sha256:e86f2ced6b3bd7a75e3b22cc54c4a6b47e2279b17f36192ecdc01904883ecffd\n```\n\n<br />\n\n`kubectl`が設定を読み込めるように環境変数をセットします。\n\n```shell-session\n# export KUBECONFIG=/etc/kubernetes/admin.conf\n```\n\n<blockquote class=\"info\">\n<p>今回rootで操作していますが、ユーザー権限の場合、以下の操作になります。</p>\n<p><code>$ mkdir -p $HOME/.kube</code></p>\n<p><code>$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config</code></p>\n<p><code>$ sudo chown $(id -u):$(id -g) $HOME/.kube/config</code></p>\n</blockquote>\n\n<br />\n\nマスターノードが追加されているか確認します。\n\n```shell-session\n# kubectl get nodes\nNAME     STATUS     ROLES                  AGE    VERSION\nmaster   NotReady   control-plane,master   6m9s   v1.23.3\n```\n\nヨシ！\n\n<blockquote class=\"info\">\n<p>マスターノードは、コントロールプレーン（control plane）ノードとも呼びます。今後は、マスター（master）という言葉は無くなっていくようです。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor5\"></a>\n\n# ワーカーノード追加\n\nワーカーノードを追加します。マスターノードとは別のサーバーで、「Docker インストール」「Kubernetes インストール」「マスターノード構築準備」まで行って、reboot 直後とします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes7.png\" alt=\"Ubuntu Kubernetes reboot 直後 図\" width=\"752\" height=\"238\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n`kubeadm join`でワーカーノードとして、登録します。`192.168.12.200`は、マスターノードの IP アドレスです。<span style=\"color: red;\"><strong>token、sha256:ハッシュ値は、`kubeadm init`成功時に表示された値</strong></span>です。  \nデフォルトではホスト名がノード名として登録されるため、`--node-name node1`でノード名を指定しています。\n\n```shell-session\n# kubeadm join 192.168.12.200:6443 --node-name node1 --token 95w8vq.89t27bukxp8dn6l7 \\\n        --discovery-token-ca-cert-hash sha256:e86f2ced6b3bd7a75e3b22cc54c4a6b47e2279b17f36192ecdc01904883ecffd\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes8.png\" alt=\"Ubuntu Kubernetes kubeadm join 図\" width=\"732\" height=\"283\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><code>kubeadm init</code>と同じく、<code>--v=5</code>オプションを付けると、詳細が表示されます。</p>\n</blockquote>\n\n<br />\n\n```shell-session\n# kubectl get nodes\nNAME     STATUS     ROLES                  AGE   VERSION\nmaster   NotReady   control-plane,master   46m   v1.23.3\nnode1    NotReady   <none>                 32s   v1.23.3\n```\n\nノードの追加に成功しました！\n\n<br />\n\nただ、この時点では、ノード間通信の専用ネットワークが構築されていないため、STATUS が\"NotReady\"になっています。\"Ready\"にしていきます。\n\n<br />\n\n<a class=\"anchor\" id=\"anchor6\"></a>\n\n# Flannel インストール\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes9.png\" alt=\"Ubuntu Kubernetes Flannel インストール 図\" width=\"599\" height=\"327\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nノード間通信を可能にし、STATUS を\"Ready\"にするため、Flannel をインストールします。  \n**マスターノードでのみの作業**です。\n\n<blockquote class=\"info\">\n<p>【 Flannel 】</p>\n<p>CNI（Container Network Interface）、つまりコンテナネットワークのAPIインターフェースです。</p>\n<p>LinuxのVXLAN機能を用いてL3ネットワーク上に論理的なL2ネットワークを構築するツールです。</p>\n<p>スライドですが、こちらの説明がかなり分かりやすいと思いました。</p>\n<p>↓</p>\n<p>参考：<a href=\"https://speakerdeck.com/hhiroshell/kubernetes-network-fundamentals-69d5c596-4b7d-43c0-aac8-8b0e5a633fc2?slide=19\" target=\"_blank\">整理しながら理解するKubernetesネットワークの仕組み</a></p>\n</blockquote>\n\n<br />\n\nKubernetes 公式サイトに「Flannel を使用する場合は、iptables チェーンへのブリッジ IPv4 トラフィックを有効にすることをお勧めします。」とありますので、確認します。\n\n```shell-session\n# sysctl -a | grep net.bridge.bridge-nf-call-iptables\nnet.bridge.bridge-nf-call-iptables = 1\n```\n\n最初からなっていました。なっていない場合は、有効にします。\n\n```shell-session\n# sysctl net.bridge.bridge-nf-call-iptables=1\n```\n\n<br />\n\n`kubectl apply`でインストールします。\nなお、`https_proxy=\"http://192.168.0.158:3128\"`は、プロキシサーバーで、`no_proxy=\"192.168.12.200\"`部分は、自分自身の IP アドレス（192.168.12.200）です。プロキシサーバーを使わない場合は、必要有りません。  \n<span style=\"color: red;\"><strong>自分自身へのアクセスにプロキシを使い、タイムアウトするため、`no_proxy=\"192.168.12.200\"`は必要でした。</strong></span>\n\n```shell-session\n# https_proxy=\"http://192.168.0.158:3128\" no_proxy=\"192.168.12.200\" kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml\n```\n\n```shell-session\nWarning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+\npodsecuritypolicy.policy/psp.flannel.unprivileged created\nclusterrole.rbac.authorization.k8s.io/flannel created\nclusterrolebinding.rbac.authorization.k8s.io/flannel created\nserviceaccount/flannel created\nconfigmap/kube-flannel-cfg created\ndaemonset.apps/kube-flannel-ds created\n```\n\n成功です。\n\n<br />\n\n```shell-session\n# kubectl get nodes\nNAME     STATUS   ROLES                  AGE     VERSION\nmaster   Ready    control-plane,master   6h33m   v1.23.3\nnode1    Ready    <none>                 5h29m   v1.23.3\n```\n\n\"Ready\"になっています！ヨシ！\n\n<br />\n\n<a class=\"anchor\" id=\"anchor7\"></a>\n\n# Pod 追加\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes10.png\" alt=\"Ubuntu Kubernetes Pod追加 図\" width=\"715\" height=\"703\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nここまでで、Pod は一つもありません。Deployment で nginx の Pod を追加します。\n\n```shell-session\n# kubectl get pods\nNo resources found in default namespace.\n```\n\n<br />\n\nDeployment の yaml を作成します。\n\n```shell-session\n# vi nginx-deployment.yaml\n```\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 3\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:1.21.6-alpine\n          ports:\n            - containerPort: 80\n```\n\n<br />\n\nDocker Hub にある、Nginx 公式のイメージ `nginx:1.21.6-alpine` をインストールします。  \nレプリカ数（Pod 数）を 3 にします。  \n（今回、ブラウザからのアクセスが確認できるようなアプリをお試しで入れるだけなので、このイメージにした理由は特に有りません。）\n\n```shell-session\n# kubectl apply -f nginx-deployment.yaml\ndeployment.apps/nginx-deployment created\n```\n\n<br />\n\nこれにより、`nginx:1.21.6-alpine`が`docker pull`されて、デプロイされます。（プロキシ利用の場合、Docker のプロキシ設定は必要です。）\n\n```shell-session\n# kubectl get pods\nNAME                                READY   STATUS              RESTARTS   AGE\nnginx-deployment-5b7d486f5c-c8hzk   0/1     ContainerCreating   0          14s\nnginx-deployment-5b7d486f5c-nms6k   0/1     ContainerCreating   0          14s\nnginx-deployment-5b7d486f5c-q9mw8   0/1     ContainerCreating   0          14s\n```\n\n一瞬で、「created」になりますが、しばらくは、構築中ステータスになります。\n\n<br />\n\n```shell-session\n# kubectl get pods -o wide\nNAME                                READY   STATUS    RESTARTS   AGE   IP           NODE    NOMINATED NODE   READINESS GATES\nnginx-deployment-5b7d486f5c-c8hzk   1/1     Running   0          92s   10.244.1.4   node1   <none>           <none>\nnginx-deployment-5b7d486f5c-nms6k   1/1     Running   0          92s   10.244.1.3   node1   <none>           <none>\nnginx-deployment-5b7d486f5c-q9mw8   1/1     Running   0          92s   10.244.1.5   node1   <none>           <none>\n```\n\n構築成功です。  \n`-o wide`でどこのノードにデプロイされたか確認できますが、ワーカーノードの`node1`にデプロイされています。\n\n<blockquote class=\"info\">\n<p>デフォルトでは、セキュリティ上の理由により、クラスターはコントロールプレーンノード（マスターノード）にPodをスケジューリング（割り当て）しません。Taintを変更する必要がありますが、今回は、それで良いものとします。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor8\"></a>\n\n# Service 追加\n\nここまでで、nginx は Kubernetes の世界に閉じ込められた状態で、外からアクセスできません。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes11.png\" alt=\"Ubuntu Kubernetes 外からアクセス不可 図\" width=\"644\" height=\"948\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nService の NodePort を追加し、外からアクセスできるようにします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/kubernetes12.png\" alt=\"Ubuntu Kubernetes Service 追加 外からアクセス可能 図\" width=\"644\" height=\"1026\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nService の yaml を作成します。\n\n```shell-session\n# vi nginx-service.yaml\n```\n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx\nspec:\n  type: NodePort\n  ports:\n    - protocol: 'TCP'\n      port: 8080\n      targetPort: 80\n      nodePort: 30080\n  selector:\n    app: nginx\n```\n\n<br />\n\nService の yaml を apply します。\n\n```shell-session\n# kubectl apply -f nginx-service.yaml\n```\n\n```shell-session\n# kubectl get services -o wide\nNAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE   SELECTOR\nkubernetes   ClusterIP   10.96.0.1        <none>        443/TCP          19h   <none>\nnginx        NodePort    10.103.127.182   <none>        8080:30080/TCP   7s    app=nginx\n```\n\nnginx という名の NodePort タイプの Service が追加されました。\n\n<br />\n\nマスターノードの IP アドレス：`192.168.12.200`  \nワーカーノードの IP アドレス：`192.168.12.201`  \nですので、  \nブラウザで、 `http://192.168.12.200:30080/` にアクセスしてみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/image1.png\" alt=\"Ubuntu Kubernetes ブラウザでアクセス\" width=\"775\" height=\"502\" loading=\"lazy\"></a>\n\nヨシ！\n\n<blockquote class=\"info\">\n<p>ワーカーノードのIPアドレス（http://192.168.12.201:30080/）でも行けます。</p>\n</blockquote>\n\n<br />\n\nこの後、Ingress を使って、ホスト名でアクセスできるようになりますが、ここまでとします。\n\n<blockquote class=\"warn\">\n<p>LoadBalancerタイプのServiceでもhttp://[ホスト名]/でアクセスできますが、今回の手順では、LoadBalancerが実装されていない構成のため、NodePortタイプとしました。（AWS、Google Cloudではできます。）</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor9\"></a>\n\n# トラブルシュートまとめ\n\n## kubeadm init\n\n**----- 現象 -----**\n\n```shell-session\n# kubeadm init --node-name master --pod-network-cidr=10.244.0.0/16\nW0125 23:40:54.844826    1779 version.go:103] could not fetch a Kubernetes version from the internet: unable to get URL \"https://dl.k8s.io/release/stable-1.txt\": Get \"https://dl.k8s.io/release/stable-1.txt\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)\nW0125 23:40:54.844885    1779 version.go:104] falling back to the local client version: v1.23.3\n[init] Using Kubernetes version: v1.23.3\n[preflight] Running pre-flight checks\n        [WARNING Swap]: swap is enabled; production deployments should disable swap unless testing the NodeSwap feature gate of the kubelet\n        [WARNING Hostname]: hostname \"master\" could not be reached\n        [WARNING Hostname]: hostname \"master\": lookup master on 127.0.0.53:53: server misbehaving\n[preflight] Pulling images required for setting up a Kubernetes cluster\n[preflight] This might take a minute or two, depending on the speed of your internet connection\n[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'\n```\n\nここで停止。\n\n```shell-session\n# kubeadm config images pull\nW0126 00:35:41.005320    1993 version.go:103] could not fetch a Kubernetes version from the internet: unable to get URL \"https://dl.k8s.io/release/stable-1.txt\": Get \"https://dl.k8s.io/release/stable-1.txt\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)\nW0126 00:35:41.005376    1993 version.go:104] falling back to the local client version: v1.23.3\nfailed to pull image \"k8s.gcr.io/kube-apiserver:v1.23.3\": output: Error response from daemon: Get \"https://k8s.gcr.io/v2/\": dial tcp: lookup k8s.gcr.io: Temporary failure in name resolution\n, error: exit status 1\n```\n\n<br />\n\n**----- 原因 -----**  \ndocker pull ができていない。（インターネットに繋がっていない。）\n\n<br />\n\n**----- 対処 -----**  \nProxy 環境下で docker pull できるようにする。\n\n```shell-session\n# mkdir -p /etc/systemd/system/docker.service.d\n# echo -e \"[Service]\\nEnvironment=HTTP_PROXY=http://192.168.0.158:3128/ HTTPS_PROXY=http://192.168.0.158:3128/\" | sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf\n[Service]\nEnvironment=HTTP_PROXY=http://192.168.0.158:3128/ HTTPS_PROXY=http://192.168.0.158:3128/\n# systemctl daemon-reload\n# systemctl restart docker\n```\n\n<br />\n\n## cgroup を切り替えていない\n\n**----- 現象 -----**\n\nkubelet 起動エラー。\n\n```shell-session\n# journalctl -xeu kubelet\nJan 27 17:20:49 ubuntu kubelet[342857]: E0127 17:20:49.359548  342857 server.go:302] \"Failed to run kubelet\" err=\"failed to run Kubelet: misconfiguration: kubelet cgroup driver: \\\"systemd\\\" is>\nJan 27 17:20:49 ubuntu systemd[1]: kubelet.service: Main process exited, code=exited, status=1/FAILURE\n```\n\n<br />\n\n**----- 原因 -----**  \ndocker の cgroup を切り替えていない。（kubelet の systemd なのに対し、docker の cgroup driver は、cgroupfs）\n\n<br />\n\n**----- 対処 -----**\n\n```shell-session\n# vi /etc/docker/daemon.json\n```\n\n```json\n{\n  \"exec-opts\": [\"native.cgroupdriver=systemd\"]\n}\n```\n\n```shell-session\n# systemctl daemon-reload\n# systemctl restart docker\n```\n\n<br />\n\n## Port 6443 is in use\n\n**----- 現象 -----**\n\n```shell-session\n# kubeadm init --node-name master --pod-network-cidr=10.244.0.0/16\nW0126 01:29:08.438000    6843 version.go:103] could not fetch a Kubernetes version from the internet: unable to get URL \"https://dl.k8s.io/release/stable-1.txt\": Get \"https://dl.k8s.io/release/stable-1.txt\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)\nW0126 01:29:08.438145    6843 version.go:104] falling back to the local client version: v1.23.3\n[init] Using Kubernetes version: v1.23.3\n[preflight] Running pre-flight checks\n        [WARNING Hostname]: hostname \"master\" could not be reached\n        [WARNING Hostname]: hostname \"master\": lookup master on 127.0.0.53:53: server misbehaving\nerror execution phase preflight: [preflight] Some fatal errors occurred:\n        [ERROR Port-6443]: Port 6443 is in use\n        [ERROR Port-10259]: Port 10259 is in use\n```\n\n<br />\n\n**----- 原因 -----**  \n既に`kubeadm init`が実行されている。\n\n<br />\n\n**----- 対処 -----**\n\n```shell-session\n# kubeadm reset\n# kubeadm init --node-name master --pod-network-cidr=10.244.0.0/16\n```\n\n<br />\n\n## kubectl\n\n**----- 現象 -----**\n\n```shell-session\n# kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml\nThe connection to the server localhost:8080 was refused - did you specify the right host or port?\n```\n\n<br />\n\n**----- 原因 -----**  \n設定が読み込まれていない。\n\n<br />\n\n**----- 対処 -----**  \nroot の場合：\n\n```shell-session\n# export KUBECONFIG=/etc/kubernetes/admin.conf\n```\n\nユーザー権限の場合：\n\n```shell-session\n$ mkdir -p $HOME/.kube\n$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n$ sudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n<br />\n\n## flannel\n\n**----- 現象 -----**  \nProxy 環境でエラー。\n\n```shell-session\n# https_proxy=\"http://192.168.0.158:3128\" kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml\nUnable to connect to the server: Forbidden\n```\n\n<br />\n\n**----- 原因 -----**\n\nno_proxy に自 IP アドレスが必要。\n\n<br />\n\n**----- 対処 -----**\n\n```shell-session\n# https_proxy=\"http://192.168.0.158:3128\" no_proxy=\"192.168.12.200\" kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml\nWarning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+\npodsecuritypolicy.policy/psp.flannel.unprivileged created\nclusterrole.rbac.authorization.k8s.io/flannel created\nclusterrolebinding.rbac.authorization.k8s.io/flannel created\nserviceaccount/flannel created\nconfigmap/kube-flannel-cfg created\ndaemonset.apps/kube-flannel-ds created\n```\n\n<br />\n\n<a class=\"anchor\" id=\"tokenexpire\"></a>\n\n## Token 失効\n\n**----- 現象 -----**\n\n```shell-session\n# kubeadm join 192.168.12.200:6443 --token 7bquah.q7ofwnr0obpeg44p \\\n>         --discovery-token-ca-cert-hash sha256:1c6e75a5f9743a6f766efb5d5dba1b82541c7a1614f3d93214f3b4b777756ba7\n[preflight] Running pre-flight checks\nerror execution phase preflight: couldn't validate the identity of the API Server: could not find a JWS signature in the cluster-info ConfigMap for token ID \"7bquah\"\nTo see the stack trace of this error execute with --v=5 or higher\n```\n\n<br />\n\n**----- 原因 -----**\n\nトークンが失効している。（`kubeadm init`後 24 時間で失効する。）\n\n（マスターノード側作業）\n\n```shell-session\n# kubeadm token list\n```\n\n→ 何も表示されない。\n\n<br />\n\n**----- 対処 -----**\n\n（マスターノード側作業）\n\n```shell-session\n# kubeadm token create --print-join-command\nkubeadm join 192.168.12.200:6443 --token 558az8.uh111y2v4eme4dcj --discovery-token-ca-cert-hash sha256:1c6e75a5f9743a6f766efb5d5dba1b82541c7a1614f3d93214f3b4b777756ba7\n```\n\n→ ハッシュは変らない。\n\n<br />\n\n（ワーカーノード側作業）\n\n```shell-session\n# kubeadm join 192.168.12.200:6443 --token 558az8.uh111y2v4eme4dcj --discovery-token-ca-cert-hash sha256:1c6e75a5f9743a6f766efb5d5dba1b82541c7a1614f3d93214f3b4b777756ba7\n```\n\n<br />\n\n## InvalidImageName\n\n**----- 現象 -----**\n\n```shell-session\n# kubectl get pods -o wide\nNAME                                READY   STATUS             RESTARTS   AGE     IP           NODE     NOMINATED NODE   READINESS GATES\nnginx-deployment-84d98d877f-cs6dr   0/1     InvalidImageName   0          2m45s   10.244.1.3   node1    <none>           <none>\nnginx-deployment-84d98d877f-m2pgj   0/1     InvalidImageName   0          2m45s   10.244.1.4   node1    <none>           <none>\nnginx-deployment-84d98d877f-qxpvc   0/1     InvalidImageName   0          2m45s   10.244.1.2   node1    <none>           <none>\n```\n\n<br />\n\n**----- 原因 -----**  \nnginx-deployment.yaml の image が間違っている。\n\n例：`image: nginx:1.21.6:alpine`（正しいのは、`image: nginx:1.21.6-alpine`）\n\n<br />\n\n**----- 対処 -----**\n\n```shell-session\n# kubectl delete deployment nginx-deployment\n```\n\n↓\n\nnginx-deployment.yaml 修正\n\n↓\n\n```shell-session\n# kubectl apply -f nginx-deployment.yaml\n```\n\n<br />\n\n## EXTERNAL-IP が pending\n\n**----- 現象 -----**\n\nEXTERNAL-IP が`<pending>`のままになる。\n\n```shell-session\n# kubectl get services\nNAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE\nkubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          6h57m\nnginx        LoadBalancer   10.107.14.190   <pending>     8080:32446/TCP   18s\n```\n\n<br />\n\n**----- 原因 -----**\n\nService タイプ LoadBalancer に対応していない環境。\n\n<br />\n\n**----- 対処 -----**\n\n```shell-session\n# kubectl delete services nginx\n```\n\n↓\n\nIngress 導入など。\n","description":"Ubuntu 20.04 LTS のオンプレミス環境に Kubernetes 環境を構築しました。マスターノード、ワーカーノードを作成し、ワーカーノードにNginx Podを追加しました。そこまでの全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"04zs6ajwbtj","createdAt":"2022-01-31T12:50:58.842Z","updatedAt":"2022-02-02T02:04:20.990Z","publishedAt":"2022-01-31T12:50:58.842Z","revisedAt":"2022-02-02T02:04:20.990Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-kubernetes/ITC_Engineering_Blog.png","alt":"Ubuntu 20 04 LTSのオンプレにKubernetes環境構築からnginx Pod稼働まで","width":1200,"height":630}],"seo_authors":[]},{"id":"keycloak-saml-azuread","createdAt":"2023-11-28T10:35:43.184Z","updatedAt":"2024-03-02T10:10:41.932Z","publishedAt":"2023-11-28T10:35:43.184Z","revisedAt":"2024-03-02T10:10:41.932Z","title":"Ubuntu 22にKeycloak 22をインストールして、Identity providers=Azure ADでSAML","category":{"id":"r6qzmkowt0b","createdAt":"2021-11-30T12:16:44.824Z","updatedAt":"2021-11-30T12:16:44.824Z","publishedAt":"2021-11-30T12:16:44.824Z","revisedAt":"2021-11-30T12:16:44.824Z","topics":"Keycloak","logo":"/logos/Keycloak.png","needs_title":false},"topics":[{"id":"r6qzmkowt0b","createdAt":"2021-11-30T12:16:44.824Z","updatedAt":"2021-11-30T12:16:44.824Z","publishedAt":"2021-11-30T12:16:44.824Z","revisedAt":"2021-11-30T12:16:44.824Z","topics":"Keycloak","logo":"/logos/Keycloak.png","needs_title":false},{"id":"ik0y39076","createdAt":"2024-02-04T12:20:33.135Z","updatedAt":"2024-02-04T12:20:33.135Z","publishedAt":"2024-02-04T12:20:33.135Z","revisedAt":"2024-02-04T12:20:33.135Z","topics":"Java","logo":"/logos/Java.png","needs_title":false},{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"k7x51z-0y5","createdAt":"2021-05-05T06:30:34.213Z","updatedAt":"2021-08-31T12:05:59.237Z","publishedAt":"2021-05-05T06:30:34.213Z","revisedAt":"2021-08-31T12:05:59.237Z","topics":"Apache","logo":"/logos/Apache.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false},{"id":"umqsrvfrv7","createdAt":"2021-08-29T10:56:17.442Z","updatedAt":"2021-08-31T12:02:21.915Z","publishedAt":"2021-08-29T10:56:17.442Z","revisedAt":"2021-08-31T12:02:21.915Z","topics":"Unix/Linux","logo":"/logos/Linux.png","needs_title":true}],"content":"# はじめに\n\n<blockquote class=\"alert\">\n<p>この記事に「SSO とは」「SAML とは」「Keycloak とは」等々、用語の説明はありません。</p>\n</blockquote>\n\nUbuntu 22.04.3 LTS に Keycloak 22.0.5(Quarkus 版) をインストールして、SAML による SSO（シングルサインオン／Single Sign On）環境を作成しました。  \nさらに作業を進めて、Identity providers に Azure AD（Azure Active Directory／Microsoft Entra ID）を追加して、Azure AD のアカウントで認証することに成功しました。  \nアプリ - Keycloak - Azure AD の場合、Keycloak は、仲介サービス（Identity Broker）として機能します。\n\n<blockquote class=\"warn\">\n<p>Azure AD（Azure Active Directory）は、Microsoft Entra ID に名称が変わりましたが、この記事では、Azure AD 表記のままでいきます。</p>\n</blockquote>\n\n<br />\n\n今回作成したのは、以下の２パターンです。\n\n**`●基本パターン`**  \n・**SP（Service Provider）：** Apache ＆ mod_auth_mellon ＆ php  \n・**IdP（Identity Provider）：** Keycloak  \n・**User federation：** Keycloak の DB(PostgreSQL14)\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu1.png\" alt=\"基本パターン 図\" width=\"802\" height=\"382\" loading=\"lazy\"></a>\n\n<br />\n\n**`●Azure AD利用パターン`**  \n・**SP（Service Provider）：** Apache ＆ mod_auth_mellon ＆ php  \n・**Identity Broker：** Keycloak  \n・**IdP（Identity Provider）：** Azure AD  \n・**User federation：** Azure AD\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu2.png\" alt=\"Azure AD利用パターン 図\" width=\"1031\" height=\"461\" loading=\"lazy\"></a>\n\n<br />\n\nIdentity Broker（Keycloak）は、mod_auth_mellon から見ると、IdP で、Azure AD から見ると、SP でもあります。  \n\n<br />\n\nこの２パターンの環境構築について、Keycloak インストールから全手順を紹介していきます。\n\n<br />\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;\">Keycloak に関しては、Docker, Podman, Kubernetes 等コンテナでデプロイではなく、ソースコードを Ubuntu の /opt/keycloak にビルドしてデプロイします。</span></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>Keycloak は、v17 までアプリケーションサーバーとして、Wildfly を使っていたのですが、v17 から Quarkus を利用するようになったようです。今回、Wildfly ではなく、Quarkus 前提の手順です。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>Quarkus（カーカス）は、Red Hat 社が開発しているオープンソースの Java フレームワークです。</p>\n<p>コンテナ化・サーバレス化した開発環境に最適化されており、アプリケーションの起動時間や応答時間を速くし、省メモリで実行することができます。﻿</p>\n<p>Quarkus の主な機能は次のとおりです。﻿</p>\n<p>・柔軟な開発モデル</p>\n<p>・省メモリ</p>\n<p>・高速起動</p>\n<p>・繰り返しタスクの自動化</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p>●Keycloak</p>\n<p>　・Ubuntu 22.04.3 LTS</p>\n<p>　　・Keycloak 22.0.5</p>\n<p>　　・PostgreSQL 14.9</p>\n<p>　</p>\n<p>●Apache ＆ mod_auth_mellon ＆ php</p>\n<p>　・Debian Bullseye with Raspberry Pi Desktop 2022-07-01(Debian version: 11)</p>\n<p>　　・Apache 2.4.56</p>\n<p>　　・libapache2-mod-auth-mellon 0.17.0-1+deb11u1</p>\n<p>　　・PHP 7.4.33</p>\n<p>　</p>\n<p>● クラウド：Azure AD（Azure Active Directory／Microsoft Entra ID）</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>この記事では、とにかく動作するまでの最低限の設定しか行いません。ログアウトのことは考えません。</strong></span></p>\n<p><span style=\"color: red;\"><strong>本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。</strong></span></p>\n</blockquote>\n\n<br />\n\n# IdP：Keycloak インストール\n\n<blockquote class=\"warn\">\n<p>OS（Ubuntu 22.04.3 LTS）を標準のインストール方法でインストールした直後とします。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>全て root 権限で実行しています。そのため、sudo は省略しています。</p>\n<p>hosts 登録しないといけないとか当たり前のことは省略しています。</p>\n</blockquote>\n\nKeycloak 22.0.5 をインストールします。\n\n<br />\n\napt を最新化します。\n\n```shellsession\n# apt update -y && apt upgrade -y\n```\n\n<br />\n\n必要パッケージと時刻合わせに使う chrony をインストールします。\n\n<blockquote class=\"info\">\n<p>SAML 認証では、IdP（Identity Provider）と SP（Service Provider）の間で時刻が大きくずれていると問題が生じることがあります。</p>\n<p>これは、SAML アサーション（認証情報）には有効期限が含まれており、その有効期限が IdP と SP で異なる解釈をされる可能性があるからです。</p>\n</blockquote>\n\n```shellsession\n# apt install software-properties-common ca-certificates chrony -y\n# vi /etc/chrony/chrony.conf\n# systemctl restart chrony.service\n```\n\n<br />\n\nここで、chrony.conf は、以下のように NTP サーバーを設定して、時刻がずれないようにします。\n\n```systemd:/etc/chrony/chrony.conf\n#pool ntp.ubuntu.com        iburst maxsources 4\n#pool 0.ubuntu.pool.ntp.org iburst maxsources 1\n#pool 1.ubuntu.pool.ntp.org iburst maxsources 1\n#pool 2.ubuntu.pool.ntp.org iburst maxsources 2\npool ntp.nict.jp iburst\n```\n\n<br />\n\nJava ランタイム環境と開発環境をインストールします。\n\n```shellsession\n# apt install openjdk-17-jre-headless openjdk-17-jdk-headless -y\n```\n\n<br />\n\nkeycloak-22.0.5.tar.gz をダウンロードして、展開し、/opt/keycloak に配置します。\n\n```shellsession\n# wget https://github.com/keycloak/keycloak/releases/download/22.0.5/keycloak-22.0.5.tar.gz\n# tar zxvf keycloak-22.0.5.tar.gz\n# mv keycloak-22.0.5 keycloak\n# mv keycloak /opt/\n```\n\n<br />\n\nPostgreSQL 14 をインストールして、DB=`keycloak`、 User=`keycloak`, Password=`PASSWORD` を作成します。\n\n```shellsession\n# apt install postgresql -y\n# su - postgres\n$ psql -V\npsql (PostgreSQL) 14.9 (Ubuntu 14.9-0ubuntu0.22.04.1)\n$ psql -d postgres\nCREATE USER keycloak WITH PASSWORD 'PASSWORD' CREATEDB;\nCREATE DATABASE keycloak OWNER keycloak;\n\\q\n$ exit\n```\n\n<br />\n\nkeycloak.conf にデータベースの種類、データベース名、データベースユーザー名等設定します。\n\n```shellsession\n# vi /opt/keycloak/conf/keycloak.conf\n```\n\n```systemd:/opt/keycloak/conf/keycloak.conf\n# データベースの種類=PostgreSQL\ndb=postgres\n# データベースへの接続に使用するユーザー名=keycloak\ndb-username=keycloak\n# データベースへの接続に使用するパスワード=PASSWORD\ndb-password=PASSWORD\n# データベースへの接続URL\ndb-url=jdbc:postgresql://localhost/keycloak\n# ヘルスチェック（サービスの健康状態を確認する機能）有効\nhealth-enabled=true\n# メトリクス（サービスのパフォーマンスを測定するデータ）有効\nmetrics-enabled=true\n```\n\n<br />\n\nkeycloak ユーザーと keycloak グループを作成します。（最後に /opt/keycloak の Owner:Group のパーミッションを `keycloak:keycloak` とします。）\n\n```shellsession\n# groupadd -r keycloak\n# useradd -m -d /var/lib/keycloak -s /sbin/nologin -r -g keycloak keycloak\n```\n\n<br />\n\n`https://` で Keycloak の管理コンソールにアクセスするため、自己署名証明書を作成します。\n今回、<span style=\"color: red;\"><strong>Keycloak の URL は、`https://kctest.contoso.com` とします。</strong></span>\n\n<blockquote class=\"info\">\n<p>Keycloak の管理コンソール とは、ＧＵＩの Keycloak 設定画面のことです。</p>\n</blockquote>\n\n```shellsession\n# openssl req -newkey rsa:2048 -nodes -keyout server.key.pem -x509 -days 3650 -out server.crt.pem\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:ITC\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:kctest.contoso.com\nEmail Address []:\n# mv server.crt.pem /opt/keycloak/conf/\n# mv server.key.pem /opt/keycloak/conf/\n```\n\n<br />\n\nkeycloak.conf に先ほどのホスト名、証明書、秘密鍵を設定します。\n\n```shellsession\n# vi /opt/keycloak/conf/keycloak.conf\n```\n\n```systemd:/opt/keycloak/conf/keycloak.conf\n# 注意：confの前に / の追加が必要。（コメントを外すだけでは、NG）\nhttps-certificate-file=${kc.home.dir}/conf/server.crt.pem\n# 注意：confの前に / の追加が必要。（コメントを外すだけでは、NG）\nhttps-certificate-key-file=${kc.home.dir}/conf/server.key.pem\nhostname=kctest.contoso.com\n# 443ポートを使用する設定を追加します。※デフォルトは、8443です。\nhttps-port=443\n```\n\n<blockquote class=\"warn\">\n<p>keycloak.conf の <code>https-certificate-file</code> と <code>https-certificate-key-file</code> について、上記コメントにもありますが、初期状態でコメントアウトされている</p>\n<p><code>${kc.home.dir}conf</code></p>\n<p>のままの場合、<code>/opt/keycloakconf</code> を参照しようとしてエラーになりました。</p>\n</blockquote>\n\n<br />\n\nビルドして、手動起動します。  \nビルドする際に、<span style=\"color: red;\"><strong>Keycloak 管理コンソールの管理者ユーザー名とパスワードを設定</strong></span>します。  \n<span style=\"color: red;\"><strong>管理者ユーザー名： `admin`</strong></span>  \n<span style=\"color: red;\"><strong>管理者パスワード： `KYC_PASS`</strong></span>\n\n```shellsession\n# cd /opt/keycloak/bin/\n# export KEYCLOAK_ADMIN=admin\n# export KEYCLOAK_ADMIN_PASSWORD=KYC_PASS\n# ./kc.sh --verbose build\n# ./kc.sh --verbose start\n```\n\n<br />\n\n`https://kctest.contoso.com/` にアクセスして、  \n<span style=\"color: red;\"><strong>管理者ユーザー名： `admin`</strong></span>  \n<span style=\"color: red;\"><strong>管理者パスワード： `KYC_PASS`</strong></span>  \nでログインできたら、ひとまずは、成功です。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image1.png\" alt=\"Administration Console\" width=\"922\" height=\"726\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image2.png\" alt=\"Sign in to your account\" width=\"913\" height=\"682\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image3.png\" alt=\"master realm\" width=\"1010\" height=\"727\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！\n\n<br />\n\n# IdP：Keycloak サービス追加\n\n`./kc.sh --verbose start` で問題無いことが確認できたため、`CTRL + C` で止めます。\n\n<br />\n\n`systemctl` で keycloak を起動する設定を追加します。\n\n```shellsession\n# vi /etc/systemd/system/keycloak.service\n```\n\n```systemd:/etc/systemd/system/keycloak.service\n# 汎用オプション\n[Unit]\n# サービスの説明書き\nDescription=Keycloak Application Server\n# ネットワークとsyslogサービスが起動して実行された後にこのサービスを開始\nAfter=syslog.target network.target\n# 固有のオプション\n[Service]\n# 実行したタイミングで起動完了と判断\nType=simple\n# 停止完了までに待機する時間\nTimeoutStopSec=0\n# systemctl stopコマンドでSIGTERMシグナルを送る\nKillSignal=SIGTERM\n# サービスが停止されたときにメインプロセスのみが強制終了\nKillMode=process\n# 成功とみなされる終了ステータス=143\nSuccessExitStatus=143\n# 最大ロックメモリアドレス空間\nLimitMEMLOCK=infinity\n# systemd がサービスを停止した後、残りのプロセスにSIGKILLシグナルを送信しない\nSendSIGKILL=no\n# 作業ディレクトリ\nWorkingDirectory=/opt/keycloak/\n# 実行に使用されるユーザーとグループ\nUser=keycloak\nGroup=keycloak\n# オープンファイル記述子の最大数\nLimitNOFILE=102642\n# ログをコンソールとファイルに出力するオプションを指定して起動\n# ログの出力先は、/opt/keycloak/data/log/keycloak.log\nExecStart=/opt/keycloak/bin/kc.sh start --log=console,file\n# インストールに関する情報\n[Install]\n# システムがマルチユーザーモードで起動するときに自動的に起動する\nWantedBy=multi-user.target\n```\n\n```shellsession\n# chown keycloak: -R /opt/keycloak\n# systemctl enable keycloak.service\n# systemctl start keycloak.service\n（しばらくしてから確認）\n# systemctl status keycloak.service\n× keycloak.service - Keycloak Application Server\n     Loaded: loaded (/etc/systemd/system/keycloak.service; enabled; vendor preset: enabled)\n     Active: failed (Result: exit-code) since Sat 2023-11-25 17:18:55 JST; 43s ago\n    Process: 1774 ExecStart=/opt/keycloak/bin/kc.sh start --log=console,file (code=exited, status=1/FAILURE)\n   Main PID: 1774 (code=exited, status=1/FAILURE)\n        CPU: 16.515s\n\n11月 25 17:18:54 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:54,266 INFO  [org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory] (main) Node name: ubuntu-22043-6245, S>\n11月 25 17:18:55 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:55,119 INFO  [org.infinispan.CLUSTER] (main) ISPN000080: Disconnecting JGroups channel `ISPN`\n11月 25 17:18:55 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:55,162 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Failed to start server in (production) mode\n11月 25 17:18:55 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:55,163 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Unable to start HTTP server\n11月 25 17:18:55 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:55,163 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: io.quarkus.runtime.QuarkusBindException: Por>\n11月 25 17:18:55 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:55,163 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Port(s) already bound: 443: 許可がありません\n11月 25 17:18:55 ubuntu-22043 kc.sh[1774]: 2023-11-25 17:18:55,164 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) For more details run the same command passing the '>\n11月 25 17:18:55 ubuntu-22043 systemd[1]: keycloak.service: Main process exited, code=exited, status=1/FAILURE\n11月 25 17:18:55 ubuntu-22043 systemd[1]: keycloak.service: Failed with result 'exit-code'.\n11月 25 17:18:55 ubuntu-22043 systemd[1]: keycloak.service: Consumed 16.515s CPU time.\n```\n\n<br />\n\n`systemctl start keycloak.service` ですんなり起動して、ヨシ！っと思ったら、エラー停止しました。  \nエラー：<span style=\"color: #e70500;background-color: #ffebe7;\">ERROR: Port(s) already bound: 443: 許可がありません.</span>  \n<span style=\"color: red;\"><strong>非特権（非 root）ユーザ（keycloak）で起動していて、ウェルノウンポート（1024 未満のポート）にバインドできないため、エラーです。</strong></span>\n\n<br />\n\n```shellsession\n# sysctl net.ipv4.ip_unprivileged_port_start\nnet.ipv4.ip_unprivileged_port_start = 1024\n```\n\n1024 以上しか非特権ユーザーのアクセスが許されていません。\n\n```shellsession\n# sysctl -w net.ipv4.ip_unprivileged_port_start=443\n```\n\nで 443 未満に変更します。  \nさらに、この設定を恒久化します。\n\n```shellsession\n# vi /etc/sysctl.d/99-sysctl.conf\n```\n\n```systemd:/etc/sysctl.d/99-sysctl.conf\nnet.ipv4.ip_unprivileged_port_start=443\n```\n\n反映して、再スタートします。\n\n```shellsession\n# sysctl --system\n# systemctl start keycloak\n# systemctl status keycloak\n● keycloak.service - Keycloak Application Server\n     Loaded: loaded (/etc/systemd/system/keycloak.service; enabled; vendor preset: enabled)\n     Active: active (running) since Sat 2023-11-25 17:31:25 JST; 10s ago\n   Main PID: 1928 (java)\n      Tasks: 52 (limit: 9387)\n     Memory: 291.1M\n        CPU: 14.350s\n     CGroup: /system.slice/keycloak.service\n             mq1928 java -Dkc.config.built=true -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED -Dkc.home.dir=/opt/keycloak/bin/.. -Djboss.ser>\n\n11月 25 17:31:29 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:29,829 INFO  [org.infinispan.LIFECYCLE] (jgroups-7,ubuntu-22043-12067) [Context=offlineSessions] ISPN100002: Starting rebalance with members [ubuntu-22043-46092, ubuntu-22043-12067], phase READ_OLD_WRITE_ALL, topology id 12\n11月 25 17:31:29 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:29,831 INFO  [org.infinispan.LIFECYCLE] (jgroups-7,ubuntu-22043-12067) [Context=offlineSessions] ISPN100010: Finished rebalance with members [ubuntu-22043-46092, ubuntu-22043-12067], topology id 12\n11月 25 17:31:29 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:29,848 INFO  [org.infinispan.LIFECYCLE] (jgroups-10,ubuntu-22043-12067) [Context=sessions] ISPN100002: Starting rebalance with members [ubuntu-22043-46092, ubuntu-22043-12067], phase READ_OLD_WRITE_ALL, topology id 12\n11月 25 17:31:29 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:29,850 INFO  [org.infinispan.LIFECYCLE] (jgroups-10,ubuntu-22043-12067) [Context=sessions] ISPN100010: Finished rebalance with members [ubuntu-22043-46092, ubuntu-22043-12067], topology id 12\n11月 25 17:31:29 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:29,862 INFO  [org.infinispan.LIFECYCLE] (jgroups-10,ubuntu-22043-12067) [Context=work] ISPN100002: Starting rebalance with members [ubuntu-22043-46092, ubuntu-22043-12067], phase READ_OLD_WRITE_ALL, topology id 12\n11月 25 17:31:29 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:29,864 INFO  [org.infinispan.LIFECYCLE] (non-blocking-thread--p2-t5) [Context=work] ISPN100010: Finished rebalance with members [ubuntu-22043-46092, ubuntu-22043-12067], topology id 12\n11月 25 17:31:30 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:30,035 INFO  [org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory] (main) Node name: ubuntu-22043-12067, Site name: null\n11月 25 17:31:30 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:30,784 INFO  [io.quarkus] (main) Keycloak 22.0.5 on JVM (powered by Quarkus 3.2.7.Final) started in 4.791s. Listening on: https://0.0.0.0:443\n11月 25 17:31:30 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:30,786 INFO  [io.quarkus] (main) Profile prod activated.\n11月 25 17:31:30 ubuntu-22043 kc.sh[1928]: 2023-11-25 17:31:30,786 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-h2, jdbc-mariadb, jdbc-mssql, jdbc-mysql, jdbc-oracle, jdbc-postgresql, keycloak, logging-gelf, micrometer, narayana-jta, reactive-routes, resteasy, resteasy-jackson, smallrye-context-propagation, smallrye-health, vertx]\n```\n\n<br />\n\n`https://kctest.contoso.com/` にアクセスして、起動確認します。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image1.png\" alt=\"アクセスして、起動確認\" width=\"922\" height=\"726\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！\n\n<br />\n\n# IdP：Keycloak レルム作成\n\nまず、テスト用にレルムを作成します。\n\n<blockquote class=\"info\">\n<p>「レルム(Realm)」は、以下のように各種設定をまとめた入れ物のようなイメージで良いと思います。master レルムが最初からあります。</p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image4.png\" alt=\"レルム イメージ 図\" width=\"721\" height=\"551\" loading=\"lazy\"></a></p>\n</blockquote>\n\n<br />\n\n管理コンソールの \"master\" と表示されているところをクリックして、**Create Realm** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image5.png\" alt=\"Create Realm\" width=\"1240\" height=\"281\" loading=\"lazy\"></a>\n\n<br />\n\nRealm name に作成するレルムの名前を入力して、**Create** をクリックします。\nここでは、`TestRealm` とします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image6.png\" alt=\"レルム Create\" width=\"1217\" height=\"658\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image7.png\" alt=\"レルム作成成功\" width=\"1215\" height=\"150\" loading=\"lazy\"></a>\n\n<br />\n\n# IdP：Keycloak 一般ユーザー作成\n\nレルム：TestRealm を選択して、Keycloak の DB(PostgreSQL)にユーザーを作成します。  \nここで作成するユーザーは、レルム：TestRealm 内に作成された Keycloak 独自管理下のユーザーです。\n\n<br />\n\n**Users** をクリックして、**Add user** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image8.png\" alt=\"Add user\" width=\"1216\" height=\"485\" loading=\"lazy\"></a>\n\n<br />\n\nUsername に作成するユーザー名を入力します。  \nここでは、`testuser` とします。  \n確認メール不要としたいため、Email verified は、`Yes` とします。  \n**Create** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image9.png\" alt=\"ユーザー Create\" width=\"1207\" height=\"745\" loading=\"lazy\"></a>\n\n<br />\n\n**Credentials** タブをクリックして、パスワードを設定します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image10.png\" alt=\"Credentials\" width=\"1315\" height=\"519\" loading=\"lazy\"></a>\n\n<br />\n\n**Set password** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image11.png\" alt=\"Set password\" width=\"1324\" height=\"503\" loading=\"lazy\"></a>\n\n<br />\n\n設定したいパスワードを入力します。  \nここで、Temporary を `On` にすると、初回ログイン時にパスワードの変更を求められますので、`Off` にしておきます。  \n**Save** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image12.png\" alt=\"パスワード入力\" width=\"1318\" height=\"654\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image13.png\" alt=\"Save password\" width=\"1315\" height=\"584\" loading=\"lazy\"></a>\n\n<br />\n\nテストユーザーの準備完了です。\n\n<br />\n\n# SP：アプリ環境作成\n\n・Debian Bullseye with Raspberry Pi Desktop 2022-07-01(Debian version: 11)  \nの OS インストール直後からスタートして、  \n・Apache 2.4.56  \n・PHP 7.4.33  \n・libapache2-mod-auth-mellon 0.17.0-1+deb11u1  \nをインストールします。\n\n<blockquote class=\"info\">\n<p>OS インストール方法は、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/dl9sesmgga3\" target=\"_blank\">Raspberry Pi OS を VMware-workstation-16.1.1 にインストール</a>」を参考にしてください。※バージョンが少し古いですが、手順は同じです。</p>\n</blockquote>\n\n<br />\n\nここで、<span style=\"color: red;\"><strong>SAML クライアント、つまり、アプリ（SP）側の URL は、`https://kcapp.example.com/`</strong></span> であるという前提として進めます。\n\n<br />\n\n`https://kcapp.example.com/`  \nで Apache のデフォルト画面（`index.html`）  \n`https://kcapp.example.com/info.php`  \nで `phpinfo()` の画面を表示する Web サーバーを構築し、それを「アプリ」とします。\n\n<blockquote class=\"warn\">\n<p>全て root 権限で実行しています。そのため、sudo は省略しています。</p>\n<p>hosts 登録しないといけないとか当たり前のことは省略しています。</p>\n</blockquote>\n\n```shellsession\n# apt -y update\n# apt -y install apache2\n# apt -y install libapache2-mod-auth-mellon\n# a2enmod auth_mellon\n# a2enmod ssl\n# a2ensite default-ssl\n# openssl genrsa -aes128 2048 > server.key\n# openssl req -new -key server.key > server.csr\n# openssl x509 -in server.csr -days 365 -req -signkey server.key > server.crt\n# openssl rsa -in server.key -out server.key\n# cp -p server.crt /etc/ssl/certs/server.crt\n# cp -p server.key /etc/ssl/private/server.key\n# chmod 400 /etc/ssl/private/server.key\n# vi /etc/apache2/sites-available/default-ssl.conf\n    SSLCertificateFile      /etc/ssl/certs/ssl-cert-snakeoil.pem\n    SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\nを以下に変更\n↓\n    SSLCertificateFile      /etc/ssl/certs/server.crt\n    SSLCertificateKeyFile /etc/ssl/private/server.key\n# apt -y install php-common libapache2-mod-php php-cli\n# systemctl restart apache2\n# vi /var/www/html/info.php\n```\n\n```php:/var/www/html/info.php\n<?php\n  phpinfo();\n```\n\nここまで実施して、動作確認します。\nなお、<span style=\"color: red;\"><strong>この時点では、mod_auth_mellon の設定をしていないため、SSO 認証無しです。</strong></span>\n\n<br />\n\n`https://kcapp.example.com/`\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image14.png\" alt=\"kcapp.example.comアクセス\" width=\"1287\" height=\"255\" loading=\"lazy\"></a>\n\n<br />\n\n`https://kcapp.example.com/info.php`\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image15.png\" alt=\"kcapp.example.com info.phpアクセス\" width=\"1284\" height=\"303\" loading=\"lazy\"></a>\n\n<br />\n\nOK！\n\n<br />\n\n# SP：mod auth_mellon 設定\n\nApache の conf に mod_auth_mellon の設定を追加します。\nここでは、簡略化のため、既に最初から存在する defult-ssl.conf に `VirtualHost` の設定を追加します。\n\n<blockquote class=\"warn\">\n<p>設定に指定されている各ファイルは、この後作成します。</p>\n</blockquote>\n\n```shellsession\n# vi /etc/apache2/sites-available/default-ssl.conf\n```\n\n```apacheconf:/etc/apache2/sites-available/default-ssl.conf\n<IfModule mod_ssl.c>\n\t<VirtualHost *:443>\n\t\tServerAdmin webadmin@kcapp.example.com\n\t\tServerName kcapp.example.com\n\t\tDocumentRoot /var/www/html\n\n\t\tSSLEngine on\n\t\tSSLCertificateKeyFile /etc/apache2/saml/https_kcapp.example.com.key\n\t\tSSLCertificateFile /etc/apache2/saml/https_kcapp.example.com.cert\n\t\t<Location />\n\t\t\t# SAML認証に関するエンドポイントのパス\n\t\t\t# エンドポイント=IdPとSPがSAML認証の要求や応答をやり取りするためのURL\n\t\t\tMellonEndpointPath \"/mellon\"\n\t\t\t# IdPのメタデータファイルのパス\n\t\t\t# メタデータ=IdPやSPの設定や機能を記述したXMLファイル\n\t\t\tMellonIdPMetadataFile /etc/apache2/saml/idp_metadata.xml\n\t\t\t# SPの秘密鍵ファイルのパス\n\t\t\t# 秘密鍵=SAML認証の通信を暗号化するための鍵\n\t\t\tMellonSPPrivateKeyFile /etc/apache2/saml/https_kcapp.example.com.key\n\t\t\t# SPの証明書ファイルのパス\n\t\t\t# 証明書=SPの秘密鍵に対応する公開鍵を含むファイル\n\t\t\tMellonSPCertFile /etc/apache2/saml/https_kcapp.example.com.cert\n\t\t\t# SPのメタデータファイルのパス\n\t\t\tMellonSPMetadataFile /etc/apache2/saml/https_kcapp.example.com.xml\n\n\t\t\t# SAML認証を使用する\n\t\t\tAuthType \"Mellon\"\n\t\t\tRequire valid-user\n\t\t\t# SAML認証を有効にする\n\t\t\tMellonEnable \"auth\"\n\t\t</Location>\n\t</VirtualHost>\n...（既存設定）\n```\n\n<br />\n\n# SP：SAML 認証メタデータ作成\n\n<span style=\"color: red;\"><strong>秘密鍵、証明書、SAML の詳細な設定を行うメタデータ（XML ファイル）の配置が必要になるのですが、`mellon_create_metadata.sh` を使って、一気に終わらせます。</strong></span>  \nそのため、メタデータの内容に関する説明は省略します。\n\n```shellsession\n# mkdir /etc/apache2/saml\n# cd /etc/apache2/saml\n# ENTITY_ID=https://kcapp.example.com\n# BASE_URL=https://kcapp.example.com/mellon\n# curl -O https://raw.githubusercontent.com/latchset/mod_auth_mellon/main/mellon_create_metadata.sh\n# chmod 755 mellon_create_metadata.sh\n# ./mellon_create_metadata.sh ${ENTITY_ID} ${BASE_URL}\nOutput files:\nPrivate key:               https_kcapp.example.com.key\nCertificate:               https_kcapp.example.com.cert\nMetadata:                  https_kcapp.example.com.xml\n\nHost:                      kcapp.example.com\n\nEndpoints:\nSingleLogoutService:       https://kcapp.example.com/mellon/logout\nAssertionConsumerService:  https://kcapp.example.com/mellon/postResponse\n# ls\nhttps_kcapp.example.com.cert  https_kcapp.example.com.key  https_kcapp.example.com.xml  mellon_create_metadata.sh\n```\n\n<blockquote class=\"info\">\n<p>【ENTITY_ID】</p>\n<p>SAMLのEntity IDは、アプリケーション（SP）を一意に識別するためのものです。SAML 認証では、IdP と SP はそれぞれのメタデータを交換します。メタデータとは、IdP や SP の設定や機能を記述した XML ファイルです。メタデータには、ENTITY_ID が含まれており、IdP と SP が互いに認識するために使用されます。</p>\n<p>URL形式であることが推奨されますが、URL形式であることは必須ではありません。</p>\n<p><span style=\"color: red;\"><strong>Keycloak で設定する Client ID と同一にします。（今回この後自動的に同一になります。）</strong></span></p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【BASE_URL】</p>\n<p>BASE_URL は、SP のエンドポイントのベースとなる URL です。エンドポイントとは、IdP と SP が SAML 認証の要求や応答をやり取りするための URL です。エンドポイントには、以下のような種類があります。</p>\n<p>SingleLogoutService：ログアウト処理を行うエンドポイント</p>\n<p>AssertionConsumerService：認証応答を受け取るエンドポイント</p>\n<p>SingleSignOnService：認証要求を送るエンドポイント</p>\n<p><span style=\"color: red;\"><strong>BASE_URL は、アプリケーションで使用しない URL パスを指定する必要があります。</strong></span></p>\n</blockquote>\n\nカレントディレクトリに  \nhttps_kcapp.example.com.cert  \nhttps_kcapp.example.com.key  \nhttps_kcapp.example.com.xml  \nが作成されます。\n\n<br />\n\n<span style=\"color: red;\"><strong>idp_metadata.xml がまだありません。Keycloak から取得します。</strong></span>  \n<span style=\"color: red;\"><strong>取得先は、`https://kctest.contoso.com/realms/[Realm名（今回は、TestRealm）]/protocol/saml/descriptor` です。</strong></span>\n\n```shellsession\n# curl -k -o /etc/apache2/saml/idp_metadata.xml \\\nhttps://kctest.contoso.com/realms/TestRealm/protocol/saml/descriptor\n```\n\n<br />\n\n設定を反映します。\n\n```shellsession\n# systemctl reload apache2\n```\n\n<br />\n\n# IdP：Keycloak SAML 設定\n\nKeycloak 管理コンソールにて、SAML クライアントの設定を行います。  \nここまで、SAML クライアント、つまり、アプリ（SP）側の URL は、`https://kcapp.example.com/` であるという前提として進めてきました。  \n<span style=\"color: red;\"><strong>SAML クライアント（mod_auth_mellon のサーバー）は既に構築済みのため、mod_auth_mellon のサーバーからクライアントメタデータをダウンロードして、それを Keycloak へインポートして登録します。</strong></span>  \n<span style=\"color: red;\">これにより、本来、Keycloak 管理コンソールで設定が必要な URL の設定や Signing keys config の登録など、省略することになります。</span>\n\n<br />\n\n`https://kcapp.example.com/mellon/metadata` から SP のメタデータをダウンロードします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image16.png\" alt=\"SP のメタデータをダウンロード\" width=\"1079\" height=\"175\" loading=\"lazy\"></a>\n\n<br />\n\nKeycloak 管理コンソールから TestRealm に移動して、\n**Clients** をクリックして、**Import client** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image17.png\" alt=\"Import client\" width=\"1203\" height=\"341\" loading=\"lazy\"></a>\n\n<br />\n\n**Browse...** クリックし、先ほどダウンロードしたファイルを選択します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image18.png\" alt=\"Browse... クリック\" width=\"1199\" height=\"271\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image19.png\" alt=\"ファイルを選択\" width=\"1202\" height=\"472\" loading=\"lazy\"></a>\n\n<br />\n\nインポートされるため、ひとまず、何も変更せずに、**Save** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image20.png\" alt=\"Save クリック\" width=\"1358\" height=\"981\" loading=\"lazy\"></a>\n\n<br />\n\nKeycloak に SAML クライアントが登録されました。  \nその他、設定は全てデフォルトとします。  \nValid redirect URIs は、認可後リダイレクトされる URI を入力します。デフォルトは、`*` です。  \nパスだけ設定する場合は、Root URL からの相対パスとなります。  \n今回、認可後リダイレクトされる URI は、`https://kcapp.example.com/mellon/postResponse` になるため、`https://kcapp.example.com/mellon/postResponse` が自動的に設定されています。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image21.png\" alt=\"SAMLクライアント登録直後\" width=\"1356\" height=\"993\" loading=\"lazy\"></a>\n\n<br />\n\n# SAML 認証動作確認\n\ntestuser でログインしてみます。\n\n<blockquote class=\"alert\">\n<p>今回、単一アプリの認証が通ることだけ確認して、SSO の確認は行いません。</p>\n</blockquote>\n\n<br />\n\n`https://kcapp.example.com/` へアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image22.png\" alt=\"ユーザーサインイン画面\" width=\"953\" height=\"708\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image14.png\" alt=\"kcapp.example.comアクセス\" width=\"1287\" height=\"255\" loading=\"lazy\"></a>\n\n<br />\n\n↓（そのまま URL を書き換えて）\n\n`https://kcapp.example.com/info.php` へアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image15.png\" alt=\"kcapp.example.com info.phpアクセス\" width=\"1284\" height=\"303\" loading=\"lazy\"></a>\n\n<br />\n\nOK！\n\n<br />\n\n<blockquote class=\"info\">\n<p>今回の場合、SAML クライアントのメタデータをインポートしたため、特に問題無かったですが、手動でクライアントを登録する場合、以下の問題が発生するかもしれません。</p>\n<p>　</p>\n<p>/opt/keycloak/data/log/keycloak.log に以下のログが出力されている場合、Keycloak 側設定 Client ID と mod_auth_mellon で作成したメタデータの ENTITY_ID がずれている可能性があります。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">2023-11-25 17:14:22,540 WARN [org.keycloak.events] (executor-thread-82) type=LOGIN_ERROR, realmId=167b42ad-a626-4475-9d32-3a8c97e8f6b3, clientId=null, userId=null, ipAddress=192.168.11.5, error=client_not_found, reason=Cannot_match_source_hash</span></p>\n<p>　</p>\n<p>以下の場合は、Signing keys config の Certificate が間違っている可能性があります。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">2023-11-25 18:55:27,139 WARN [org.keycloak.events] (executor-thread-72) type=LOGIN_ERROR, realmId=be563de8-bb3d-49af-9073-1dfe8e859dfd, clientId=null, userId=null, ipAddress=192.168.11.5, error=invalid_signature</span>  </p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image23.png\" alt=\"We are sorry...\" width=\"917\" height=\"265\" loading=\"lazy\"></a></p>\n<p>　</p>\n<p>その他、SP、IdP の時刻が大幅にずれていないか確認が必要です。</p>\n</blockquote>\n\n<br />\n\n# IdP：Keycloak SAML 同意画面\n\nKeycloak の設定によって、同意画面（Consent）を表示することもできます。\n**Clients** → **`https://kcapp.example.com`** → **Consent required** を `On` に設定して、**Save** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image24.png\" alt=\"Consent required を On\" width=\"1192\" height=\"727\" loading=\"lazy\"></a>\n\n<br />\n\nアプリにログインしてみます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image22.png\" alt=\"ユーザーサインイン画面\" width=\"953\" height=\"708\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image25.png\" alt=\"同意画面（Consent）\" width=\"967\" height=\"666\" loading=\"lazy\"></a>\n\n<br />\n\n同意画面が追加されました！\n\n<br />\n\n<blockquote class=\"info\">\n<p>同意した事実は、Keycloak 管理コンソールから TestRealm に移動して、</p>\n<p><strong>Users</strong> → <strong>testuser</strong> → <strong>Consents</strong> タブにて、確認できて、取り消すこともできます。（取り消すと、もう一度同意画面が出てきます。）</p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image26.png\" alt=\"Consents確認\" width=\"1244\" height=\"465\" loading=\"lazy\"></a></p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"info\">\n<p>今回は、内部 DB にユーザー情報を登録していますが、Kerberos（Active Directory）や LDAP にもできます。</p>\n<p>OpenLDAP を利用した例は、過去記事「<a href=\"\" target=\"_blank\">Keycloak PostgreSQL OpenLDAP mod_auth_openidc で SSO 全手順</a>」にあります。</p>\n</blockquote>\n\n<br />\n\n# IdP：Azure AD\n\nIdentity providers に Azure AD（Azure Active Directory／Microsoft Entra ID）を追加して、Azure AD のアカウントで認証するように変更していきます。  \nまず、Azure AD 側の設定を行います。\n\n<br />\n\nAzure ポータルから、**Microsoft Entra ID** に移動して、**エンタープライズ アプリケーション** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image27.png\" alt=\"Microsoft Entra ID\" width=\"1255\" height=\"213\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image28.png\" alt=\"エンタープライズ アプリケーション\" width=\"1254\" height=\"616\" loading=\"lazy\"></a>\n\n<br />\n\n**＋新しいアプリケーション** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image29.png\" alt=\"＋新しいアプリケーション\" width=\"1256\" height=\"376\" loading=\"lazy\"></a>\n\n<br />\n\n**＋独自のアプリケーション作成** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image30.png\" alt=\"＋独自のアプリケーション作成\" width=\"1252\" height=\"331\" loading=\"lazy\"></a>\n\n<br />\n\nお使いのアプリの名前は何ですか?  \nのところを `KeycloakTEST` とします。  \n`ギャラリーに見つからないその他のアプリケーションを統合します (ギャラリー以外)` にチェックが入った状態とし、**作成** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image31.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image31.png\" alt=\"アプリケーション作成\" width=\"1256\" height=\"485\" loading=\"lazy\"></a>\n\n<br />\n\n**シングル サインオンの設定** のところの **作業の開始** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image32.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image32.png\" alt=\"シングル サインオンの設定\" width=\"1255\" height=\"624\" loading=\"lazy\"></a>\n\n<br />\n\n**SAML** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image33.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image33.png\" alt=\"SAML\" width=\"1254\" height=\"712\" loading=\"lazy\"></a>\n\n<br />\n\n**編集** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image34.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image34.png\" alt=\"編集\" width=\"1252\" height=\"526\" loading=\"lazy\"></a>\n\n<br />\n\n識別子 (エンティティ ID)：  \n`https://kctest.contoso.com/realms/TestRealm`\n\n<br />\n\n応答 URL (Assertion Consumer Service URL)：  \n`https://kctest.contoso.com/realms/TestRealm/broker/saml/endpoint`  \nとし、**保存** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image35.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image35.png\" alt=\"識別子 (エンティティ ID)と応答 URL (Assertion Consumer Service URL)\" width=\"1275\" height=\"985\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>識別子 (エンティティ ID)は、mod_auth_mellon で出てきた ENTITY_ID と考え方は同じで、一意の識別子です。何でも良いですが、この後 Keycloak の設定画面で初期設定された状態で表示されるのがこの URL です。</p>\n<p>応答 URL (Assertion Consumer Service URL) もこの後 Keycloak の設定画面で固定のエンドポイント URL が表示されるからこの URL です。ここでは、両方分かっているものとします。</p>\n</blockquote>\n\n<br />\n\nここで、<span style=\"color: red;\"><strong>アプリのフェデレーション メタデータ URL をコピー</strong></span>して、メモっておきます。※後でも見に来れます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image36.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image36.png\" alt=\"アプリのフェデレーション メタデータ URL をコピー\" width=\"1278\" height=\"985\" loading=\"lazy\"></a>\n\n<br />\n\n**ユーザーとグループ** をクリックし、**＋ユーザーまたはグループの追加** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image37.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image37.png\" alt=\"＋ユーザーまたはグループの追加\" width=\"1106\" height=\"464\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>しばらく待つか、一旦初期画面に戻って、KeycloakTEST を選択しなおさないといけないかもしれません。</p>\n<p>続けて操作していた時、割り当てられず、エラーになりました。</p>\n</blockquote>\n\n<br />\n\n割り当ての追加 画面に切り替わり、**選択されていません** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image38.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image38.png\" alt=\"選択されていません\" width=\"1271\" height=\"257\" loading=\"lazy\"></a>\n\n<br />\n\nSAML 認証を使うユーザー／グループを選択し、**選択** をクリックします。  \n今回は、`すべてのユーザー` にします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image39.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image39.png\" alt=\"すべてのユーザー\" width=\"1103\" height=\"580\" loading=\"lazy\"></a>\n\n<br />\n\n**割り当て** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image40.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image40.png\" alt=\"割り当て\" width=\"1101\" height=\"421\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image41.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image41.png\" alt=\"割り当て完了後\" width=\"1101\" height=\"449\" loading=\"lazy\"></a>\n\n<br />\n\nこれで Azure AD 側は、準備完了です。\n\n<br />\n\n# IdP/SP：Keycloak Identity providers 追加\n\nAzure AD と紐づけて、IdP とします。  \nKeycloak を アプリにとっての IdP、Azure AD にとっての SP とします。\n\n<br />\n\nKeycloak 管理コンソールから TestRealm に移動して、  \n**Identity providers** をクリックして、**SAML v2.0** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image42.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image42.png\" alt=\"SAML v2.0\" width=\"1233\" height=\"988\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>SAML entity descriptor の設定のところに、先ほど Azure AD に表示されていてコピーした アプリのフェデレーション メタデータ URL をペーストします。</strong></span>\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image43.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image43.png\" alt=\"アプリのフェデレーション メタデータ URL をペースト\" width=\"1273\" height=\"902\" loading=\"lazy\"></a>\n\nここで、Keycloak → `https://login.microsoftonline.com/...` とサーバーサイドでアクセスが行きます。  \nKeycloak がインターネットに出られない場合、プロキシ経由で出る必要があります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">No valid metadata was found at thie URL:'Network response was not OK.'</span>  \nとなった場合、そういうことです。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu3.png\" alt=\"No valid metadata was found 図\" width=\"871\" height=\"461\" loading=\"lazy\"></a>\n\n↓  \n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/zu4.png\" alt=\"プロキシ利用でOK 図\" width=\"871\" height=\"461\" loading=\"lazy\"></a>\n\n<br />\n\nここでは、プロキシ経由でインターネットへ出る設定を行います。  \n緑色のチェックマークが付いた場合、必要のない作業ですので、読み飛ばしてください。\n\n<br />\n\n```shellsession\n# vi /etc/systemd/system/keycloak.service\n```\n\n```systemd:/etc/systemd/system/keycloak.service\n以下のように書き換え（http://192.168.0.158:3128 は、プロキシサーバーのこと）\nExecStart=/opt/keycloak/bin/kc.sh start --log=console,file\n↓\nExecStart=/opt/keycloak/bin/kc.sh start --log=console,file --spi-connections-http-client-default-proxy-mappings=\"'.*\\\\\\.microsoftonline\\\\\\.com;http://192.168.0.158:3128'\"\n```\n\n<blockquote class=\"info\">\n<p>余談ですが、<a href=\"https://www.keycloak.org/server/outgoinghttp\" target=\"_blank\">マニュアルの Configuring outgoing HTTP requests</a>（2023 年 11 月時点）に</p>\n<p><code>bin/kc.[sh|bat] start --spi-connections-http-client-default-&lt;configurationoption&gt;=&lt;value&gt;</code></p>\n<p>と説明があるのですが、<code>proxy-mappings</code> の具体的な記述例の</p>\n<p><code>...-proxy-mappings=\"'</code> の直後にドットが無く、これをベースに設定しても効きませんでした。ドットが必要でした。</p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image45.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image45.png\" alt=\"マニュアルの Configuring outgoing HTTP requests ドットが必要\" width=\"1089\" height=\"239\" loading=\"lazy\"></a></p>\n</blockquote>\n\n```shellsession\n# systemctl daemon-reload\n# systemctl restart keycloak.service\n```\n\nでプロキシが有効になり、緑色のチェックマークが付くはずです。\n\n<blockquote class=\"info\">\n<p>他の方法としては、keycloak.service の</p>\n<p><code>[Service]</code> セクションに</p>\n<p><code>Environment=\"HTTP_PROXY=http://192.168.0.158:3128\"</code></p>\n<p><code>Environment=\"HTTPS_PROXY=http://192.168.0.158:3128\"</code></p>\n<p><code>Environment=\"NO_PROXY=.example.com,.contoso.com\"</code></p>\n<p>と記述してもプロキシ有効になりました。</p>\n<p>※この場合、<code>kc.sh start</code> のオプションは変更不要です。</p>\n</blockquote>\n\n<br />\n\nここからは、プロキシうんぬん関係なく、共通の手順に戻ります。  \n↓\n\n<br />\n\nSAML entity descriptor の URL が認識されたら、**Add** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image46.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image46.png\" alt=\"SAML entity descriptor Add\" width=\"1270\" height=\"284\" loading=\"lazy\"></a>\n\n<br />\n\nいろいろ自動登録されますので、ひとまず、**Save** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image47.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image47.png\" alt=\"Identity providers Save\" width=\"1286\" height=\"1318\" loading=\"lazy\"></a>\n\n<br />\n\n# SAML Azure AD 認証動作確認１\n\n`https://kcapp.example.com/` へアクセスすると、  \nOr sing in with  \n**saml**  \nが追加されていますので、**saml** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image48.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image48.png\" alt=\"samlをクリック\" width=\"1132\" height=\"829\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><strong>saml</strong> の表記は、Keycloak の <strong>Identity providers</strong> → <strong>saml</strong> → General settings - Alias 設定の文字列になります。</p>\n</blockquote>\n\n<br />\n\nAzure AD でおなじみのサインイン画面が表示されるようになります。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" alt=\"Azure ADサインイン画面１\" width=\"878\" height=\"613\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image50.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image50.png\" alt=\"Azure ADサインイン画面２\" width=\"881\" height=\"573\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image51.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image51.png\" alt=\"Azure ADサインイン画面３\" width=\"876\" height=\"571\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！っといきたいところですが、  \nKeycloak の **Identity providers** → **saml** → Advanced settings - First login flow  \nの設定が `first broker login` となっているため、最初に Username, Email, First name, Last name の登録を迫られます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image52.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image52.png\" alt=\"first broker loginフローの画面\" width=\"1167\" height=\"899\" loading=\"lazy\"></a>\n\n<br />\n\n今回は、ここで止まって欲しくないので、First login flow の設定を変更します。\n\n<blockquote class=\"info\">\n<p>First login flow は、ユーザーが初めて外部 IdP からログインした後に使用されるワークフローを選択します。</p>\n<p>デフォルトでは、この設定は <code>first broker login</code> を指していますが、独自のフローを作成して設定することも可能です。</p>\n</blockquote>\n\n<br />\n\n# IdP/SP：Keycloak Flow 追加\n\nFirst login flow に簡略化されたフローを指定したいため、オリジナルのフローを作成します。  \n※フローについての細かい説明は省略します。\n\n<br />\n\nKeycloak 管理コンソールから TestRealm に移動して、\n**Authentication** をクリックして、**Create flow** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image53.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image53.png\" alt=\"Create flow\" width=\"1201\" height=\"727\" loading=\"lazy\"></a>\n\n<br />\n\nName に `None` を入力して、**Create** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image54.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image54.png\" alt=\"Noneフロー Create\" width=\"1210\" height=\"726\" loading=\"lazy\"></a>\n\n<br />\n\n**Add execution** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image55.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image55.png\" alt=\"Add execution\" width=\"1197\" height=\"714\" loading=\"lazy\"></a>\n\n<br />\n\n`Create User If Unique` にチェックを入れて、**Add** をクリックします。\n\n<blockquote class=\"info\">\n<p>Create User if Unique ステップは、Keycloak アカウント（今回は、<code>testuser</code> 一人）と email が一致した場合、そのユーザーと特定し、それ以外の場合は、新規に Keycloak アカウント 作成という意味です。</p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image56.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image56.png\" alt=\"Create User If Unique Add\" width=\"1199\" height=\"727\" loading=\"lazy\"></a>\n\n<br />\n\nRequirement を `Alternative` と変更します。（変更した時点でシステムに反映されます。）\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image57.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image57.png\" alt=\"Alternative\" width=\"1209\" height=\"634\" loading=\"lazy\"></a>\n\n<br />\n\n再び、\n**Identity providers** → **saml** → Advanced settings - First login flow\nに戻ると、`None` が選択可能になっているため、`None` を選択して、**Save** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image58.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image58.png\" alt=\"Advanced settings - First login flow\" width=\"1194\" height=\"572\" loading=\"lazy\"></a>\n\n<br />\n\n# SAML Azure AD 認証動作確認２\n\n`https://kcapp.example.com/` へアクセスし、**saml** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image48.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image48.png\" alt=\"samlをクリック\" width=\"1132\" height=\"829\" loading=\"lazy\"></a>\n\n<br />\n\nAzure AD のサインイン画面が表示され、サインインします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" alt=\"Azure ADサインイン画面１\" width=\"878\" height=\"613\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image50.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image50.png\" alt=\"Azure ADサインイン画面２\" width=\"881\" height=\"573\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image51.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image51.png\" alt=\"Azure ADサインイン画面３\" width=\"876\" height=\"571\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image25.png\" alt=\"同意画面（Consent）\" width=\"967\" height=\"666\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image14.png\" alt=\"kcapp.example.comアクセス\" width=\"1287\" height=\"255\" loading=\"lazy\"></a>\n\n<br />\n\n↓（そのまま URL を書き換えて）\n\n<br />\n\n`https://kcapp.example.com/info.php` へアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image15.png\" alt=\"kcapp.example.com info.phpアクセス\" width=\"1284\" height=\"303\" loading=\"lazy\"></a>\n\n<br />\n\nアプリが表示されました！  \nヨシ！\n\n<br />\n\nしかし、実は、Keycloak の **Users** を見ると、Azure AD が決めた ID で強引に登録されています。誰が誰だか分かりません。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image59.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image59.png\" alt=\"Users確認\" width=\"1197\" height=\"541\" loading=\"lazy\"></a>\n\n<br />\n\n・・・あとは頑張ればなんとかなりそうだし、ヨシ！\n\n<br />\n","description":"Ubuntu 22.04.3 LTS に Keycloak 22.0.5(Quarkus 版) をインストールして、SAML による SSO（シングルサインオン／Single Sign On）環境を作成しました。さらに、Azure AD（Microsoft Entra ID）と連携しました。その全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"m_9ef_z4y","createdAt":"2023-11-28T10:33:28.829Z","updatedAt":"2023-11-28T10:33:28.829Z","publishedAt":"2023-11-28T10:33:28.829Z","revisedAt":"2023-11-28T10:33:28.829Z","url":"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/ITC_Engineering_Blog.png","alt":"Ubuntu 22にKeycloak 22をインストールして、Identity providers=Azure ADでSAML","width":1200,"height":630}],"seo_authors":[]},{"id":"i-hna4_wx","createdAt":"2021-05-06T08:40:55.281Z","updatedAt":"2021-07-16T10:11:21.627Z","publishedAt":"2021-05-06T08:40:55.281Z","revisedAt":"2021-07-16T10:11:21.627Z","title":"Ubuntu 20.04.2.0にapache2,php,postgresqlをインストール","category":{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},"topics":[{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},{"id":"k7x51z-0y5","createdAt":"2021-05-05T06:30:34.213Z","updatedAt":"2021-08-31T12:05:59.237Z","publishedAt":"2021-05-05T06:30:34.213Z","revisedAt":"2021-08-31T12:05:59.237Z","topics":"Apache","logo":"/logos/Apache.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false},{"id":"nh6w1w9gt","createdAt":"2021-05-05T06:31:09.993Z","updatedAt":"2021-08-31T12:05:45.656Z","publishedAt":"2021-05-05T06:31:09.993Z","revisedAt":"2021-08-31T12:05:45.656Z","topics":"PostgreSQL","logo":"/logos/PostgreSQL.png","needs_title":false}],"content":"# はじめに\nUbuntu 20.04.2.0（Desktop）  \nに  \n・Apache 2.4.41  \n・PHP 8.0.3  \n・PostgreSQL 12.6  \nインストールを行います。  \n作業時期は、2021年4月ですので、  \n時期によっては、バージョンが異なっていたり、  \nうまくいかなかったりするかもしれません。  \n\n<br />\n\n# apache2インストール\n\n\n```sh\n# apt update\n```\n\n<br />\n\nunattended-upgrades　が既に80番ポートを使っているため、killしました。  \nunattended-upgrades　は、Ubuntuの無人アップグレード機能です。\n```sh\n# ps ax | grep unattended\n    778 ?        Ssl    0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal\n   1780 pts/0    S+     0:00 grep --color=auto unattended\n# kill 778\n```\n\n<br />\n\n```sh\n# apt install apache2\nDo you want to continue? [Y/n] Y(or　エンター)\n```\n\n※以降基本的にYのため、-yを付けます。  \n　-y は、? [Y/n] のようなときに自動的に Y とするオプションです。\n\n<br />\n\n<blockquote class=\"info\">\n<p>【パッケージ管理コマンド aptとapt-getの違い】</p>\n<p>Debian管理者ハンドブックによると「aptはapt-getの持っていた設計上のミスを克服しています」と記載されています。</p>\n<p>Ubuntuにおいては、バージョン14.04よりaptコマンドの使用が推奨されています。</p>\n<p>よって、aptを使うのが正解になります。</p>\n<p><a href=\"https://qiita.com/quzq/items/8e47414bf95d1fcfa24a\">参考記事</a></p>\n</blockquote>\n\n<br />\n\n# OS 起動時の apt update と unattended-upgrade を抑制\n\n<blockquote class=\"info\">\n<p>不要と思ってやっただけで、必須ではありません。</p>\n</blockquote>\n\n```sh\n# systemctl edit apt-daily.timer\n# systemctl edit apt-daily-upgrade.timer\n```\nどちらも次の内容で保存\n\n```sh\n[Timer]\nPersistent=false\n```\n\n→CTRL+0, エンター, CTRL+X\n\n```sh\n# systemctl daemon-reload\n```\n\n<br />\n\n<blockquote class=\"info\">\n<p>※恒久的に削除する場合</p>\n</blockquote>\n\n```sh\n# apt remove unattended-upgrades\n```\n\n<br />\n\n# postgresqlインストール\n```sh\n# apt -y install postgresql postgresql-contrib\n```\n<blockquote class=\"info\">\n<p>postgresql-contribは、pgbenchなどの便利ツール群です。</p>\n</blockquote>\n\n<br />\n\n```sh\n# mkdir /var/lib/postgresql/data\n# chown postgres:postgres /var/lib/postgresql/data\n# su - postgres\n$ /usr/lib/postgresql/12/bin/initdb --encoding='UTF-8' -D /var/lib/postgresql/data\n$ exit\n```\n\n<br />\n\n# phpインストール\n\n```sh\n# apt -y install software-properties-common\n```\n\n<blockquote class=\"info\">\n<p>software-properties-commonについて調べると、</p>\n<p>「このソフトウェアは、使用されているaptリポジトリの抽象化を提供します。これにより、ディストリビューションと独立したソフトウェアベンダーのソフトウェアソースを簡単に管理できます。」</p>\n<p>とあり、いまいち良く分かりませんでしたが・・・要するに、software-properties-common→いろいろインストールされる→この後必要な add-apt-repository が使えるようになるから必要な手順になります。</p>\n<p>サードパーティ製の Ubuntu 非公式のリポジトリの情報を Ubuntu に教えたい場合には、add-apt-repository コマンドを使ってリポジトリ情報を追加することができます。</p>\n</blockquote>\n\n<br />\n\nppa:ondrej/phpリポジトリを追加します。  \n```sh\n# add-apt-repository ppa:ondrej/php\nPress [ENTER] to continue or Ctrl-c to cancel adding it.\n```\n\n<blockquote class=\"info\">\n<p>ppaとは、Personal Package Archives の略で、個人が作成したパッケージとそれを保管する場所の意味です。</p>\n<p>ondrejは、オンドレイ（アンドレア？）さんです。</p>\n</blockquote>\n\n<br />\n\n```sh\n# apt update\n# apt -y install php8.0 php8.0-gd php8.0-mbstring php8.0-common\n```\n\n<br />\n\n`php-pgsql対応`\n```sh\n# apt -y install php8.0-pgsql\n```\n\n`php-curl対応`\n```sh\n# apt -y install curl\n# apt -y install php8.0-curl\n```\n\n<br />\n\n# バージョン確認\n\n```sh\n# php -v\nPHP 8.0.3 (cli) (built: Mar  5 2021 07:54:13) ( NTS )\n\n# apache2 -v\nServer version: Apache/2.4.41 (Ubuntu)\n\n# su - postgres -c \"/usr/lib/postgresql/12/bin/postmaster -V\"\npostgres (PostgreSQL) 12.6 (Ubuntu 12.6-0ubuntu0.20.04.1)\n\n# curl -V\ncurl 7.68.0 (x86_64-pc-linux-gnu) libcurl/7.68.0 OpenSSL/1.1.1f zlib/1.2.11 brotli/1.0.7 libidn2/2.2.0 libpsl/0.21.0 (+libidn2/2.2.0) libssh/0.9.3/openssl/zlib nghttp2/1.40.0 librtmp/2.3\n```\n\n<br />\n\nインストールできました！  \n\n<br />\n\nphpが動作して、postgresqlの機能が組み込まれていることを確認します。  \n\n<br />\n\n`info.php作成`\n```sh\n# vi /var/www/html/info.php\n```\n\n```php\n<?php\n  phpinfo();\n?>\n```\n<blockquote class=\"info\">\n<p>初期設定では、&lt;?php か &lt;?= から始めないとphpプログラムと認識されません。</p>\n<p>php.ini が short_open_tag = On の場合、&lt;? から始めてもＯＫになります。</p>\n<p>なお、phpのコードだけの場合、最後の ?&gt; は有っても無くても構いません。</p>\n</blockquote>\n\n<br />\n\n`phpのエラーを非表示から表示するに変更`\n```sh\n# vi /etc/php/8.0/apache2/php.ini\n```\n\n```sh\ndisplay_errors = On\ndisplay_startup_errors = On\n```\n\n<br />\n\n`apache再起動`  \n```sh\n# service apache2 restart\n```\n\n<br />\n\nhttp://192.168.xxx.xxx/info.php  \nでアクセスします。  \nあるいは、UbuntuのGUIから  \nhttp://localhost/info.php  \nです。  \n\n![info.php](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-php/ubuntu-php.jpg) \n\n<br />\n\nPostgreSQLが組み込まれています。  \n成功です！  \n\n<br />\n\n<blockquote class=\"info\">\n<p>【phpのエラー】</p>\n<p>phpのエラーは、何も設定変更していない現在の場合、</p>\n<p>/var/log/apache2/error.log</p>\n<p>へ</p>\n<p>[php:error]・・・</p>\n<p>で出力されました。</p>\n</blockquote>\n\n<br />\n\nちなみに応答ヘッダを見てみますと・・・  \n\n```sh\n# curl --head http://localhost/info.php\nHTTP/1.1 200 OK\nDate: Tue, 13 Apr 2021 13:53:44 GMT\nServer: Apache/2.4.41 (Ubuntu)\nContent-Type: text/html; charset=UTF-8\n```\n\nServer:　ヘッダからPHPの存在が分からず、  \nX-Powered-By　ヘッダは付きません。  \n","description":"Ubuntu 20.04.2.0（Desktop） に ・Apache 2.4.41 ・PHP 8.0.3 ・PostgreSQL 12.6 インストールを行います。 作業時期は、2021年4月ですので、 時期によっては、バージョンが異なっていたり、 うまくいかなかったりするかもしれません。 apache2インストール # apt update unattended-upgrades　が既に80番ポートを使っているため、killしました。 unattended-upgrades　は、Ubuntuの無人アップグレード機能です。 # ps ax | grep unattended     778 ?        Ssl    0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal    1780 pts/0    S+     0:00 grep --color=auto unattended # kill 778","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"g2ig9se-u","createdAt":"2021-07-16T10:05:21.615Z","updatedAt":"2021-07-16T10:05:21.615Z","publishedAt":"2021-07-16T10:05:21.615Z","revisedAt":"2021-07-16T10:05:21.615Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu-php/ITC_Engineering_Blog.png","alt":"Ubuntu 20.04.2.0にapache2,php,postgresqlをインストール","width":1200,"height":630}],"seo_authors":[]},{"id":"nextjs-blog-search","createdAt":"2021-10-04T13:52:20.425Z","updatedAt":"2021-12-29T08:31:00.321Z","publishedAt":"2021-10-04T13:52:20.425Z","revisedAt":"2021-12-29T08:31:00.321Z","title":"Next.JSのブログにSSRの検索機能を実装してNetlifyにデプロイ","category":{"id":"9acgdtf6pf","createdAt":"2021-06-03T13:51:31.714Z","updatedAt":"2021-08-31T12:04:14.034Z","publishedAt":"2021-06-03T13:51:31.714Z","revisedAt":"2021-08-31T12:04:14.034Z","topics":"Next.js","logo":"/logos/NextJS.png","needs_title":false},"topics":[{"id":"9acgdtf6pf","createdAt":"2021-06-03T13:51:31.714Z","updatedAt":"2021-08-31T12:04:14.034Z","publishedAt":"2021-06-03T13:51:31.714Z","revisedAt":"2021-08-31T12:04:14.034Z","topics":"Next.js","logo":"/logos/NextJS.png","needs_title":false},{"id":"vdv2uk0sio-q","createdAt":"2021-06-03T13:50:09.976Z","updatedAt":"2021-08-31T12:04:35.481Z","publishedAt":"2021-06-03T13:50:09.976Z","revisedAt":"2021-08-31T12:04:35.481Z","topics":"Netlify","logo":"/logos/Netlify.png","needs_title":false}],"content":"# はじめに\nNext.jsで作られているこのブログ（<a href=\"https://github.com/itc-lab/itc-blog\" target=\"_blank\">ソースコード：GitHub</a>）に検索機能を実装しました。\n今回は、どのように実装したか、何がひっかかったか、から、果てはNetlifyへのデプロイまで書いていきます。（作り変え前も書きますので、長いです。）ソースコードの全体は、GitHubにありますので、断片を掲載しての説明になります。  \n\n<br />\n\n●最初の実装  \nCommits on Sep 13, 2021  \nFix image component warning  \n<a href=\"https://github.com/itc-lab/itc-blog/commit/603d035baa8e0be2c794554dd57b9ea7c337d08c\" target=\"_blank\">https://github.com/itc-lab/itc-blog/commit/603d035baa8e0be2c794554dd57b9ea7c337d08c</a>  \n\n<br />\n\n●少し作り変えた実装  \nCommits on Sep 17, 2021  \nUpdate add loading spinner  \n<a href=\"https://github.com/itc-lab/itc-blog/commit/24a167caf720c96d931fee4de9d50342e92fbb95\" target=\"_blank\">https://github.com/itc-lab/itc-blog/commit/24a167caf720c96d931fee4de9d50342e92fbb95</a>  \n\n<br />\n\n<blockquote class=\"alert\">\n<p>検索機能実装にあたり、何から手を付ければ良いか分からず、下記記事とソースコードを参考にさせていただきました。敬意と感謝を申し上げます。</p>\n<p><a href=\"https://zenn.dev/wattanx/articles/d45d5627ffef54\" target=\"_blank\">microCMSブログのNext.js版を作成した</a></p>\n<p><code><a href=\"https://github.com/wattanx/microcms-blog-with-next\" target=\"_blank\">https://github.com/wattanx/microcms-blog-with-next</a></code></p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>CentOS 7.6.1810</code></p>\n<p>　<code>node 14.16.1</code></p>\n<p>　<code>npm 6.14.13</code></p>\n<p>　<code>react 17.0.2</code></p>\n<p>　<code>next 11.1.2</code></p>\n<p><code>Netlify(2021年9月)</code></p>\n</blockquote>\n\n<br />\n\n# 検索機能全体像\nバックエンドは実装していません。  \nmicroCMSのAPIを利用して、全文検索しています。  \nURLにq パラメータを付けて、結果を得ているだけになります。  \n\n<br />\n\n**●最初の実装**  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first1.png\" alt=\"Next.js ブログ 検索機能 最初の実装 図\" width=\"860\" height=\"981\" loading=\"lazy\"></a>  \n\n<br />\n\n検索  \n↓  \nhttps://itc-engineering-blog.netlify.app/search?q=ubuntu  \n（pages/search/index.tsx）  \nへ遷移  \n↓  \ngetServerSideProps  \n↓  \n自分のAPIを呼ぶ  \nhttps://itc-engineering-blog.netlify.app/api/search?q=ubuntu  \n（api/search/index.ts）  \n↓  \napi/search/index.tsからmicroCMS APIを呼ぶ  \nhttps://xxxxx.microcms.io/api/v1/contents?q=ubuntu  \n↓  \n検索結果で画面描画  \n↓  \n以前検索したワードが入力された瞬間（onChange）検索結果をキャッシュにより復元  \n\n<br />\n\n**●少し作り変えた実装**  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/second1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/second1.png\" alt=\"Next.js ブログ 検索機能 少し作り変えた実装 図\" width=\"1008\" height=\"1318\" loading=\"lazy\"></a>  \n\n<br />\n\n検索  \n↓  \nhttps://itc-engineering-blog.netlify.app/search?q=ubuntu  \n（pages/search/index.tsx）  \nへ遷移  \n↓  \n静的ページ（ビルド済みのHTML/JS）読み込み  \n↓  \n（画面から）自分のAPIを呼ぶ  \nhttps://itc-engineering-blog.netlify.app/api/search?q=ubuntu  \n（api/search/index.ts）  \n↓  \napi/search/index.tsからmicroCMS APIを呼ぶ  \nhttps://xxxxx.microcms.io/api/v1/contents?q=ubuntu  \n↓  \nisLoading = true のときは、スピナー表示  \n↓  \n検索結果で画面描画  \n↓  \n以前検索したワードが入力されて再検索（エンター or 検索ボタンクリック）されたとき、検索結果をキャッシュにより復元  \n\n<br />\n\n# 検索欄\n## 右上検索欄\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click1.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click1.gif\" alt=\"Next.js ブログ 検索機能 右上検索欄 動画\" width=\"500\" height=\"169\" loading=\"lazy\"></a>  \n\n```jsx\n<div className=\"hidden lg:flex border-2 rounded focus-within:ring focus-within:border-blue-300 text-gray-600 focus-within:text-black h-3/4\">\n  <input\n    type=\"text\"\n    className=\"px-2 py-2 w-48 text-black text-sm border-0 rounded-none focus:outline-none\"\n    placeholder=\"サイト内検索\"\n    onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>\n      onEnterKeyEvent(e)\n    }\n  />\n  <button\n    className=\"flex items-center justify-center px-3 border-0 bg-white\"\n    onClick={(e) => onClickSearchButton(e)}>\n    <svg\n      className=\"w-5 h-5\"\n      fill=\"currentColor\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\">\n      <path d=\"M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z\" />\n    </svg>\n  </button>\n</div>\n```\n\nCSSは、<a href=\"https://tailwindcss.com/\" target=\"_blank\">Tailwind</a>のクラスで調整しています。  \n\n<blockquote class=\"info\">\n<p>【 Tailwind 】</p>\n<p>CSSを別のどこかに書かなくても、classNameのところに直接デザインが指定できます。例えば、m-1なら、margin: 0.25rem;を指定した意味になります。（例えば、もうちょっとマージンが欲しいとかの場合、m-2とかm-3とか書き換えるだけになります。）</p>\n<p><code>hidden lg:flex</code>となっているところは、lg=横幅1024px未満の時は、display: none;で非表示、1024px以上のときは、display: flex;で表示という意味です。※lg=1024pxは設定で変更できます。</p>\n</blockquote>\n\n`focus-within:`によって、クリックしてフォーカスが当たったときに、inputとbuttonを内包したdivの枠が変化するギミックになっています。  \n\n<br />\n\n<span style=\"color: red;\">iPhoneの場合、デフォルトでラウンド（縁の丸まり）がかかり、ボタンとの継ぎ目が見えてしまうため、`rounded-none` = `border-radius: 0px;` で明示的にラウンドを否定しないといけませんでした。</span>\n\n<br />\n\n## 右上検索ボタン\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click2.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click2.gif\" alt=\"Next.js ブログ 検索機能 右上検索ボタン 動画\" width=\"500\" height=\"106\" loading=\"lazy\"></a>  \n\n```jsx\n<div\n  className=\"flex lg:hidden items-center mr-2 md:mr-10\"\n  onClick={() => setSearchModal(true)}>\n  <svg\n    className=\"w-6 h-6 text-white cursor-pointer\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\">\n    <path d=\"M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z\" />\n  </svg>\n</div>\n```\n\n`flex lg:hidden`で、先ほどの右上検索欄とは逆に、1024px未満で表示されるようにしています。  \n`useState`の`setSearchModal`で、`isSearchModal`を`true`に変更して、再描画がかかり、以下の中央検索欄を表示しています。\n\n<br />\n\n## 中央検索欄\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click3.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click3.gif\" alt=\"Next.js ブログ 検索機能 中央検索欄 動画\" width=\"500\" height=\"146\" loading=\"lazy\"></a>  \n\n```jsx\n{isSearchModal && (\n  <div\n    className={`menuWrapper ${\n      isSearchModal ? 'menuWrapper__active' : ''\n    }`}\n    onClick={(e) => {\n      closeWithClickOutSideMethod(e, setSearchModal);\n    }}>\n    <form\n      className=\"block absolute top-12 right-0 left-0 z-50 w-11/12 my-0 mx-auto\"\n      action=\"/search\"\n      method=\"get\">\n      <input\n        type=\"text\"\n        className=\"w-full h-11 border border-solid border-gray-200 bg-white shadow text-base pl-2\"\n        autoComplete=\"off\"\n        placeholder=\"サイト内検索\"\n        defaultValue=\"\"\n        name=\"q\"\n      />\n    </form>\n  </div>\n)}\n```\n\n`isSearchModal`で表示非表示を切り替えていて、`false`のときは、DOMが生成されていない状態です。  \n`z-index: 999;` のdivで画面全体を覆って、その中に検索欄を表示しています。このdivは、クリックされると、`closeWithClickOutSideMethod`でform以外がクリックされたことを判定して、消えるようにしています。  \n\n<br />\n\n## 検索結果画面検索欄\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click4.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_click4.gif\" alt=\"Next.js ブログ 検索機能 検索結果画面検索欄 動画\" width=\"500\" height=\"77\" loading=\"lazy\"></a>  \n\n```jsx\n<div className=\"flex justify-center px-5 pb-5\">\n  <div className=\"flex w-full max-w-screen-sm border-2 rounded focus-within:ring focus-within:border-blue-300 text-gray-600 focus-within:text-black\">\n    <input\n      type=\"text\"\n      value={searchValue}\n      className=\"w-full px-2 py-2 text-black border-0 rounded-none focus:outline-none\"\n      placeholder=\"サイト内検索\"\n      onChange={(e) => setSearchValue(e.target.value)}\n      onKeyPress={(e) => onEnterKeyEvent(e)}\n    />\n    <button\n      className=\"flex items-center justify-center px-3 border-0 bg-white\"\n      onClick={(e) => onClickSearchButton(e)}>\n      <svg\n        className=\"w-5 h-5\"\n        fill=\"currentColor\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\">\n        <path d=\"M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z\" />\n      </svg>\n    </button>\n  </div>\n</div>\n```\n\n右上検索欄の長くなったバージョンです。`value={searchValue}`のところで、検索したワードをあらかじめセットしています。  \n\n<br />\n\n# 最初の実装\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search1.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search1.gif\" alt=\"Next.js ブログ 検索機能 最初の実装 動画\" width=\"500\" height=\"220\" loading=\"lazy\"></a>  \n\n<br />\n\n## 検索→自分のAPIへ\n※ここでは、記事一覧画面、右上検索欄エンターキーで検索したものとします。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first2.png\" alt=\"Next.js ブログ 検索機能 最初の実装 検索→自分のAPIへ\" width=\"860\" height=\"981\" loading=\"lazy\"></a>  \n\n検索  \n↓  \nhttps://itc-engineering-blog.netlify.app/search?q=ubuntu  \n（pages/search/index.tsx）  \nへ遷移  \n↓  \ngetServerSideProps  \n↓  \n自分のAPIを呼ぶ  \nhttps://itc-engineering-blog.netlify.app/api/search?q=ubuntu  \n（api/search/index.ts）  \n\n<br />\n\n`pages/list/[[...slug]].tsx`  \n\n```typescript\n  const router = useRouter();\n  const onEnterKeyEvent = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (!e.currentTarget.value.trim()) {\n      return;\n    }\n    if (e.key === 'Enter') {\n      router.push(`/search?q=${e.currentTarget.value}`);\n    }\n  };\n```\n\n`useRouter`の`` router.push(`/search?q=${e.currentTarget.value}`); ``により、検索結果画面`/search?q=xxx`に遷移します。  \nこれにより、まず、`pages/search/index.tsx` の `getServerSideProps` が動きます。  \n\n<br />\n\n`getServerSideProps`は、SSR(Server Side Rendering)のため、アクセスするたびに処理されます。※`getStaticPaths`, `getStaticProps`の場合は、SSG(Static Site Generation)で、ビルドするときのみ処理されます。  \n\n<br />\n\nここから、\n`service.getBlogsByQuery`により、`/api/search?q=xxx`にGETが行きます。  \nこのとき、クエリ文字列（?q=xxxのxxxの部分）はURLエンコードされています。  \n直接microCMSのAPIをGETしても良いのですが、画面にAPIキーが露出しないように  \n一旦サーバー側の`/api/search`を経由しています。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/API_KEY.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/API_KEY.png\" alt=\"Next.js ブログ 検索機能 最初の実装 自分のAPI→microCMSのAPIへ\" width=\"781\" height=\"311\" loading=\"lazy\"></a>  \n\n<blockquote class=\"info\">\n<p>Next.jsの\"API Routes\"によって、<code>pages/api</code>配下のコードは、<code>/api/*</code>としてマッピングされて、APIのエンドポイントとして利用できます。サーバーサイドでのみ動作します。</p>\n</blockquote>\n\n<br />\n\n## 自分のAPI→microCMSのAPIへ\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first3.png\" alt=\"Next.js ブログ 検索機能 最初の実装 自分のAPI→microCMSのAPIへ\" width=\"860\" height=\"981\" loading=\"lazy\"></a>  \n\n自分のAPIを呼ぶ  \nhttps://itc-engineering-blog.netlify.app/api/search?q=ubuntu  \n（api/search/index.ts）  \n↓  \napi/search/index.tsからmicroCMS APIを呼ぶ  \nhttps://xxxxx.microcms.io/api/v1/contents?q=ubuntu  \n\n<br />\n\n`pages/api/search/index.ts`\n\n```typescript\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\n\nexport default async (\n  req: NextApiRequest,\n  res: NextApiResponse\n): Promise<void> => {\n  const query: string | string[] = req.query.q;\n  if (!query) {\n    res.status(400).json({ error: `missing queryparamaeter` });\n  }\n  const header: HeadersInit = new Headers();\n  header.set('X-API-KEY', process.env.API_KEY || '');\n  const proxy = process.env.https_proxy;\n  const opt = proxy\n    ? {\n        headers: header,\n        agent: new HttpsProxyAgent(proxy),\n      }\n    : {\n        headers: header,\n      };\n  return await fetch(\n    //ページネーション未実装のため、limit=100\n    `${process.env.API_URL}contents?limit=100&q=${encodeURIComponent(\n      query as string\n    )}`,\n    opt\n  )\n    .then(async (data) => {\n      res.status(200).json(await data.json());\n    })\n    .catch(async (error) => {\n      res.status(500).json(await error.json());\n    });\n};\n```\n\npages/api/search/index.tsからmicroCMS APIを呼んで、結果を返しているだけになります。  \n\n<br />\n\n## 検索結果で画面描画\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/first4.png\" alt=\"Next.js ブログ 検索機能 最初の実装 検索結果で画面描画\" width=\"860\" height=\"981\" loading=\"lazy\"></a>  \n\napi/search/index.tsからmicroCMS APIを呼ぶ  \nhttps://xxxxx.microcms.io/api/v1/contents?q=ubuntu  \n↓  \n検索結果で画面描画  \n\n<br />\n\n`getServerSideProps`でAPIの結果を受け取って、画面の描画に入ります。  \n\n```typescript\ntype Props = {\n  blogs: MicroCmsResponse<IBlog>;\n  query: string;\n};\n\nconst Page: NextPage<Props> = ({ blogs, query }) => {\n  const {\n    searchValue,\n    setSearchValue,\n    onEnterKeyEvent,\n    onClickSearchButton,\n    data,\n  } = useSearchByQuery(query, blogs);\n\n  const [isTooltipVisible, setTooltipVisibility] = useState(false);\n  useEffect(() => {\n    setTooltipVisibility(true);\n  }, []);\n（略）\n```\n\nここで、`blogs`が`useSearchByQuery`に入れられて、それ以降使われません。  \n戻ってきた`data`を画面の描画に使います。  \n\n<br />\n\n`blogs`と`data`は以下の形式のデータです。  \n\n```json\n{\n    \"contents\": [\n        {\n            \"id\": \"fgm-dkbu10\",\n            \"createdAt\": \"2021-05-09T08:41:26.685Z\",\n            \"updatedAt\": \"2021-07-16T10:13:36.149Z\",\n            \"publishedAt\": \"2021-05-09T08:41:26.685Z\",\n            \"revisedAt\": \"2021-07-16T10:13:36.149Z\",\n            \"title\": \"Ubuntu 20.04.2.0にGitLabをインストール\",\n            \"category\": {\n                \"id\": \"xexrrtp93gce\",\n                \"createdAt\": \"2021-05-09T08:36:14.468Z\",\n                \"updatedAt\": \"2021-08-31T12:05:21.841Z\",\n                \"publishedAt\": \"2021-05-09T08:36:14.468Z\",\n                \"revisedAt\": \"2021-08-31T12:05:21.841Z\",\n                \"topics\": \"GitLab\",\n                \"logo\": \"/logos/GitLab.png\",\n                \"needs_title\": false\n            },\n            \"topics\": [\n                {\n                    \"id\": \"xexrrtp93gce\",\n                    \"createdAt\": \"2021-05-09T08:36:14.468Z\",\n                    \"updatedAt\": \"2021-08-31T12:05:21.841Z\",\n                    \"publishedAt\": \"2021-05-09T08:36:14.468Z\",\n                    \"revisedAt\": \"2021-08-31T12:05:21.841Z\",\n                    \"topics\": \"GitLab\",\n                    \"logo\": \"/logos/GitLab.png\",\n                    \"needs_title\": false\n                },\n                {\n                    \"id\": \"bcluojl_o\",\n                    \"createdAt\": \"2021-02-18T07:36:53.394Z\",\n                    \"updatedAt\": \"2021-08-31T12:08:52.380Z\",\n                    \"publishedAt\": \"2021-02-18T07:36:53.394Z\",\n                    \"revisedAt\": \"2021-08-31T12:08:52.380Z\",\n                    \"topics\": \"ubuntu\",\n                    \"logo\": \"/logos/ubuntu.png\",\n                    \"needs_title\": false\n                }\n            ],\n            \"content\": \"（略 ブログの内容）\",\n            \"description\": \"（略 ブログの説明文）\",\n            \"reflect_updatedAt\": false,\n            \"reflect_revisedAt\": false,\n            \"seo_images\": [\n                {\n                    \"id\": \"x5ad6bml5\",\n                    \"createdAt\": \"2021-07-16T10:02:27.966Z\",\n                    \"updatedAt\": \"2021-07-16T10:02:27.966Z\",\n                    \"publishedAt\": \"2021-07-16T10:02:27.966Z\",\n                    \"revisedAt\": \"2021-07-16T10:02:27.966Z\",\n                    \"url\": \"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_install/ITC_Engineering_Blog.png\",\n                    \"alt\": \"Ubuntu 20.04.2.0にGitLabをインストール\",\n                    \"width\": 1200,\n                    \"height\": 630\n                }\n            ],\n            \"seo_authors\": []\n        },\n        {\n            \"id\": \"i-hna4_wx\",\n            ・・・略　同じ形式のデータの繰り返し・・・\n        }\n    ],\n    \"totalCount\": 10,\n    \"offset\": 0,\n    \"limit\": 10\n}\n```\n\n`useSearchByQuery`に`query`と`blogs`を渡して、`hooks/useSearchByQuery.tsx`にて`useQuery`に渡っています。`query`は、`useState`の初期値に渡されて、`searchValue`に名を変え、`blogs`は`initialData`になっています。  \n\n<br />\n\n`hooks/useSearchByQuery.tsx`\n\n```typescript\nexport function useSearchByQuery(\n  query: string,\n  initialData: MicroCmsResponse<IBlog>\n): Props {\n  const [searchValue, setSearchValue] = useState<string>(query);\n  const { isLoading, data, refetch } = useQuery(\n    ['blogs', searchValue],\n    async (context) => {\n      return await new BlogService().getBlogsByQuery(\n        context.queryKey[1] as string\n      );\n    },\n    {\n      initialData: initialData,\n      enabled: false,\n    }\n  );\n\n  const onEnterKeyEvent = async (e: React.KeyboardEvent<HTMLInputElement>) => {\n    console.log('onEnterKeyEvent', e);\n    if (!e.currentTarget.value.trim()) return;\n    if (e.key === 'Enter') {\n      refetch();\n    }\n  };\n\n  const onClickSearchButton = (\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>\n  ) => {\n    const { value } = (e.currentTarget as HTMLButtonElement)\n      .previousElementSibling as HTMLInputElement;\n    if (!value.trim()) {\n      return;\n    }\n    refetch();\n  };\n\n  return {\n    setSearchValue,\n    onEnterKeyEvent,\n    onClickSearchButton,\n    data,\n    searchValue,\n    isLoading,\n  };\n```\n\n`useQuery`の詳細は、<a href=\"https://react-query.tanstack.com/reference/useQuery\" target=\"_blank\">`https://react-query.tanstack.com/reference/useQuery`</a>になるのですが、\"queryKey\"毎に検索結果をキャッシュします。以下のように一度キャッシュした結果は瞬時に表示されます。今回の場合、`query`（検索文字列）毎にキャッシュになります。入力された瞬間に表示されるのは、`onChange`でキャッシュを引き出しているからです。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_cache.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_cache.gif\" alt=\"Next.js ブログ 検索機能 useQuery キャッシュ 動画\" width=\"500\" height=\"232\" loading=\"lazy\"></a>  \n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/useSearchQuery.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/useSearchQuery.png\" alt=\"Next.js ブログ 検索機能 useQuery キャッシュ 図\" width=\"809\" height=\"690\" loading=\"lazy\"></a>  \n\n`initialData: initialData,`の部分は、初回表示時`getServerSideProps`での検索結果をキャッシュに入れています。  \n`enabled: false,`の部分は、検索を実行しないことを意味しています。（既に分かっている検索結果をキャッシュに入れて、`{ isLoading, data, refetch }`を返す）  \nクエリに対する検索キャッシュが無い場合、とりあえず、`initialData`に入れられたデータを返して、バックグラウンドで`fetch`する仕様ですが、`enabled: false,`のため、バックグラウンドで`fetch`は動かないようになっています。つまり、クエリに対する検索キャッシュが無い場合、`initialData`に入れられた既に分かっている検索結果を返す動きだけします。  \n※`isLoading`は使われていません。  \n\n<br />\n\n`data`は、検索結果です。初回表示の時は、`blogs`と同じ値が入っています。  \n`blogs`は、`getServerSideProps`での検索結果です。  \n`refetch`は、クエリを手動で再フェッチする関数です。  \n以下のようにエンターキー押下、検索ボタンクリック時に呼ばれるようになっていて、この時、APIにGETが行きます。  \n検索欄の内容を変更時（`onChange`）、`useQuery`が呼ばれますが、`refetch`は呼ばれないため、キャッシュの`data`によって、再描画されます。（キャッシュが有れば。）  \n\n```typescript\n  const onEnterKeyEvent = async (e: React.KeyboardEvent<HTMLInputElement>) => {\n    console.log('onEnterKeyEvent', e);\n    if (!e.currentTarget.value.trim()) return;\n    if (e.key === 'Enter') {\n      refetch();\n    }\n  };\n\n  const onClickSearchButton = (\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>\n  ) => {\n    const { value } = (e.currentTarget as HTMLButtonElement)\n      .previousElementSibling as HTMLInputElement;\n    if (!value.trim()) {\n      return;\n    }\n    refetch();\n  };\n\n  return {\n    setSearchValue,\n    onEnterKeyEvent,\n    onClickSearchButton,\n    data,\n    searchValue,\n    isLoading,\n  };\n```\n\nなお、`useQuery`を使うときは、`_app.tsx`に以下の記述が必要です。無い場合、`Error: No QueryClient set, use QueryClientProvider to set one`とエラーになります。  \n\n<br />\n\n`pages/_app.tsx の記述例：`\n\n```typescript\nimport { QueryClient, QueryClientProvider } from 'react-query';\n\nconst queryClient = new QueryClient();\n\nconst MyApp: FC<AppProps> = ({ Component, pageProps }) => {\n  usePageView();\n  return (\n    <>\n      <QueryClientProvider client={queryClient}>\n        <Component {...pageProps} />\n      </QueryClientProvider>\n    </>\n  );\n};\n\nexport default MyApp;\n```\n\n<br />\n\n# ビルドのエラー\nproxy環境でもfetchできるようにしていたのですが、今回、クライアント側でもfetchが動くようになり、以下のエラーになりました。  \n\n```sh\nerror - ./node_modules/https-proxy-agent/dist/agent.js:15:0\nModule not found: Can't resolve 'net'\n```\n\n原因は、`getBlogsByQuery`が定義してある`utils/BlogService.ts`の  \n\n```typescript\nimport { HttpsProxyAgent } from 'https-proxy-agent';\n```\n\nにより、クライアント側処理で必要な `net` と `tls` が無いという理由でエラーになっていました。  \nクライアント側処理に関係する`getBlogsByQuery`は、proxyを使っていなくて、別の場所に移したりして何とかなかったかもしれませんが、特に影響無さそうなので、`npm install`しておきました。  \n\n```sh\n$ npm install net\n$ npm install tls\n```\n\n<br />\n\n# 少し作り変えた実装\n最初の実装から違うところだけ書きます。  \n\n## 検索画面へ切り替わり\n※ここでは、記事一覧画面、右上検索欄エンターキーで検索したものとします。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/second2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/second2.png\" alt=\"Next.js ブログ 検索機能 少し作り変えた実装 検索画面へ切り替わり\" width=\"1008\" height=\"1318\" loading=\"lazy\"></a>  \n\n検索  \n↓  \nhttps://itc-engineering-blog.netlify.app/search?q=ubuntu  \n（pages/search/index.tsx）  \nへ遷移  \n↓  \n静的ページ（ビルド済みのHTML/JS）読み込み  \n\n<br />\n\n`useRouter`の`` router.push(`/search?q=${e.currentTarget.value}`); ``により、検索結果画面`/search?q=xxx`に遷移するのですが、`pages/search/index.tsx`が静的ページになっていて、サーバーサイドでは何もしなくなりました。  \n画面が読み込まれたときに、画面から`/api/search?q=xxx`にGETが行きます。  \n\n<br />\n\n## 検索結果で画面描画\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/second3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/second3.png\" alt=\"Next.js ブログ 検索機能 少し作り変えた実装 検索結果で画面描画\" width=\"1008\" height=\"1318\" loading=\"lazy\"></a>  \n\n`useEffect(() => {`（レンダー後に処理）  \n↓  \n`useQuery`発動  \n↓  \n`useRouter`でq=の値を取得  \n↓  \n（画面から）自分のAPIを呼ぶ  \nhttps://itc-engineering-blog.netlify.app/api/search?q=ubuntu  \n（api/search/index.ts）  \n↓  \napi/search/index.tsからmicroCMS APIを呼ぶ  \nhttps://xxxxx.microcms.io/api/v1/contents?q=ubuntu  \n↓  \nisLoading = true の間は、スピナー表示  \n↓  \n検索結果を受け取って、再描画（isLoading = false）  \nとなっています。  \n\n<br />\n\nレンダー後に`const { q } = router.query;`で`?q=`の値を取り出しています。（レンダー後じゃないと取り出せない。）  \nなお、<span style=\"color: red;\">`useEffect`でqueryの値が得られる前に`fetch`するとまずいため、`searchValue`が`undefined`のときは、 `enabled: false` を設定して、`fetch`に行かないようにしています。</span>  \n\n<br />\n\nURLが`/search?q=`に切り替わった瞬間は、以下のように値が遷移します。（「ubuntu」で検索した場合）  \n\n<br />\n\n[レンダー前]  \nsearchValue: `undefined`  \nisLoading: `false`  \ndata: `undefined`  \n↓  \n[レンダー後]  \nsearchValue: `ubuntu`  \nisLoading: `true`  \ndata: `undefined`  \n↓  \n[検索結果取得後]  \nsearchValue: `ubuntu`  \nisLoading: `false`  \ndata: `{contents: Array(10), totalCount: 10, offset: 0, limit: 100}`  \n\n```typescript\nconst Page: NextPage = () => {\n  const router = useRouter();\n\n  const [searchValue, setSearchValue] = useState<string>();\n\n  useEffect(() => {\n    if (!router.isReady) return;\n    const { q } = router.query;\n    setSearchValue(q as string);\n  }, [router.isReady, router.query]);\n\n  const { isLoading, data } = useQuery(\n    ['blogs', searchValue],\n    async (context) => {\n      return await new BlogService().getBlogsByQuery(\n        context.queryKey[1] as string\n      );\n    },\n    {\n      staleTime: Infinity,\n      enabled: searchValue ? true : false,\n    }\n  );\n```\n\n`data`の形式は作り変え前と同じです。  \n\n<br />\n\n`staleTime: Infinity,`により、一度検索した結果は、再確認することなくキャッシュされます。以下のように一度検索した文字で再検索をすると瞬時に表示されます。この時、fetchには行きません。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_ubuntu.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/search_ubuntu.gif\" alt=\"Next.js ブログ 検索機能 少し作り変えた実装 staleTime: Infinity 再検索\" width=\"500\" height=\"218\" loading=\"lazy\"></a>  \n\n`staleTime`はキャッシュを再確認し始めるまでの時間です。`staleTime`の時間が過ぎると、とりあえず手持ちのキャッシュをすぐに返して、バックグラウンドでfetchに行って、内容が更新されていたら、それを新たにキャッシュします。デフォルトは、0です。  \nつまり、デフォルトでは、常にバックグラウンドでfetchに行きます。`Infinity`により、逆に常に再確認に行かないようにしています。  \nキャッシュ関係の設定で、`cacheTime`が有るのですが、こちらは、単純にキャッシュの有効期間です。デフォルトは５分です。  \n\n<br />\n\n**スピナーの表示**  \n\nスピナーの表示は、  \n\n```typescript\nimport FadeLoader from 'react-spinners/FadeLoader';\n```\n\nにて、`FadeLoader`コンポーネントを表示しています。  \nスピナーの形は、<a href=\"https://www.davidhu.io/react-spinners/\">react-spinners demo</a>から選べます。  \n\n```jsx\n{isLoading && (\n  <div className=\"text-center h-20\">\n    <div className=\"inline-block\">\n      <div className=\"relative\" style={{ left: '-20px' }}>\n        <FadeLoader color={'#4A90E2'} loading={isLoading} />\n      </div>\n    </div>\n  </div>\n)}\n```\n\n`color={'#4A90E2'}`のように、コンポーネントの色を指定しています。他にスピード、サイズを変更できます。loading=のところは表示有無です。（それ以前に`isLoading &&`のところで決まっていますが。）今回は色と表示有無しか指定していません。  \n`style={{ left: '-20px' }}`のところは、悩ましかったのですが、<span style=\"color: red;\">`FadeLoader`はデフォルトで`left: 20px`が指定されていて、真ん中に表示しようとしても少しずれるため、親要素を-20pxずらして元に戻しています。</span>  \n`css=`パラメータでCSSが指定できるのですが、CSS-in-JSライブラリの<a href=\"https://github.com/emotion-js/emotion\">emotion</a>を使わないといけないらしく、これだけのために導入はしたくなかったので、`css=`無しで解決しました。  \n\n<br />\n\n**getServerSideProps**  \n\n当初そのつもり無かったのですが、画面読み込み時に検索しているので、SSR(Server Side Rendering)にする必要が無いことに気付き、`getServerSideProps`をまるごと削除して、静的ページにしました。結果、検索結果画面に一瞬で遷移します。  \n以下のコードがまるごと削除したコードです。  \n\n```typescript\nexport const getServerSideProps: GetServerSideProps = async (context) => {\n  const query = context.query.q;\n  const service: IBlogService = new BlogService();\n  const blogs = await service.getBlogsByQuery(query as string);\n  return {\n    props: {\n      blogs: blogs,\n      query: query,\n    },\n  };\n};\n```\n\n<br />\n\n# Netlifyへのデプロイ\n基本的なデプロイの手順は、過去記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/efxq_5j84z\" target=\"_blank\">Next.js microCMS GitHub NetlifyでJamstackなブログを公開</a>」の通りです。今回対応した事だけ書きます。  \n\n## SSG → SSG & SSR について\nSSG(Static Site Generation)だけでやっていたため、package.jsonに`\"export\": \"next build && next export\",`を設定して、  \n\n```sh\n$ npm run export\n```\n\nにより、ビルドしていました。  \n\nが、今回、検索機能がSSR(Server Side Rendering)のため、`npm run export`ではエラーになります。（`next export`が不要。） \n\n\npackage.jsonに`\"build\": \"next build\",`を設定して、  \n\n```sh\n$ npm run build\n```\n\nでビルドする必要がありました。  \nそのため、Netlifyのビルドコマンドの設定 Build & deploy → Build settings → Build command: も  \n`npm run build`  \nに変更しました。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/npmrunbuild.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/npmrunbuild.png\" alt=\"Next.js ブログ 検索機能 Netlify NPM_FLAGS\" width=\"690\" height=\"309\" loading=\"lazy\"></a>  \n\n<br />\n\n`Netlifyビルドコマンド変更前コンソールのエラー抜粋：`\n\n```log\nwarn  - Statically exporting a Next.js application via `next export` disables API routes.\nThis command is meant for static-only hosts, and is not necessary to make your application static.\nPages in your application without server-side data dependencies will be automatically statically exported by `next build`, including pages powered by `getStaticProps`.\nLearn more: https://nextjs.org/docs/messages/api-routes-static-export\ninfo  - Exporting (0/3)\ninfo  - Copying \"public\" directory\nError occurred prerendering page \"/search\". Read more: https://nextjs.org/docs/messages/prerender-error\nError: Error for page /search: pages with `getServerSideProps` can not be exported. See more info here: https://nextjs.org/docs/messages/gssp-export\n    at /opt/build/repo/node_modules/next/dist/export/worker.js:227:27\n    at async Span.traceAsyncFn (/opt/build/repo/node_modules/next/dist/telemetry/trace/trace.js:60:20)\ninfo  - Exporting (3/3)\nError: Export encountered errors on following paths:\n\t/search\n    at /opt/build/repo/node_modules/next/dist/export/index.js:487:19\n    at async Span.traceAsyncFn (/opt/build/repo/node_modules/next/dist/telemetry/trace/trace.js:60:20)\n​\n────────────────────────────────────────────────────────────────\n  \"build.command\" failed                                        \n────────────────────────────────────────────────────────────────\n​\n  Error message\n  Command failed with exit code 1: npm run export\n​\n  Error location\n  In Build command from Netlify app:\n  npm run export\n​\n  Resolved config\n  build:\n    command: npm run export\n    commandOrigin: ui\n    environment:\n```\n\n<br />\n\n<strong><span style=\"color: red;\">Netlifyのプラグインのインストールが必要かと思いましたが、必要無かったです。SSG → SSG & SSR で行ったことは、ビルドコマンドの変更だけです。※netlify.tomlは使っていません。</span></strong>\n\n<br />\n\n## Next.js側の対応\nNetlifyにデプロイするにあたり、Next.js側の対応は何もありませんでした。  \n\n<br />\n\n`next.config.js`\n\n```javascript\nmodule.exports = {\n  ・・・\n  target: 'serverless',\n  ・・・\n};\n```\n\n<strong><span style=\"color: red;\">が必要なような記述を見つけて、追加してビルドしてみましたが、エラーになり、結果、必要無かったです。</span></strong>  \n\n<br />\n\n## npm installのエラー\n\n`react-static-tweets`がnext10系に依存しているのに対して、next11系をインストールしたため、エラーになりました。  \n\n```log\nInstalling NPM modules using NPM version 7.21.1\nnpm ERR! code ERESOLVE\nnpm ERR! ERESOLVE unable to resolve dependency tree\nnpm ERR!\nnpm ERR! While resolving: itc-blog@0.1.0\nnpm ERR! Found: next@11.1.2\nnpm ERR! node_modules/next\nnpm ERR!   next@\"^11.0.0\" from the root project\nnpm ERR!\nnpm ERR! Could not resolve dependency:\nnpm ERR! peer next@\"^10.0.6\" from react-static-tweets@0.5.4\nnpm ERR! node_modules/react-static-tweets\nnpm ERR!   react-static-tweets@\"0.5.4\" from the root project\nnpm ERR!\nnpm ERR! Fix the upstream dependency conflict, or retry\nnpm ERR! this command with --force, or --legacy-peer-deps\nnpm ERR! to accept an incorrect (and potentially broken) dependency resolution.\nnpm ERR!\nnpm ERR! See /opt/buildhome/.npm/eresolve-report.txt for a full report.\nnpm ERR! A complete log of this run can be found in:\nnpm ERR!     /opt/buildhome/.npm/_logs/2021-09-12T08_05_49_299Z-debug.log\nError during NPM install\n```\n\n<strong><span style=\"color: red;\">react-static-tweetsのpackage.jsonにパッチを当てたかったですが、package.jsonの内容が可変なのと、package.jsonが`patch-package`の対象に加わらず、断念しました。結局、Environment variablesに`NPM_FLAGS` = `--force`を設定して、無視するようにしました。</span></strong>  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/npmforce.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/npmforce.png\" alt=\"Next.js ブログ 検索機能 Netlify NPM_FLAGS\" width=\"670\" height=\"628\" loading=\"lazy\"></a>  \n\n<br />\n\n## Request must beエラー\nビルドが通ったと思って、喜んでいたら、最後の最後でエラーになりました。  \n「Request must be smaller than 69905067 bytes for the CreateFunction operation」  \nは、Netlifyがバックエンドに使っているAWS Lambdaのエラーのようです。  \n\n<br />\n\n<strong><span style=\"color: red;\">キャッシュをクリアしないで、もう一度ビルド（Deploy site）したら、エラーは無くなりました。</span></strong>  \n\n初めてSSRをデプロイするときに出るのかもしれません。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/deployerror.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/deployerror.png\" alt=\"Next.js ブログ 検索機能 Netlify SSRをデプロイエラー\" width=\"523\" height=\"161\" loading=\"lazy\"></a>  \n\n```log\n────────────────────────────────────────────────────────────────\n  5. onPostBuild command from @netlify/plugin-nextjs            \n────────────────────────────────────────────────────────────────\n​\nNext.js cache saved.\n\nFor faster deploy times, build IDs should be set to a static value.\nTo do this, set generateBuildId: () => 'build' in your next.config.js\n​\n(@netlify/plugin-nextjs onPostBuild completed in 292ms)\n​\n────────────────────────────────────────────────────────────────\n  6. Deploy site                                                \n────────────────────────────────────────────────────────────────\n​\nStarting to deploy site from 'out'\nCreating deploy tree \nCreating deploy upload records\n92 new files to upload\n6 new functions to upload\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\nRequest must be smaller than 69905067 bytes for the CreateFunction operation\n・・・\n```\n\n<br />\n\n## コンソール出力\n以下、デプロイがうまくいったときのコンソール画面です。search関連だけλ(Lambda)になっています。※最初の実装のときの出力です。その後の実装変更で、`/search`は、○  (Static)になりました。  \n\n```log\nPage                                       Size     First Load JS\n┌ ● / (9815 ms)                            263 B           122 kB\n├   /_app                                  0 B            79.9 kB\n├ ○ /404                                   195 B          80.1 kB\n├ λ /api/search                            0 B            79.9 kB\n├ ● /blogs/[id] (137237 ms)                292 kB          410 kB\n├   ├ /blogs/download-iframe (10615 ms)\n├   ├ /blogs/nextjs-fetch-proxy (9927 ms)\n├   ├ /blogs/nextjs-patch (9807 ms)\n├   ├ /blogs/sslcert (9558 ms)\n├   ├ /blogs/next-sitemap2 (9481 ms)\n├   ├ /blogs/proxies (8400 ms)\n├   ├ /blogs/next-sitemap (3571 ms)\n├   └ [+31 more paths] (avg 2448 ms)\n├ ● /list/[[...slug]] (115105 ms)          216 B           122 kB\n├   ├ /list/1/8hcq78trdqsy (4334 ms)\n├   ├ /list/1/068udcpeazn (4258 ms)\n├   ├ /list/1/umqsrvfrv7 (4177 ms)\n├   ├ /list/1/6ra37_nhnpqm (4068 ms)\n├   ├ /list/1/o8o4z1zp7 (4019 ms)\n├   ├ /list/1/91zw54wj7d (3841 ms)\n├   ├ /list/1/n767cc7fin (3765 ms)\n├   └ [+26 more paths] (avg 3332 ms)\n└ λ /search                                41 kB           144 kB\n+ First Load JS shared by all              79.9 kB\n  ├ chunks/framework.c179ed.js             42.4 kB\n  ├ chunks/main.2ca113.js                  23.2 kB\n  ├ chunks/pages/_app.d6e24f.js            13.5 kB\n  ├ chunks/webpack.0cb069.js               825 B\n  └ css/ae63bce8d69d04f7cb7c.css           7.91 kB\nλ  (Lambda)  server-side renders at runtime (uses getInitialProps or getServerSideProps)\n○  (Static)  automatically rendered as static HTML (uses no initial props)\n●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)\n   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)\n> itc-blog@0.1.0 postbuild /opt/build/repo\n> next-sitemap --config sitemap.config.js\n​\n(build.command completed in 1m 44.3s)\n​\n────────────────────────────────────────────────────────────────\n  3. onBuild command from @netlify/plugin-nextjs                \n────────────────────────────────────────────────────────────────\n​\nDetected Next.js site. Copying files...\n** Running Next on Netlify package **\n🚀 Next on Netlify 🚀\n🌍️ Copying public folder to /opt/build/repo/out\n💼 Copying static NextJS assets to /opt/build/repo/out\n💫 Setting up API endpoints as Netlify Functions in /opt/build/repo/.netlify/functions-internal\n💫 Setting up pages with getInitialProps as Netlify Functions in /opt/build/repo/.netlify/functions-internal\n💫 Setting up pages with getServerSideProps as Netlify Functions in /opt/build/repo/.netlify/functions-internal\n🔥 Copying pre-rendered pages with getStaticProps and JSON data to /opt/build/repo/out\n💫 Setting up pages with getStaticProps and fallback: true as Netlify Functions in /opt/build/repo/.netlify/functions-internal\n💫 Setting up pages with getStaticProps and revalidation interval as Netlify Functions in /opt/build/repo/.netlify/functions-internal\n🔥 Copying pre-rendered pages without props to /opt/build/repo/out\nBuilding 150 pages\n🔀 Setting up redirects\n🔀 Setting up headers\n✅ Success! All done!\n​\n(@netlify/plugin-nextjs onBuild completed in 375ms)\n​\n────────────────────────────────────────────────────────────────\n  4. Functions bundling                                         \n────────────────────────────────────────────────────────────────\n​\nPackaging Functions from .netlify/functions-internal directory:\n - next_api_search/next_api_search.js\n - next_blogs_id/next_blogs_id.js\n - next_image/next_image.js\n - next_index/next_index.js\n - next_list_slug/next_list_slug.js\n - next_search/next_search.js\n​\n​\n(Functions bundling completed in 35.4s)\n​\n────────────────────────────────────────────────────────────────\n  5. onPostBuild command from @netlify/plugin-nextjs            \n────────────────────────────────────────────────────────────────\n​\nNext.js cache saved.\n\nFor faster deploy times, build IDs should be set to a static value.\nTo do this, set generateBuildId: () => 'build' in your next.config.js\n​\n(@netlify/plugin-nextjs onPostBuild completed in 357ms)\n​\n────────────────────────────────────────────────────────────────\n  6. Deploy site                                                \n────────────────────────────────────────────────────────────────\n​\nStarting to deploy site from 'out'\nCreating deploy tree \nCreating deploy upload records\n87 new files to upload\n5 new functions to upload\nSite deploy was successfully initiated\n​\n(Deploy site completed in 5.9s)\n​\n────────────────────────────────────────────────────────────────\n  Netlify Build Complete                                        \n────────────────────────────────────────────────────────────────\n​\n(Netlify Build completed in 2m 29s)\nStarting post processing\nPost processing - HTML\nCaching artifacts\nStarted saving node modules\nFinished saving node modules\nStarted saving build plugins\nFinished saving build plugins\nStarted saving pip cache\nFinished saving pip cache\nStarted saving emacs cask dependencies\nFinished saving emacs cask dependencies\nStarted saving maven dependencies\nFinished saving maven dependencies\nStarted saving boot dependencies\nFinished saving boot dependencies\nStarted saving rust rustup cache\nFinished saving rust rustup cache\nStarted saving go dependencies\nFinished saving go dependencies\nBuild script success\nPost processing - header rules\nPost processing - redirect rules\nPost processing done\nSite is live ✨\nFinished processing build request in 3m19.77232733s\n```\n","description":"Next.jsで作られているこのブログにSSR(Server Side Rendering)で検索機能を実装しました。どのように実装したか、何がひっかかったか、から、果てはNetlifyへのデプロイまで書きました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"3cgjizvk62nx","createdAt":"2021-10-04T13:50:36.279Z","updatedAt":"2021-10-04T13:50:36.279Z","publishedAt":"2021-10-04T13:50:36.279Z","revisedAt":"2021-10-04T13:50:36.279Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-blog-search/ITC_Engineering_Blog.png","alt":"Next.JSのブログにSSRの検索機能を実装してNetlifyにデプロイ","width":1200,"height":630}],"seo_authors":[]},{"id":"ejbca-softhsm2","createdAt":"2024-02-13T13:58:57.562Z","updatedAt":"2024-02-18T09:45:43.109Z","publishedAt":"2024-02-13T13:58:57.562Z","revisedAt":"2024-02-18T09:45:43.109Z","title":"EJBCAからPKCS#11でSoftHSM2にキーペアを登録してみた","category":{"id":"ik0y39076","createdAt":"2024-02-04T12:20:33.135Z","updatedAt":"2024-02-04T12:20:33.135Z","publishedAt":"2024-02-04T12:20:33.135Z","revisedAt":"2024-02-04T12:20:33.135Z","topics":"Java","logo":"/logos/Java.png","needs_title":false},"topics":[{"id":"ik0y39076","createdAt":"2024-02-04T12:20:33.135Z","updatedAt":"2024-02-04T12:20:33.135Z","publishedAt":"2024-02-04T12:20:33.135Z","revisedAt":"2024-02-04T12:20:33.135Z","topics":"Java","logo":"/logos/Java.png","needs_title":false},{"id":"91zw54wj7d","createdAt":"2021-06-05T07:05:37.594Z","updatedAt":"2021-08-31T12:03:57.429Z","publishedAt":"2021-06-05T07:05:37.594Z","revisedAt":"2021-08-31T12:03:57.429Z","topics":"Python","logo":"/logos/python.png","needs_title":false},{"id":"umqsrvfrv7","createdAt":"2021-08-29T10:56:17.442Z","updatedAt":"2021-08-31T12:02:21.915Z","publishedAt":"2021-08-29T10:56:17.442Z","revisedAt":"2021-08-31T12:02:21.915Z","topics":"Unix/Linux","logo":"/logos/Linux.png","needs_title":true},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\n\n<a href=\"https://itc-engineering-blog.netlify.app/blogs/ejbca-ca-certificate\" target=\"_blank\">前回記事</a>では、EJBCA Community で TLS 証明書発行をやってみました。そのとき、秘密鍵は、HSM（Hardware Security Module）を使わずに EJBCA 自身で保存するようにしました。  \nEJBCA 自身で保存するトークンのことを `ソフト トークン`(`soft token`) と呼んでいるようですが、今回は、`ハード トークン`(`hard token`) すなわち、キーペア（公開鍵/秘密鍵）を HSM に保存するということをやってみます。\n\n<br />\n\nということで、有名そうな HSM の一つ Thales Luna を Ama●on でポチっと...\n\n<br />\n\n...というふうに気軽に入手できる代物ではありませんでした！たぶん、マジモンの場合、クルマ買えるくらいの見積書が送られてきます。\n\n<br />\n\n無料の <a href=\"https://github.com/opendnssec/SoftHSMv2\" target=\"_blank\">opendnssec/SoftHSMv2</a> を使います。  \n今回、softhsm2 インストールから EJBCA からのキーペア登録、キーペアの移行について試みていきます。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image1.png\" alt=\"EJBCA - SoftHSMv2 図\" width=\"681\" height=\"471\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>SoftHSMv2 は、オープンソースのソフトウェアで、ソフトウェア的に HSM を模倣しているだけで、HSM の代替になるものではありません。</strong></span></p>\n<p><span style=\"color: red;\"><strong>つまり、「本物の HSM 高いし、予算無いから、SoftHSM でいいや。」と運用を始めるとセキュリティ的に大変危険です。</strong></span></p>\n<p>SoftHSMv2 は、HSM への投資をためらっている開発者のために、HSM のようなことができるテスト用のツールという位置付けです。</p>\n<p>SoftHSMv2 自身の自己紹介文でもそう説明しています。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 PKCS #11 】</p>\n<p>PKCS #11（Public-Key Cryptography Standard #11）は、RSA Security によって策定された暗号化情報を保持し、暗号化機能を実行する暗号化デバイスへのアプリケーションプログラミングインターフェース（API）を定義した規格です。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/ejbca-build-install\" target=\"_blank\">EJBCA(PKI および証明書管理アプリ)をビルドしてインストールしてみた</a>」で作成した以下の環境です。少しでも環境が異なると、途中で詰まるかもしれません。</p>\n<p>Ubuntu Desktop 22.04.3 LTS</p>\n<p>　 EJBCA 8.2.0.1 Community</p>\n<p>　 openjdk version \"11.0.21\" 2023-10-17</p>\n<p>　 wildfly 26.0.0.Final</p>\n<p>　 mysql Ver 15.1 Distrib 10.6.16-MariaDB</p>\n<p>　 Apache Ant(TM) version 1.10.12</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。</strong></span></p>\n</blockquote>\n\n<br />\n\n# softhsm2 インストール\n\nまずは、softhsm2 をインストールします。\n\n```shellsession\n# apt update -y\n# apt install softhsm2 -y\n取得:1 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 softhsm2-common amd64 2.6.1-2ubuntu1 [7,168 B]\n取得:2 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 libsofthsm2 amd64 2.6.1-2ubuntu1 [268 kB]\n取得:3 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 softhsm2 amd64 2.6.1-2ubuntu1 [177 kB]\n453 kB を 3秒 で取得しました (144 kB/s)\n以前に未選択のパッケージ softhsm2-common を選択しています。\n(データベースを読み込んでいます ... 現在 210742 個のファイルとディレクトリがインストールされています。)\n.../softhsm2-common_2.6.1-2ubuntu1_amd64.deb を展開する準備をしています ...\nsofthsm2-common (2.6.1-2ubuntu1) を展開しています...\n以前に未選択のパッケージ libsofthsm2 を選択しています。\n.../libsofthsm2_2.6.1-2ubuntu1_amd64.deb を展開する準備をしています ...\nlibsofthsm2 (2.6.1-2ubuntu1) を展開しています...\n以前に未選択のパッケージ softhsm2 を選択しています。\n.../softhsm2_2.6.1-2ubuntu1_amd64.deb を展開する準備をしています ...\nsofthsm2 (2.6.1-2ubuntu1) を展開しています...\nsofthsm2-common (2.6.1-2ubuntu1) を設定しています ...\n```\n\n<br />\n\nパーミッションを確認します。\n\n```shellsession\n# ls -ld /var/lib/softhsm\ndrwxrws--- 3 root softhsm 4096  2月 11 21:24 /var/lib/softhsm\n```\n\n<blockquote class=\"info\">\n<p><code>drwxrws---</code> の <code>s</code> は、セット GID（Set Group ID）です。</p>\n<p><code>drwxrws---</code> のパーミッションが設定されたディレクトリでは、そのディレクトリ内で作成される全ての新しいファイルやディレクトリは、元のディレクトリと同じグループ ID を持ちます。</p>\n</blockquote>\n\n<br />\n\nこれにより、softhsm 関連のグループ名が `softhsm` グループということが分かりますので、EJBCA を実行しているユーザー権限 `wildfly` を `softhsm` グループに追加します。\n\n<blockquote class=\"info\">\n<p><code>ods</code> グループのときもあるようです。</p>\n</blockquote>\n\n```shellsession\n# usermod -aG softhsm wildfly\n```\n\n<br />\n\n# softhsm2 初期化\n\nトークンを ラベル＝`slot1` で初期化します。  \nここで、SO と User の PIN（パスワード）を入力します。\n\n```shellsession\n# softhsm2-util --init-token --free --label slot1\nSlot 0 has a free/uninitialized token.\n=== SO PIN (4-255 characters) ===\nPlease enter SO PIN: ********\nPlease reenter SO PIN: ********\n=== User PIN (4-255 characters) ===\nPlease enter user PIN: ********\nPlease reenter user PIN: ********\nThe token has been initialized and is reassigned to slot 661497608\n```\n\n<blockquote class=\"info\">\n<p>【 softhsm2-util 】</p>\n<p><code>--init-token</code>：トークン初期化プロセスを開始します。このオプションが指定されていない場合、トークンは初期化されません。</p>\n<p><code>--free</code>：最初の空きスロット（未初期化トークン）を使用してトークンを初期化します。このオプションを指定しない場合、スロット番号を明示的に指定する必要があります。</p>\n<p><code>--label</code>：トークンラベルを設定します。ラベルは、トークンを識別するために使用されます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 スロット 】</p>\n<p>HSM（ハードウェアセキュリティモジュール）の用語である「スロット」は、一般的には「引き出し」や「ロッカー」に例えられます。</p>\n<p>この「引き出し」や「ロッカー」は、それぞれ独立した空間で、各々が異なる「トークン」（鍵の保管庫）を保持します。</p>\n<p>具体的には、HSM のスロットは以下のような機能を持っています：</p>\n<p>・トークン（鍵の保管庫）を保持する</p>\n<p>・トークンを初期化する</p>\n<p>・トークンを再初期化する</p>\n<p>これらの機能により、各スロットは独立したトークンを管理し、それぞれのトークンに対する操作（初期化、再初期化など）を行うことができます。</p>\n<p>1 つの HSM デバイスに複数のスロットが存在することがあります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 トークン 】</p>\n<p>HSM（ハードウェアセキュリティモジュール）の用語である「トークン」は、一般的には「鍵の保管庫」や「金庫」に例えられます。</p>\n<p>この「金庫」は、暗号化や電子署名に利用する鍵を安全に保管し、管理する役割を果たします。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 SO と User 】</p>\n<p>Security Officer (SO)は、セキュリティトークンの管理者（セキュリティ担当者）で、トークンの初期化や再初期化などの管理操作を行う役割を持っています。</p>\n<p>一方、ユーザーは、SO が初期化したトークンを使用して、秘密鍵や公開鍵などの暗号キーを生成、保存、使用することができます。</p>\n<p>具体的には、SO は以下のような権限を持っています：</p>\n<p>・トークンの初期化</p>\n<p>・ユーザー PIN のリセット</p>\n<p>・トークンの再初期化</p>\n<p>一方、ユーザーは以下のような権限を持っています：</p>\n<p>・暗号キーの生成と使用</p>\n<p>・PIN の変更</p>\n<p>したがって、SO とユーザーは異なる役割と権限を持っており、SO はトークンの管理者であり、ユーザーはトークンを使用する者という位置付けになります。</p>\n<p>このように、SO とユーザーの役割を分けることで、セキュリティの確保と操作の柔軟性が向上します。</p>\n</blockquote>\n\n<br />\n\n利用可能なスロット情報を表示します。\n\n```shellsession\n# softhsm2-util --show-slots\nAvailable slots:\nSlot 661497608\n    Slot info:\n        Description:      SoftHSM slot ID 0x276da708\n        Manufacturer ID:  SoftHSM project\n        Hardware version: 2.6\n        Firmware version: 2.6\n        Token present:    yes\n    Token info:\n        Manufacturer ID:  SoftHSM project\n        Model:            SoftHSM v2\n        Hardware version: 2.6\n        Firmware version: 2.6\n        Serial number:    c630b01f276da708\n        Initialized:      yes\n        User PIN init.:   yes\n        Label:            slot1\nSlot 1\n    Slot info:\n        Description:      SoftHSM slot ID 0x1\n        Manufacturer ID:  SoftHSM project\n        Hardware version: 2.6\n        Firmware version: 2.6\n        Token present:    yes\n    Token info:\n        Manufacturer ID:  SoftHSM project\n        Model:            SoftHSM v2\n        Hardware version: 2.6\n        Firmware version: 2.6\n        Serial number:\n        Initialized:      no\n        User PIN init.:   no\n        Label:\n```\n\nLabel: slot1 が登録されました！\n\n<blockquote class=\"info\">\n<p>スロットの ID は自動的に振られます。</p>\n</blockquote>\n\n<br />\n\n/var/lib/softhsm/tokens/ 配下にトークンが作成されています。\n\n```shellsession\n# find /var/lib/softhsm/tokens/ -ls\n 23462679      4 drwxrws---   3 root     softhsm      4096  2月 11 21:25 /var/lib/softhsm/tokens/\n 23462645      4 drwx--S---   2 root     softhsm      4096  2月 11 21:25 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708\n 23462683      4 -rw-------   1 root     softhsm         8  2月 11 21:32 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708/generation\n 23462682      0 -rw-------   1 root     softhsm         0  2月 11 21:30 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708/token.lock\n 23462647      4 -rw-------   1 root     softhsm       320  2月 11 21:30 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708/token.object\n```\n\n<blockquote class=\"info\">\n<p><code>drwx--S---</code> の <code>S</code> は、SUID (Set User ID) ビットが設定されていることを意味します。</p>\n<p>SUID ビット は、ファイル所有者の権限でプログラムを実行できる特別な権限です。つまり、通常は実行権限がないユーザーでも、SUID ビットが設定された（ディレクトリの中の）プログラムを実行すれば、ファイル所有者と同じ権限で実行することができます。</p>\n</blockquote>\n\n<br />\n\nroot にしか参照書き込み権がないため、EJBCA 実行ユーザーである wildfly を owner にします。\n\n```shellsession\n# chown -R wildfly /var/lib/softhsm/tokens\n```\n\n<br />\n\n# キーをトークンに追加\n\nEJBCA を使用して、キーをトークンに追加します。\n\n<br />\n\nまずは、wildfly を再起動します。\n\n```shellsession\n# systemctl restart wildfly\n```\n\n<blockquote class=\"alert\">\n<p>どハマり注意：これをしないと、スロットが認識されません！</p>\n</blockquote>\n\n<br />\n\nEJBCA Admin UI から、**CA Functions** - **Crypto Tokens** にアクセスして、**Create new...** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image2.png\" alt=\"CA Functions - Crypto Tokens - Create new\" width=\"1163\" height=\"378\" loading=\"lazy\"></a>\n\n<br />\n\n**Name** に 任意の 暗号トークン名を入力します。ここでは、 `SoftHSM2Token` とします。  \n**Type** のところを `PKCS#11` に変更します。  \n**PKCS#11 : Library** のところが `SoftHSM 2` になっていることを確認して、  \n**PKCS#11 : Reference Type** を `Slot ID` とします。  \n**PKCS#11 : Reference** に スロット ID すなわち、  \n先ほど、`softhsm2-util --show-slots` で確認した、  \n`661497608` を入力します。  \n**Authentication Code** は、この EJBCA の Crypto Token をアクティベートするパスワードを入力します。（任意の文字列です。PIN ではありません。）  \n入力したら、**Save** をクリックします。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image3.png\" alt=\"Crypto Token Save\" width=\"864\" height=\"438\" loading=\"lazy\"></a>\n\n<br />\n\n**PKCS#11 : Reference Type** について、`Slot/Token Label` を選択すると、自動的に見つかった `slot1` が表示されるため、それでも良いです。  \n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image4.png\" alt=\"Slot/Token Label\" width=\"551\" height=\"99\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image5.png\" alt=\"Crypto Token が登録されました\" width=\"802\" height=\"484\" loading=\"lazy\"></a>\n\nCrypto Token が登録されました！（まだ鍵を登録したわけではありません。）\n\n<br />\n\n続いて、キーペアを作成します。\n\n<br />\n\n`signKey` と入力されているところの **Generate new key pair** ボタンをクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image6.png\" alt=\"Generate new key pair\" width=\"802\" height=\"73\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><code>signKey</code> は、Alias で PKCS #11 属性で言うところの ID になります。任意の名前を付けられます。</p>\n<p><code>RSA 4096</code> と表示されているところは、アルゴリズム選択で、いろいろな種類の ECDSA（楕円曲線暗号）から選択できたり、ポスト量子暗号を選べたり、いろいろあります。</p>\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image7.png\" alt=\"アルゴリズム選択\" width=\"315\" height=\"278\" loading=\"lazy\"></a>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image8.png\" alt=\"キーペア作成後\" width=\"898\" height=\"487\" loading=\"lazy\"></a>\n\n登録されました！\n\n<br />\n\n登録に失敗したときは、以下のようにエラー表示されます。  \n<span style=\"background-color: cornsilk; color: red;\">Error: Error when creating Crypto Token with ID -1759357103.</span>  \n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image9.png\" alt=\"Crypto Token登録エラー\" width=\"608\" height=\"145\" loading=\"lazy\"></a>\n\n<br />\n\nエラーケース別に  \n/opt/wildfly-26.0.0.Final/standalone/log/server.log  \nに出力されたログは、以下です。\n\n<br />\n\n`再起動していない`：  \n<span style=\"background-color: cornsilk; color: red;\">2024-02-11 11:38:06,957 ERROR [com.keyfactor.util.keys.token.pkcs11.SunP11SlotListWrapper] (default task-4) Wrong arguments were passed to sun.security.pkcs11.wrapper.PKCS11.CK_C_INITIALIZE_ARGS.getInstance threw an exception for log.error(msg, e): java.lang.reflect.InvocationTargetException</span>  \n<span style=\"background-color: cornsilk; color: red;\">（...略...）</span>  \n<span style=\"background-color: cornsilk; color: red;\">Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_GENERAL_ERROR</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.C_Initialize(Native Method)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11$SynchronizedPKCS11.C_Initialize(PKCS11.java:1631)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.getInstance(PKCS11.java:166)</span>  \n\n<br />\n\n`/var/lib/softhsm/tokens パーミッションエラー`：  \n<span style=\"background-color: cornsilk; color: red;\">2024-02-11 11:47:58,014 ERROR [com.keyfactor.util.keys.token.pkcs11.Pkcs11SlotLabel] (default task-1) Error constructing pkcs11 provider: null: java.lang.reflect.InvocationTargetException</span>  \n<span style=\"background-color: cornsilk; color: red;\">（...略...）</span>  \n<span style=\"background-color: cornsilk; color: red;\">Caused by: java.security.ProviderException: Initialization failed</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.<init>(SunPKCS11.java:396)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11$1.run(SunPKCS11.java:116)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11$1.run(SunPKCS11.java:113)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at java.base/java.security.AccessController.doPrivileged(Native Method)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.configure(SunPKCS11.java:113)</span>  \n<span style=\"background-color: cornsilk; color: red;\">... 167 more</span>  \n<span style=\"background-color: cornsilk; color: red;\">Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_GENERAL_ERROR</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.C_GetTokenInfo(Native Method)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.Token.<init>(Token.java:135)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.initToken(SunPKCS11.java:1006)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.<init>(SunPKCS11.java:387)</span>  \n\n<br />\n\n`スロット ID 間違い`：  \n<span style=\"background-color: cornsilk; color: red;\">Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_SLOT_ID_INVALID</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.C_GetSlotInfo(Native Method)</span>  \n<span style=\"background-color: cornsilk; color: red;\">at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.<init>(SunPKCS11.java:385)</span>  \n\n<br />\n\n# キー確認\n\nEJBCA の Client Toolbox を使用して、SoftHSM の slot1 というラベルのトークンに格納されているキーをテストします。  \nここで、ユーザー PIN の入力が必要です。\n\n```shellsession\n# cd /opt/ejbca/dist/clientToolBox/\n# ./ejbcaClientToolBox.sh PKCS11HSMKeyTool test /usr/lib/softhsm/libsofthsm2.so TOKEN_LABEL:slot1\n```\n\n<blockquote class=\"info\">\n<p><code>./ejbcaClientToolBox.sh PKCS11HSMKeyTool test</code>：EJBCA の Client Toolbox の一部である PKCS11HSMKeyTool を使用して、HSM 上のキーをテストします。</p>\n<p><code>/usr/lib/softhsm/libsofthsm2.so</code>：SoftHSM の PKCS#11 ライブラリへのパスを指定します。このライブラリは、HSM とのインターフェースを提供します。</p>\n<p><code>TOKEN_LABEL:slot1</code>：テストするトークンのラベルを指定します。この例では、slot1 というラベルのトークンをテストします。</p>\n</blockquote>\n\n<br />\n\n```shellsession\nTesting of key: signKey\nPrivate part:\nSunPKCS11-libsofthsm2.so-slot661497608 RSA private key, 4096 bitstoken object, sensitive, unextractable)\nRSA key:\n  modulus: adb65f0e17d306edc3e2e2bbc92dcc200559100b7dbe4ad380c51d99290399a411804d0742009b4a2baf5e2f97483e66fb4e658b61ac55bc61ce97ef3c085b732e0a6048ceedb2ea4abdc4824d28e3b947c32308690590ff8f3eb1b3806b7431c45954506198431a9fcbb38755fad1f1e358010a7131e2ceddcaecf08ec6376b720d10c5726c3f8c0fc953c732f1c20fd8f231a143b615908c8b7adaf294fc36e41a5b201560a519457093fe9e9d941b168a0f62859fcad7b85bbe97e17f443587ed10efdb989d00fb929ce9bc1995cad7754b61e4134bf794efe7adbe4c2848688a61404bf5360d74f66d4aea9ee8d959731bdbbce0bac4378966d0567cea50a76351129ccfd817dce7003f3e08a550c057b7517fdff641995356482528174b7999b34cb4d4275ef0839517c9a6a1cbe31115d5825e0b103ddcfb051a6e4b4e5b90fc7dabad257f80bc4b48ca768fac580409ce6ef8ea1475659c20ff7d4eff1bcc426dee71215c5009aba8c5a06a8f26d2379f77eec9d5cf34fe9028335a6233c71156f725ce14a58a306105a54da763fbe18da1c01e42833a7a7f50803addd74f9ff3f3559a669e952027ddf5121ce7290691e374908fea625becf71dcdf945a0bd1a319153f44875add88e8badffedcb6d16994d72f3ef9dd2c6f11ffbd9d4a6e42d851a2dbcdae6e271f55a2b1891b828ee76777e22f0e2344b6468eac7\n  public exponent: 10001\nencryption provider: SunJCE version 11; decryption provider: SunPKCS11-libsofthsm2.so-slot661497608 version 11; modulus length: 4096; byte length 501. The decoded byte string is equal to the original!\n2024-02-13 17:20:20,017 INFO  [com.keyfactor.util.keys.SignWithWorkingAlgorithm] Signature algorithm 'SHA1WithRSA' working for provider 'SunPKCS11-libsofthsm2.so-slot661497608 version 11'.\nSignature test of key signKey: signature length 512; first byte a7; verifying true\nSignings per second: 93\nDecryptions per second: 96\n```\n\nテストに成功し、以下の結果が示されています。  \n\n<br />\n\n**`SunPKCS11-libsofthsm2.so-slot661497608 RSA private key, 4096 bitstoken object, sensitive, unextractable)`**：  \n秘密鍵のテスト：signKey というラベルの RSA 秘密鍵がテストされ、その鍵は 4096 ビットの長さで、トークンオブジェクトであり、センシティブ（sensitive）で、抽出不可能（unextractable）です。\n\n<br />\n\n**`RSA key:`**：  \n**`  modulus: （略）`**：  \n**`  public exponent: 10001`**：  \n**`encryption provider: SunJCE（略）`**：  \nRSA 鍵の詳細：モジュラス（modulus）と公開指数（public exponent）が表示されています。また、暗号化プロバイダ（encryption provider）は SunJCE version 11 で、復号化プロバイダ（decryption provider）は SunPKCS11-libsofthsm2.so-slot661497608 version 11 です。モジュラスの長さは 4096 ビットで、バイト長は 501 です。元のバイト文字列とデコードされたバイト文字列が等しいことが確認されています。\n\n<br />\n\n**`Signature algorithm 'SHA1WithRSA' working for provider 'SunPKCS11-libsofthsm2.so-slot661497608 version 11'.`**：  \n署名アルゴリズムのテスト：署名アルゴリズム SHA1WithRSA がプロバイダ SunPKCS11-libsofthsm2.so-slot661497608 version 11 で動作していることが確認されています。\n\n<br />\n\n**`Signature test of key signKey: signature length 512; first byte a7; verifying true`**：  \n署名テスト：signKey の署名テストが行われ、署名の長さは 512 で、最初のバイトは a7、そして検証は真（true）です。\n\n<br />\n\n**`Signings per second: 93`**：  \n**`Decryptions per second: 96`**：  \n性能テスト：1 秒あたりの署名数（Signings per second）は 93 で、1 秒あたりの復号化数（Decryptions per second）は 96 です。\n\n<blockquote class=\"info\">\n<p>【 モジュラス（modulus） 】</p>\n<p>RSA 暗号では、大きな 2 つの素数 p と q を生成し、それらの積 n (= pq)を求めます。この n をモジュラスと呼びます。モジュラスは、公開鍵と秘密鍵の両方で使用され、その長さ（ビット数）が RSA 鍵の強度を決定します。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 公開指数（public exponent）】</p>\n<p>公開指数 e は、平文の暗号化に使用される指数で、RSA 鍵を作成するために用いられます。公開指数は公開鍵の一部であり、通常は小さい値（一般的には 65537）が選ばれます。これは、小さい値を選ぶことで、暗号化処理を高速化できるからです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 SunJCE 】</p>\n<p>SunJCE（Sun Java Cryptography Extension）は、Java のセキュリティーフレームワークの一部で、暗号化、公開鍵インフラストラクチャー、認証、安全な通信、アクセス制御などの主要なセキュリティー分野にわたる一連の API を提供します。</p>\n<p>SunJCE は、Java Development Kit (JDK)の一部として提供されており、さまざまな暗号化アルゴリズムとサービスを実装しています。</p>\n<p>これにより、開発者はアプリケーションコードにセキュリティー機構を簡単に統合できます。</p>\n<p>また、SunJCE は PKCS#11（暗号トークンインタフェース標準）をサポートしており、ハードウェア暗号化アクセラレータやスマートカードなどの暗号化機構に対するネイティブプログラミングインタフェースを提供しています。</p>\n<p>適切に構成すると、アプリケーションは標準の JCA/JCE API を使用してネイティブ PKCS#11 ライブラリにアクセスできるようになります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 JCA/JCE API 】</p>\n<p>JCA/JCE API は、Java プログラムで暗号化機能を利用するための標準的な API です。</p>\n<p>Java Cryptography Architecture (JCA)：デジタル署名、メッセージダイジェスト、証明書、暗号化、鍵管理など暗号化サービスを提供します。</p>\n<p>Java Cryptography Extensions (JCE)：より強力な暗号化アルゴリズムや新しい暗号化スキームを Java プラットフォームに追加するための拡張パッケージです。</p>\n</blockquote>\n\n<br />\n\nここで、以下のエラーになった場合、ユーザー PIN が間違っている可能性があります。  \n<span style=\"background-color: cornsilk; color: red;\">2024-02-04 17:10:35,580 ERROR [org.ejbca.ui.cli.KeyStoreContainerTest] Not possible to load keys.</span>  \n<span style=\"background-color: cornsilk; color: red;\">java.security.KeyStoreException: KeyStore instantiation failed</span>  \n<span style=\"background-color: cornsilk; color: red;\">（...略...）</span>  \n<span style=\"background-color: cornsilk; color: red;\">at java.security.KeyStore$Builder$2.getKeyStore(KeyStore.java:2237) ~[?:?]</span>  \n<span style=\"background-color: cornsilk; color: red;\">        ... 11 more</span>  \n<span style=\"background-color: cornsilk; color: red;\">Caused by: javax.security.auth.login.FailedLoginException</span>  \n<span style=\"background-color: cornsilk; color: red;\">（...略...）</span>  \n<span style=\"background-color: cornsilk; color: red;\">        at java.security.KeyStore$Builder$2.getKeyStore(KeyStore.java:2237) ~[?:?]</span>  \n<span style=\"background-color: cornsilk; color: red;\">        ... 11 more</span>  \n<span style=\"background-color: cornsilk; color: red;\">Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_PIN_INCORRECT</span>  \n<span style=\"background-color: cornsilk; color: red;\">（...略...）</span>  \n<span style=\"background-color: cornsilk; color: red;\">        at java.security.KeyStore$Builder$2.getKeyStore(KeyStore.java:2237) ~[?:?]</span>  \n<span style=\"background-color: cornsilk; color: red;\">... 11 more</span>  \n<span style=\"background-color: cornsilk; color: red;\">Not possible to load keys. Maybe a smart card should be inserted or maybe you just typed the wrong PIN. Press enter when the problem is fixed or 'x' enter to quit.</span>  \n\n<br />\n\npkcs11-tool をインストールして、トークン上のすべてのオブジェクトの情報を表示します。\n\n```shellsession\n# apt install opensc -y\n# pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --token-label slot1 --pin foo123 -O\nCertificate Object; type = X.509 cert\nlabel: signKey\nsubject: DN: CN=Dummy certificate created by a CESeCore application\nID: 7369676e4b6579\nPrivate Key Object; RSA\nlabel:\nID: 7369676e4b6579\nUsage: decrypt, sign, unwrap\nAccess: sensitive, always sensitive, never extractable, local\n```\n\n<blockquote class=\"info\">\n<p>PKCS#11 準拠の HSM（Hardware Security Module）上のオブジェクトをリストアップしています。</p>\n<p><code>--module /usr/lib/softhsm/libsofthsm2.so</code>：SoftHSM の PKCS#11 ライブラリへのパスを指定します。このライブラリは、HSM とのインターフェースを提供します。</p>\n<p><code>--token-label slot1</code>：操作対象のトークンのラベルを指定します。この例では、<code>slot1</code> というラベルのトークンが操作対象となります。</p>\n<p><code>--pin foo123</code>：トークンにアクセスするための PIN を指定します。この例では、<code>foo123</code> が PIN として使用されています。</p>\n<p><code>-O</code>：トークン内のすべてのオブジェクトをリストアップします。</p>\n</blockquote>\n\n<br />\n\nここで、以下のエラーになった場合、ユーザー PIN（`foo123` 部分）が間違っている可能性があります。  \n<span style=\"background-color: cornsilk; color: red;\">error: PKCS11 function C_Login failed: rv = CKR_PIN_INCORRECT (0xa0)</span>  \n<span style=\"background-color: cornsilk; color: red;\">Aborting.</span>  \n\n<br />\n\n該当するスロットが無い場合、以下のエラーです。  \n<span style=\"background-color: cornsilk; color: red;\">No slot with token named \"slotx\" found</span>  \n\n<br />\n\n# キー移行\n\nSoftHSM2 に登録したキーを別のサーバーの EJBCA に読み込ませようと思います。...簡単にできたら、すぐに盗まれてしまいますので、SoftHSM2 で登録された秘密鍵は、例えば DER や PEM などにしてエクスポートできないようになっているようです。\n\n<br />\n\nしかし、エクスポートはできなくても移行は簡単にできました。\n\n<blockquote class=\"warn\">\n<p>これは、SoftHSM だからなせるわざです。通常の HSM でこんなことはできません。</p>\n</blockquote>\n\n<br />\n\n/var/lib/softhsm/tokens/* をコピーするだけです。\n\n<br />\n\ntar で固めて、移行先のサーバーに転送します。\n\n```shellsession\n# tar czf t.tar.gz /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708\n# scp t.tar.gz admin@xxx.xxx.xxx.xxx:~/.\n```\n\n<br />\n\n転送先のサーバーでは、softhsm2 インストール済みとします。\nそこへ転送されてきた tar.gz を展開します。\n\n```shellsession\n# tar zxf t.tar.gz -C /\n# systemctl restart wildfly\n```\n\n<br />\n\nEJBCA Admin UI から、**CA Functions** - **Crypto Tokens** にアクセスして、\n**Create new...** をクリックします。\n\n<br />\n\nスロット ID を指定して、Crypto Token を登録します。\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image3.png\" alt=\"Crypto Token Save（移行先）\" width=\"864\" height=\"438\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/image8.png\" alt=\"キーペア復元確認（移行先）\" width=\"898\" height=\"487\" loading=\"lazy\"></a>\n\n<br />\n\nキーが最初からあります！\n\n<br />\n\nところで、先ほど、「DER や PEM などにしてエクスポートできない」としましたが、エクスポートする方法を見つけました。  \n（参考：`https://github.com/opendnssec/SoftHSMv2/issues/597` の一番下です。）  \nしかし、<span style=\"color: red;\">EJBCA Admin UI CA Functions - Crypto Tokens で登録したキー（Private key/秘密鍵）のことではありません。</span>  \n自分で登録する場合、取り出せました。  \n\n<br />\n\nPKCS#11 属性に EXTRACTABLE という属性があるのですが、これが `True` の場合、取り出せます。  \nEJBCA で登録した場合、EXTRACTABLE = `False` でした。  \nそれを確認する Python プログラムが以下です。  \n（ついでに他の属性値も確認しています。）\n\n```python:check_pkcs11_attrs.py\n#!/usr/bin/python3\nimport pkcs11  # PKCS#11インターフェースを提供するモジュールをインポートします。\nfrom pkcs11.constants import Attribute, ObjectClass  # PKCS#11の定数をインポートします。\n\nMODULE_PATH = \"/usr/lib/softhsm/libsofthsm2.so\"  # SoftHSM2のライブラリへのパス\nTOKEN_LABEL = \"slot1\"  # トークンのラベル\nPIN = \"********\"  # PINコード\n\n\ndef get_all_keys(session):\n    # セッション内のすべてのオブジェクトを取得\n    all_objects = list(session.get_objects())\n\n    # オブジェクトの中からキーだけを抽出\n    keys = [\n        obj\n        for obj in all_objects\n        if obj[Attribute.CLASS]\n        in (ObjectClass.PRIVATE_KEY, ObjectClass.PUBLIC_KEY, ObjectClass.SECRET_KEY)\n    ]\n\n    return keys\n\n\ndef check_key_extractable(session):\n    keys = get_all_keys(session)\n\n    # キーペアが存在しない場合、エラーメッセージを表示\n    if not keys:\n        print(f\"No keys found\")\n        return\n\n    # 最初のキー（通常は秘密鍵）を取得\n    key = keys[0]\n\n    # CLASS, KEY_TYPE, ID, SENSITIVE, EXTRACTABLE 属性値取り出し\n    try:\n        key_val = key[Attribute.CLASS]\n        print(f\"CLASS: {key_val}\")\n    except AttributeError:\n        print(\"CLASS: Attribute not available\")\n    try:\n        key_val = key[Attribute.KEY_TYPE]\n        print(f\"KEY_TYPE: {key_val}\")\n    except AttributeError:\n        print(\"KEY_TYPE: Attribute not available\")\n    try:\n        key_val = key[Attribute.ID]\n        key_id_str = key_val.decode(\"utf-8\")  # Decode the bytes to a string\n        print(f\"ID: {key_id_str}\")\n    except AttributeError:\n        print(\"ID: Attribute not available\")\n    try:\n        key_val = key[Attribute.SENSITIVE]\n        print(f\"SENSITIVE: {key_val}\")\n    except AttributeError:\n        print(\"SENSITIVE: Attribute not available\")\n    try:\n        key_val = key[Attribute.EXTRACTABLE]\n        print(f\"EXTRACTABLE: {key_val}\")\n    except AttributeError:\n        print(\"EXTRACTABLE: Attribute not available\")\n\n    # キーがエクスポート可能かどうかを確認\n    if key[Attribute.EXTRACTABLE]:\n        print(\"Key is extractable\")\n    else:\n        print(\"Key is not extractable\")\n\n\ndef main():\n    lib = pkcs11.lib(MODULE_PATH)  # PKCS#11ライブラリをロード\n    token = lib.get_token(token_label=TOKEN_LABEL)  # トークンを取得\n\n    with token.open(user_pin=PIN, rw=True) as session:  # トークンを開く\n        check_key_extractable(session)\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n<br />\n\n`pip3` 他、依存モジュールもインストールして、実行します。（注意：この記事の最後に実行する Python プログラムに依存しているものもインストールしています。）\n\n```shellsession\n# apt update -y\n# apt install python3-pip -y\n# pip3 install python-pkcs11\n# pip3 install pycryptodome\n# python3 check_pkcs11_attrs.py\nCLASS: 3\nKEY_TYPE: 0\nID: signKey\nSENSITIVE: True\nEXTRACTABLE: False\nKey is not extractable\n```\n\nEXTRACTABLE: `False` であることが確認できました。\n\n<blockquote class=\"info\">\n<p>【 PKCS#11 属性 】</p>\n<p>PKCS#11 のプライベートキーは、通常以下の属性を持つことが期待されます：</p>\n<p><code>CLASS</code>：オブジェクトのクラスを示します。プライベートキーの場合、この値は ObjectClass.PRIVATE_KEY になります。</p>\n<p><code>KEY_TYPE</code>：キーのタイプを示します。例えば、RSA キーの場合、この値は KeyType.RSA になります。</p>\n<p><code>ID</code>：キーの一意の識別子です。同じトークン内の公開キーとプライベートキーは同じ ID を共有することがよくあります。</p>\n<p><code>SENSITIVE</code>：この属性が True に設定されている場合、キーは暗号化されていて、その値はトークン外部に出力できません。</p>\n<p><code>EXTRACTABLE</code>：この属性が True に設定されている場合、キーはラップ（エクスポート）可能です。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 CLASS: 3 】</p>\n<p>CLASS: 3 は、オブジェクトのクラスが 3 であることを示しています。PKCS#11 では、各オブジェクトクラスは特定の数値にマッピングされています。</p>\n<p>たとえば、データオブジェクトは 0、証明書は 1、公開キーは 2、プライベートキーは 3、シークレットキーは 4 などです。</p>\n<p>したがって、CLASS: 3 は、このオブジェクトがプライベートキーであることを示しています。</p>\n<p>ただし、これは一般的なマッピングであり、使用している具体的な PKCS#11 ライブラリやトークンによって異なる場合があります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 KEY_TYPE: 0 】</p>\n<p>KEY_TYPE: 0 は、キーのタイプが 0 であることを示しています。PKCS#11 では、各キータイプは特定の数値にマッピングされています。たとえば、RSA キーは 0、DSA キーは 1、DH キーは 2 などです。</p>\n<p>したがって、KEY_TYPE: 0 は、このキーが RSA キーであることを示しています。ただし、これは一般的なマッピングであり、使用している具体的な PKCS#11 ライブラリやトークンによって異なる場合があります。</p>\n</blockquote>\n\n<br />\n\n以下のようにして、EXTRACTABLE = `True` の秘密鍵を作成して、.der にエクスポートし、`openssl` コマンドで その .der を読み込んで復号処理を行います。\n\n<blockquote class=\"info\">\n<p>暗号化 → 復号化 を行う意味は、取り出した秘密鍵が正しいかどうかの確認の意味です。</p>\n</blockquote>\n\n**１．** /tmp/test-softhsm-key-export ディレクトリを使って、トークンを初期化  \n**２．** RSA キーペア登録  \n**３．** 秘密鍵をラップ（キーを暗号化）して取り出す  \n**４．** 秘密鍵をアンラップ（キーを復号化してエクスポート）  \n**５．** （秘密鍵とペアの）公開鍵を使用してサンプルテキスト `\"key extracted!\\n\"` を暗号化  \n**６．** エクスポートした秘密鍵を DER 形式で /tmp/test-softhsm-key-export/sample.der に保存  \n**７．** `openssl` コマンドを使用して暗号文を復号化（このとき、６の sample.der を読み込む）  \n**８．** 復号化した結果 `\"key extracted!\\n\"` を出力\n\n<blockquote class=\"info\">\n<p>参考：<code>https://github.com/opendnssec/SoftHSMv2/issues/597</code> に掲載されているプログラムを拝借して、少し変えただけです。</p>\n</blockquote>\n\n```python:pkcs11_key_export.py\n#!/usr/bin/python3\n# このプログラムは、https://github.com/EverTrust/pkcs11-keyextractor/blob/master/src/main/scala/fr/itassets/p11/PKCS11KeyExtractor.scala を基にしています。\n\nimport pathlib  # ファイルパス操作のためのモジュールをインポート\nimport subprocess  # サブプロセスを実行するためのモジュールをインポート\nimport os  # OSに依存した機能を使用するためのモジュールをインポート\nimport struct  # バイナリデータを扱うためのモジュールをインポート\n\nimport pkcs11  # PKCS#11インターフェースを提供するモジュールをインポート\nfrom pkcs11.mechanisms import (\n    Mechanism,\n    KeyType,\n)  # PKCS#11のメカニズムとキータイプをインポート\nfrom pkcs11.constants import Attribute, ObjectClass  # PKCS#11の定数をインポート\nfrom Crypto.Cipher import AES  # AES暗号化を提供するモジュールをインポート\n\nMODULE_PATH = \"/usr/lib/softhsm/libsofthsm2.so\"  # SoftHSM2のライブラリへのパスを定義\nTOKEN_LABEL = \"test-extract\"  # トークンのラベルを定義\nKEYPAIR_LABEL = \"test-extract-keypair\"  # キーペアのラベルを定義\nPIN = \"1234\"  # PINコードを定義\nKEK = os.urandom(16)  # 16バイトのランダムなバイト列を生成\nSAMPLE_TEXT = \"key extracted!\\n\"  # サンプルテキストを定義\n\n\ndef setup_softhsm():\n    # SoftHSM2の設定\n    softhsm_dir = pathlib.Path(\n        \"/tmp/test-softhsm-key-export\"\n    )  # SoftHSM2のディレクトリパスを定義\n    softhsm_config = softhsm_dir / \"softhsm2.conf\"  # SoftHSM2の設定ファイルパスを定義\n    token_dir = softhsm_dir / \"tokens\"  # トークンのディレクトリパスを定義\n    os.environ[\"SOFTHSM2_CONF\"] = str(\n        softhsm_config\n    )  # 環境変数にSoftHSM2の設定ファイルパスを設定\n    if token_dir.exists():  # トークンディレクトリが既に存在する場合、関数を終了\n        return\n    token_dir.mkdir(parents=True)  # トークンディレクトリを作成\n    softhsm_config.write_text(\n        f\"directories.tokendir = {token_dir}\\n\"\n    )  # SoftHSM2の設定ファイルにトークンディレクトリのパスを書き込み\n    subprocess.check_call(\n        [\n            \"softhsm2-util\",\n            \"--init-token\",\n            \"--slot\",\n            \"0\",\n            \"--label\",\n            TOKEN_LABEL,\n            \"--so-pin\",\n            PIN,\n            \"--pin\",\n            PIN,\n        ]\n    )  # softhsm2-utilコマンドを使用してトークンを初期化\n\n\ndef destroy_test_rsa_keypair(session):\n    # 既存のテストRSAキーペアを破棄\n    for key in session.get_objects(\n        {Attribute.LABEL: KEYPAIR_LABEL}\n    ):  # ラベルがKEYPAIR_LABELのオブジェクトを取得\n        key.destroy()  # キーを破棄\n\n\ndef encrypt_sample_text(session, exported_key):\n    # サンプルテキストを暗号化し、その暗号文を復号化\n    pubkey = list(session.get_objects({Attribute.LABEL: KEYPAIR_LABEL}))[\n        0\n    ]  # ラベルがKEYPAIR_LABELの公開鍵を取得\n    sample_ciphertext = pubkey.encrypt(\n        SAMPLE_TEXT\n    )  # 公開鍵を使用してサンプルテキストを暗号化\n    inkey = (\n        \"/tmp/test-softhsm-key-export/sample.der\"  # エクスポートされた鍵のパスを定義\n    )\n    with open(inkey, \"wb\") as f:  # エクスポートされた鍵を書き込み\n        f.write(exported_key)\n    openssl = subprocess.Popen(\n        [\n            \"openssl\",\n            \"pkeyutl\",\n            \"-decrypt\",\n            \"-inkey\",\n            inkey,\n            \"-keyform\",\n            \"DER\",\n            \"-pkeyopt\",\n            \"rsa_padding_mode:oaep\",\n        ],\n        stdin=subprocess.PIPE,\n    )  # opensslコマンドを使用して暗号文を復号化\n    openssl.communicate(sample_ciphertext)  # 復号化した結果を出力\n\n\ndef generate_test_rsa_keypair(session):\n    # RSAキーペアを生成\n    session.generate_keypair(\n        KeyType.RSA,\n        512,\n        mechanism=Mechanism.RSA_PKCS_KEY_PAIR_GEN,\n        label=KEYPAIR_LABEL,\n        store=True,\n        public_template={},\n        private_template={\n            Attribute.SENSITIVE: False,\n            Attribute.EXTRACTABLE: True,\n        },\n    )  # PKCS#11セッションを使用してRSAキーペアを生成。このキーペアは、後でエクスポートされる。\n\n\n# Implementation of rfc5649. This doesn't account for the case of ciphertext\n# with 2 blocks (since we are using it to export RSA private keys). While\ndef unwrap_key(kek, ciphertext):\n    # RFC5649に基づいてキーをアンラップ。この実装は、2ブロックの暗号文のケースを考慮していない。\n    # AES ECBの6ステップを使用\n    decrypt = AES.new(kek, AES.MODE_ECB).decrypt\n    steps = 6\n    # 暗号文を8バイトのブロックに分割\n    blocks = []\n    block_size = 8\n    block_count = len(ciphertext) // block_size - 1\n    for i in range(block_count):\n        block = (i + 1) * block_size\n        blocks.append(ciphertext[block : block + block_size])\n    # 64ビット符号なし整数をシリアライズ/デシリアライズするためのstruct.pack形式\n    ulong_be = \">Q\"\n    # 完全性/サイズブロック。復号化後、代替初期値（AIV）と元のプレーンテキストのサイズを含むべき。\n    aiv = struct.unpack(ulong_be, ciphertext[:8])[0]\n    for j in range(0, steps):\n        for i in range(block_count, 0, -1):\n            cipherblock = (\n                struct.pack(ulong_be, aiv ^ ((5 - j) * block_count + i)) + blocks[i - 1]\n            )\n            block = decrypt(cipherblock)\n            aiv = struct.unpack(ulong_be, block[:8])[0]\n            blocks[i - 1] = block[8:]\n    assert aiv & 0xFFFFFFFF00000000 == 0xA65959A600000000\n    # 完全性チェックが通過。元の長さは、aivの32 LSBで指定。\n    length = aiv & 0xFFFFFFFF\n    plaintext_with_pad = b\"\".join(blocks)\n    return plaintext_with_pad[:length]  # パディング付きのプレーンテキストを返す\n\n\ndef export_private_key(session, key):\n    # 秘密鍵をエクスポート\n    kek_handle = session.create_object(\n        {\n            Attribute.CLASS: ObjectClass.SECRET_KEY,\n            Attribute.KEY_TYPE: KeyType.AES,\n            Attribute.VALUE: KEK,\n            Attribute.WRAP: True,\n            Attribute.UNWRAP: True,\n            Attribute.TOKEN: False,\n        }\n    )  # KEKを使用して秘密鍵をラップ\n    # KEK = Key Encryption Key（キーを暗号化するキー）の略\n    wrapped_key = kek_handle.wrap_key(\n        key, mechanism=Mechanism.AES_KEY_WRAP_PAD\n    )  # 秘密鍵をラップ\n    clear_key = unwrap_key(KEK, wrapped_key)  # ラップされた鍵をアンラップ\n    return clear_key  # アンラップされた鍵を返す\n\n\ndef main():\n    setup_softhsm()  # SoftHSM2をセットアップ\n    lib = pkcs11.lib(MODULE_PATH)  # PKCS#11ライブラリをロード\n    token = lib.get_token(token_label=TOKEN_LABEL)  # トークンを取得\n\n    with token.open(user_pin=PIN, rw=True) as session:  # トークンを開く\n        destroy_test_rsa_keypair(session)  # テストRSAキーペアを破棄\n        generate_test_rsa_keypair(session)  # テストRSAキーペアを生成\n        keys = list(\n            session.get_objects(\n                {\n                    Attribute.LABEL: KEYPAIR_LABEL,\n                    Attribute.EXTRACTABLE: True,\n                }\n            )\n        )  # エクスポート可能なキーペアを取得\n        exported_key = export_private_key(session, keys[0])  # 秘密鍵をエクスポート\n        encrypt_sample_text(session, exported_key)  # サンプルテキストを暗号化\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n```shellsession\n# python3 pkcs11_key_export.py\nThe token has been initialized and is reassigned to slot 1496171450\nkey extracted!\n# ls -l /tmp/test-softhsm-key-export/sample.der\n-rw-r--r-- 1 root root 352  2月 11 18:51 /tmp/test-softhsm-key-export/sample.der\n```\n\n<br />\n\nＯＫ！！\n\n<br />\n","description":"EJBCAからPKCS#11でSoftHSM2をHSMとして、キーペア（公開鍵/秘密鍵）を登録してみました。さらに、移行（エクスポート/インポート）について検証しました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"79hocu3k9x","createdAt":"2024-02-13T13:51:51.359Z","updatedAt":"2024-02-13T13:51:51.359Z","publishedAt":"2024-02-13T13:51:51.359Z","revisedAt":"2024-02-13T13:51:51.359Z","url":"https://itc-engineering-blog.imgix.net/ejbca-softhsm2/ITC_Engineering_Blog.png","alt":"EJBCAからPKCS#11でSoftHSM2にキーペアを登録してみた","width":1200,"height":630}],"seo_authors":[]},{"id":"gitlab-nginx-php","createdAt":"2021-12-17T08:22:42.652Z","updatedAt":"2022-01-03T11:51:29.655Z","publishedAt":"2021-12-17T08:22:42.652Z","revisedAt":"2022-01-03T11:51:29.655Z","title":"GitLabバンドルnginxを利用してphpの独自Webアプリを同居させる手順","category":{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},"topics":[{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},{"id":"9iy1ks71tv7n","createdAt":"2021-05-31T13:08:18.404Z","updatedAt":"2021-08-31T12:04:47.612Z","publishedAt":"2021-05-31T13:08:18.404Z","revisedAt":"2021-08-31T12:04:47.612Z","topics":"Nginx","logo":"/logos/Nginx.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false}],"content":"# はじめに\n\nGitLab をインストールすると、nginx がインストールされています。GitLab のインストール方法は、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/fgm-dkbu10\" target=\"_blank\">Ubuntu 20.04.2.0 に GitLab をインストール</a>」にあります。nginx の実体は、`/opt/gitlab/embedded/sbin/nginx`にインストールされます。\n\n<br />\n\nnginx は、以下の GitLab 構造図を見ると、80,443 ポートの窓口の役割になっています。  \n\n<a href=\"https://docs.gitlab.com/ee/development/img/architecture_simplified.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://docs.gitlab.com/ee/development/img/architecture_simplified.png\" alt=\"GitLab 構造図\"></a>\n\n<br />\n\nGitLab をインストールした環境に GitLab とは別の Web アプリを置きたいとなったとき、GitLab バンドル nginx の設定を変更して、同居が実現できます。  \n今回、php の独自 Web アプリを同居させる手順を書きます。（今回の記事中のゴールは、Web アプリ起動＝ phpinfo 表示です。）\n\n<blockquote class=\"warn\">\n<p>「<a href=\"https://itc-engineering-blog.netlify.app/blogs/fgm-dkbu10\" target=\"_blank\">Ubuntu 20.04.2.0にGitLabをインストール</a>」でインストールされた環境からスタートが前提です。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS</code></p>\n<p>　<code>GitLab v13.11.2</code></p>\n<p>　<code>nginx 1.18.0</code></p>\n<p>　<code>PHP 8.0.13</code></p>\n</blockquote>\n\n<br />\n\n【メモ】  \n・nginx の設定ファイル  \n`/var/opt/gitlab/nginx/conf/nginx.conf`  \n`/var/opt/gitlab/nginx/conf/gitlab-download-app.conf`\n\n・php-fpm の設定ファイル  \n`/etc/php/8.0/fpm/php-fpm.conf`\n\n・php の設定ファイル  \n`/etc/php/8.0/fpm/php.ini`\n\n・コマンド  \n`gitlab-ctl reconfigure`  \n`gitlab-ctl restart nginx`  \n`systemctl restart php8.0-fpm`\n\n<br />\n\n# PHP インストール\n\napt を更新します。\n\n```shellsession\n# apt update\n```\n\n<blockquote class=\"info\">\n<p>「<a href=\"https://itc-engineering-blog.netlify.app/blogs/i-hna4_wx\" target=\"_blank\">Ubuntu 20.04.2.0にapache2,php,postgresqlをインストール</a>」に詳しい説明付きの手順がありますので、詳しい説明は、端折ります。</p>\n</blockquote>\n\n<br />\n\nPHP8 をインストールします。（拡張モジュールは適当です。）\n\n```shellsession\n# apt -y install software-properties-common\n# add-apt-repository ppa:ondrej/php\nPress [ENTER] to continue or Ctrl-c to cancel adding it.\nエンター\n# apt update\n# apt -y install php8.0 php8.0-gd php8.0-mbstring php8.0-common\n# apt -y install curl\n# apt -y install php8.0-curl\n# php -v\nPHP 8.0.13 (cli) (built: Nov 22 2021 09:50:43) ( NTS )\nCopyright (c) The PHP Group\nZend Engine v4.0.13, Copyright (c) Zend Technologies\n    with Zend OPcache v8.0.13, Copyright (c), by Zend Technologies\n# curl -V\ncurl 7.68.0 (x86_64-pc-linux-gnu) libcurl/7.68.0 OpenSSL/1.1.1f zlib/1.2.11 brotli/1.0.7 libidn2/2.2.0 libpsl/0.21.0 (+libidn2/2.2.0) libssh/0.9.3/openssl/zlib nghttp2/1.40.0 librtmp/2.3\n```\n\n<br />\n\napache2 が同時インストールされますが、今回要らないため、削除します。  \n\n```shellsession\n# apt list --installed | grep apache2\n\nWARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n\napache2-bin/focal-updates,now 2.4.41-4ubuntu3.8 amd64 [インストール済み、自動]\napache2-data/focal-updates,focal-updates,now 2.4.41-4ubuntu3.8 all [インストール済み、自動]\napache2-utils/focal-updates,now 2.4.41-4ubuntu3.8 amd64 [インストール済み、自動]\napache2/focal-updates,now 2.4.41-4ubuntu3.8 amd64 [インストール済み、自動]\nlibapache2-mod-php8.0/focal,now 8.0.14-1+ubuntu20.04.1+deb.sury.org+1 amd64 [インストール済み、自動]\n# apt -y remove apache2-*\n```\n\n<br />\n\nphp-fpm をインストールします。\n\n```shellsession\n# apt install -y php-fpm\n```\n\n<blockquote class=\"info\">\n<p>【 php-fpm 】</p>\n<p>php-fpmとは fpmはFastCGI Process Managerの略でPHP5.4.0から公式サポートされたPHP標準のアプリケーションサーバです。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/php-fpm.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/php-fpm.png\" alt=\"php-fpmとは fpmはFastCGI Process Managerの略\" width=\"429\" height=\"88\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>【 FastCGI 】</p>\n<p>FastCGIとは、Webサーバ上でユーザプログラムを動作させるためのインタフェース仕様の一つです。プロセスの起動/終了を省略することで、負荷を軽減しています。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/FastCGI.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/FastCGI.png\" alt=\"FastCGIとは、Webサーバ上でユーザプログラムを動作させるためのインタフェース仕様の一つ\" width=\"523\" height=\"377\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n# php-fpm 設定\n\nUNIX ソケットを利用する設定にします。\n\n```shellsession\n# vi /etc/php/8.0/fpm/pool.d/www.conf\n```\n\n```sh\nlisten = /run/php/php8.0-fpm.sock\n```\n\n※デフォルトでなっていました。（その場合は、確認のみ。）\n\n<blockquote class=\"info\">\n<p>【 UNIXドメインソケット 】</p>\n<p>UNIXドメインソケットはBSDソケットの一種であり、単一マシン上でのプロセス間通信を目的としています。</p>\n<p>単一マシン上の通信である（=インターネットを介さない）ことを生かした高効率な通信を可能にしています。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/unixsocket.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/unixsocket.png\" alt=\"UNIXドメインソケット\" width=\"468\" height=\"132\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\nphp8.0-fpm が systemctl のサービスとして有効かどうか確認します。\n\n```shellsession\n# systemctl list-unit-files --type=service | grep php\n```\n\n```sh\nphp8.0-fpm.service          enabled         enabled\nphpsessionclean.service     static          enabled\n```\n\n※デフォルトで有効になっていました。（その場合は、確認のみ。）\n\n<blockquote class=\"info\">\n<p>【<code>systemctl list-unit-files</code>】</p>\n<p>定義されているサービス一覧を表示します。<code>--type=service</code>が無ければ、全てのユニットを表示します。左側が現状、右側がVENDOR PRESET、つまり、デフォルトの状態です。</p>\n</blockquote>\n\n有効になっていない場合、有効化します。\n\n```shellsession\n# systemctl enable php8.0-fpm\n```\n\n<br />\n\n`fastcgi_params` に環境変数を登録します。（ここに書いたものが php の`$_SERVER`として使える環境変数になります。）\n\n```shellsession\n# vi /var/opt/gitlab/nginx/conf/fastcgi_params\n```\n\n```sh\nfastcgi_param   QUERY_STRING            $query_string;\nfastcgi_param   REQUEST_METHOD          $request_method;\nfastcgi_param   CONTENT_TYPE            $content_type;\nfastcgi_param   CONTENT_LENGTH          $content_length;\n\nfastcgi_param   SCRIPT_FILENAME         $document_root$fastcgi_script_name;\nfastcgi_param   SCRIPT_NAME             $fastcgi_script_name;\nfastcgi_param   PATH_INFO               $fastcgi_path_info;\nfastcgi_param   PATH_TRANSLATED         $document_root$fastcgi_path_info;\nfastcgi_param   REQUEST_URI             $request_uri;\nfastcgi_param   DOCUMENT_URI            $document_uri;\nfastcgi_param   DOCUMENT_ROOT           $document_root;\nfastcgi_param   SERVER_PROTOCOL         $server_protocol;\n\nfastcgi_param   GATEWAY_INTERFACE       CGI/1.1;\nfastcgi_param   SERVER_SOFTWARE         nginx/$nginx_version;\n\nfastcgi_param   REMOTE_ADDR             $remote_addr;\nfastcgi_param   REMOTE_PORT             $remote_port;\nfastcgi_param   SERVER_ADDR             $server_addr;\nfastcgi_param   SERVER_PORT             $server_port;\nfastcgi_param   SERVER_NAME             $server_name;\n\nfastcgi_param   HTTPS                   $https;\n\n# PHP only, required if PHP was built with --enable-force-cgi-redirect\nfastcgi_param   REDIRECT_STATUS         200;\n```\n\n<br />\n\n# nginx 設定\n\nPHP Web アプリの置き場所を`/opt/gitlab-download-app/www/html`とします。`/opt/gitlab-download-app/www/html`は任意です。\n\n<br />\n\nroot ディレクトリ、ログディレクトリを作成します。\n\n```shellsession\n# mkdir -p /opt/gitlab-download-app/www/html\n# chown -R gitlab-www:gitlab-www /opt/gitlab-download-app\n# mkdir /var/log/gitlab-download-app\n```\n\n`chown -R gitlab-www:gitlab-www`は、`/etc/gitlab/gitlab.rb`に nginx 起動ユーザーが以下のように定義してあるからです。（コメントアウト＝デフォルト値）\n\n```sh\n# web_server['username'] = 'gitlab-www'\n# web_server['group'] = 'gitlab-www'\n```\n\n<br />\n\nPHP Web アプリ用の設定ファイルを新規追加します。\n\n```shellsession\n# vi /var/opt/gitlab/nginx/conf/gitlab-download-app.conf\n```\n\nサーバー名：`gitlab-download-app.itccorporation.jp`とします。\n\n```nginix\nserver\n{\n  listen 80;\n  server_name gitlab-download-app.itccorporation.jp;\n  access_log /var/log/gitlab-download-app/access.log;\n  error_log /var/log/gitlab-download-app/error.log;\n\n  root /opt/gitlab-download-app/www/html;\n\n  location /\n  {\n    index index.html index.htm index.php;\n  }\n\n  location ~ [^/]\\.php(/|$)\n  {\n    fastcgi_split_path_info ^(.+?\\.php)(/.*)$;\n    if (!-f $document_root$fastcgi_script_name)\n    {\n      return 404;\n    }\n\n    client_max_body_size 100m;\n\n    # Mitigate https://httpoxy.org/ vulnerabilities\n    fastcgi_param HTTP_PROXY \"\";\n\n    # fastcgi_pass 127.0.0.1:9000;\n    fastcgi_pass unix:/run/php/php8.0-fpm.sock;\n    fastcgi_index index.php;\n\n    # include the fastcgi_param setting\n    include fastcgi_params;\n\n    # SCRIPT_FILENAME parameter is used for PHP FPM determining\n    #  the script name. If it is not set in fastcgi_params file,\n    # i.e. /etc/nginx/fastcgi_params or in the parent contexts,\n    # please comment off following line:\n    # fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;\n  }\n}\n```\n\n<br />\n\n<span style=\"color:red;\">`gitlab-download-app.conf`が GitLab バンドルの nginx に読み込まれるように、</span>以下のように設定します。\n\n```shellsession\n# vi /etc/gitlab/gitlab.rb\n```\n\n```sh\nnginx['custom_nginx_config'] = \"include gitlab-download-app.conf;\"\n```\n\nGitLab の設定再読み込みを行います。\n\n```shellsession\n# gitlab-ctl reconfigure\n```\n\nこれにより、何が起きるかというと、GitLab バンドルの nginx の設定ファイル`/var/opt/gitlab/nginx/conf/nginx.conf`に`include gitlab-download-app.conf;`の行が追加されます。\n\nテンプレートとして以下の eRuby ファイルが置いてあり、以下のような経緯で配置されます。\n\n`/opt/gitlab/embedded/cookbooks/gitlab/templates/default/nginx.conf.erb`  \n↓  \n`gitlab-ctl reconfigure`  \n↓  \n`nginx.conf.erb` の`<%= @custom_nginx_config %>`が置換される  \n↓  \n`/var/opt/gitlab/nginx/conf/nginx.conf`更新\n\n<br />\n\n`gitlab-ctl reconfigure`の度にこれを繰り返すため、`/var/opt/gitlab/nginx/conf/nginx.conf`を直接編集すると、次の`gitlab-ctl reconfigure`で直接編集した内容は消えます。<span style=\"color:red;\">変更を固定化したい場合、`/opt/gitlab/embedded/cookbooks/gitlab/templates/default/nginx.conf.erb`を書き換えると、固定化できます。</span>（今回はその必要が無いです。）\n\n<blockquote class=\"info\">\n<p>【 .erb 】</p>\n<p>.erbは、eRubyのファイルの拡張子です。</p>\n<p>eRuby（embedded Ruby）とは、Rubyの周辺技術の一つで、HTMLへRubyスクリプトを埋め込む事を可能とする技術です。</p>\n</blockquote>\n\n<br />\n\n# php-fpm 起動～動作確認\n\nエラーが発生した場合、画面に表示されるようにします。（任意です。）\n\n```shellsession\n# vi /etc/php/8.0/fpm/php.ini\n```\n\n```ini\ndisplay_errors = On\ndisplay_startup_errors = On\n```\n\n<br />\n\nFPM プロセスのユーザーと UNIX ドメインソケットのユーザーを nginx のユーザーに合わせます。デフォルトは、www-data です。\n\n```shellsession\n# vi /etc/php/8.0/fpm/pool.d/www.conf\n```\n\n```sh\nuser = gitlab-www\ngroup = gitlab-www\n```\n\nと\n\n```sh\nlisten.owner = gitlab-www\nlisten.group = gitlab-www\n```\n\n<br />\n\nGitLab と PHP Web アプリが名前解決できない場合、hosts に追加します。\n\n```shellsession\n# vi /etc/hosts\n```\n\n```sh\n192.168.12.111 gitlab.itccorporation.jp\n192.168.12.111 gitlab-download-app.itccorporation.jp\n```\n\n<br />\n\nnginx と php-fpm をリスタートします。\n\n```shellsession\n# gitlab-ctl restart nginx\n# systemctl restart php8.0-fpm\n```\n\n<blockquote class=\"info\">\n<p>【 gitlab-ctl 】</p>\n<p>GitLab Omnibus（いろいろバンドルされたもののこと）の１つのコンポーネントを再起動するには、<code>gitlab-ctl restart &lt;component&gt;</code>を実行します。したがって、Nginxを再起動するには<code>gitlab-ctl restart nginx</code>です。</p>\n<p><code>gitlab-ctl tail</code>とすると、すべてのGitLabログを表示できます。<code>gitlab-ctl tail nginx</code>はnginxログのみをtailします。</p>\n</blockquote>\n\n<br />\n\nテストアプリを作成します。（アプリが既に有る場合、目的のアプリをここに配置します。）\n\n```shellsession\n# vi /opt/gitlab-download-app/www/html/info.php\n```\n\n```php\n<?php\n  phpinfo();\n```\n\n<br />\n\nhosts を`192.168.12.111 gitlab-download-app.itccorporation.jp`とし、  \n`http://gitlab-download-app.itccorporation.jp/info.php` で確認します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/image1.png\" alt=\"info.php確認\" width=\"995\" height=\"364\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！\n","description":"GitLab をインストールした環境に GitLab とは別の Web アプリを同居させる手順です。GitLabバンドルnginxをそのまま利用します。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"n2elug7eqm","createdAt":"2021-12-17T08:20:07.159Z","updatedAt":"2021-12-17T08:20:07.159Z","publishedAt":"2021-12-17T08:20:07.159Z","revisedAt":"2021-12-17T08:20:07.159Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-nginx-php/ITC_Engineering_Blog.png","alt":"GitLabバンドルnginxを利用してphpの独自Webアプリを同居させる手順","width":1200,"height":630}],"seo_authors":[]},{"id":"gitlab-ci-cd","createdAt":"2022-06-14T12:20:03.838Z","updatedAt":"2022-06-24T05:47:11.886Z","publishedAt":"2022-06-14T12:20:03.838Z","revisedAt":"2022-06-24T05:47:11.886Z","title":"Kotlin＋Spring Bootのアプリを GitLab CI/CD によるDockerデプロイまで全手順","category":{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},"topics":[{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false},{"id":"jgza8_u_t2xl","createdAt":"2022-06-12T08:52:15.121Z","updatedAt":"2022-06-12T08:52:15.121Z","publishedAt":"2022-06-12T08:52:15.121Z","revisedAt":"2022-06-12T08:52:15.121Z","topics":"Spring Boot","logo":"/logos/SpringBoot.png","needs_title":true},{"id":"6jp855sldd","createdAt":"2022-04-29T11:49:21.447Z","updatedAt":"2022-04-29T11:49:21.447Z","publishedAt":"2022-04-29T11:49:21.447Z","revisedAt":"2022-04-29T11:49:21.447Z","topics":"Kotlin","logo":"/logos/Kotlin.png","needs_title":false}],"content":"# はじめに\n\nKotlin ＋ SpringBoot で REST API 実装  \n↓  \nGitLab CI/CD パイプライン発動  \n↓  \nテスト／ビルド  \n↓  \nDocker ビルド  \n↓  \nデプロイ  \nの流れを\n\n<br />\n\n最初から最後までやってみましたので、その全手順になります。\n\n<br />\n\nKotlin ＋ SpringBoot で REST API 実装については、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/spring-boot-kotlin-docker\" target=\"_blank\">Kotlin ＋ Spring Boot ＋ Docker ビルド(Gradle, Fat JAR)まで全手順</a>」にありますので、省略して、その続きからになります。  \n`/home/admin/demo` にソースコードが有る状態とします。\n\n<br />\n\n全体像は以下です。  \n・develop ブランチにソースコードが push されたら、GitLab CI/CD パイプライン発動  \n　 →localhost の docker ビルド＆デプロイのみ  \n・master ブランチにソースコードが push されたら、GitLab CI/CD パイプライン発動  \n　 →localhost の docker ビルド＆デプロイ ＋ 本番サーバの docker ビルド＆デプロイ（ただし、本番サーバーについては、手動起動とする。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd1.png\" alt=\"GitLab CI/CD 完成図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>本記事にベストプラクティス的な趣旨は有りません。</p>\n<p>本番サーバのdockerビルド＆デプロイは、AWS ECS、AWS EKSなどにデプロイすれば、実運用っぽくなると思いますが、そこまでは実施しません。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>全て一つのマシンにインストールする必要はありません。一つのUbuntuマシンにIntelliJ IDEA、Docker、GitLab、GitLab Runnerと詰め込み過ぎて、最初メモリ4GBにしていましたが、8GBにしないとまともに動かなくなりました。</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS</code></p>\n<p>　<code>IntelliJ IDEA 2022.1.2(Community Edition)</code></p>\n<p>　<code>Docker version 20.10.17, build 100c701</code></p>\n<p>　<code>GitLab CE 15.0.2</code></p>\n<p>　<code>gitlab-runner 15.0.0</code></p>\n<p>　<code>gradle:7.3.3-jdk17-alpine</code></p>\n</blockquote>\n\n<br />\n\n# GitLab CI/CD 基本\n\nまずおさえておきたい基本用語です。作業進行中にもなるべく説明していますので、ここで去らないでください...。\n\n<br />\n\n<blockquote class=\"info\">\n<p>【CI(継続的インテグレーション)】</p>\n<p>リポジトリへのプッシュごとに、アプリケーションにエラーが発生する可能性を減らすために、</p>\n<p>アプリケーションを自動的にビルド、テストするためのスクリプトを作成できます。</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"info\">\n<p>【CD(継続的デリバリー)】</p>\n<p>コードベースに変更がプッシュされるごとにビルド、テストされます。</p>\n<p>それだけではなく、手動でデプロイを開始するステップを追加して、継続的にデプロイされます。</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"info\">\n<p>【継続的デプロイ】</p>\n<p>継続的デリバリーとの違いは、アプリケーションを手動でデプロイするのではなく、</p>\n<p>自動的にデプロイされるように設定することです。アプリケーションをデプロイするために、</p>\n<p>人の介入は全く必要ありません。</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"info\">\n<p>【パイプライン】</p>\n<p>CI/CD一連の自動化された工程の事です。</p>\n<p><span style=\"color: red;\"><strong>設定ファイル .gitlab-ci.yml</strong></span> をリポジトリに追加すると、</p>\n<p>GitLabがそれを検出して <span style=\"color: red;\"><strong>GitLab Runner というツールを使ってスクリプトを実行します。</strong></span></p>\n<p>スクリプトはジョブとしてグループ化され、パイプラインの構成要素となります。</p>\n<p>.preは、常にパイプラインの最初のステージであることが保証されています。</p>\n<p>.postは、常にパイプラインの最後のステージであることが保証されています。</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd2.png\" alt=\"GitLab CI/CD 設定ファイル .gitlab-ci.yml の説明\" width=\"485\" height=\"607\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n</blockquote>\n\n<br />\n\nRunner（CI/CD 実行体）は、以下の３種類ありますが、今回は、Specific runners を利用します。\n\n| Runner 種類      | 利用可能範囲       |\n| ---------------- | ------------------ |\n| Specific runners | 特定のリポジトリ   |\n| Group runners    | 特定のグループ     |\n| Shared runners   | すべてのリポジトリ |\n\n<br />\n\n# Docker インストール\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd3.png\" alt=\"GitLab CI/CD Dockerインストール 図\" width=\"942\" height=\"772\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nGitLab、GitLab Runner（GitLab CI/CD を実行するもの）を Docker でインストールしようと思いますので、Docker をインストールします。  \nアプリのデプロイも Docker でビルド＆デプロイしますので、Docker インストールが必要です。\n\n```shellsession\n# apt update\n# apt install -y apt-transport-https ca-certificates software-properties-common\n# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n# add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\"\n# apt update\n# apt install -y docker-ce\n# docker -v\nDocker version 20.10.17, build 100c701\n```\n\n<br />\n\ndocker-compose up で GitLab、GitLab Runner を起動しますので、docker-compose 1.29.2 をインストールします。\n\n```shellsession\n# curl -L \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n# chmod +x /usr/local/bin/docker-compose\n# docker-compose --version\ndocker-compose version 1.29.2, build 5becea4c\n```\n\n<br />\n\n# GitLab インストール\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd4.png\" alt=\"GitLab CI/CD GitLabインストール 図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\ndocker-compose.yml を作成します。\n\n<br />\n\nDocker Hub の `gitlab/gitlab-ce:latest` を利用します。  \n**ホスト名：** `gitlab-ci.example.com`  \n**URL:** `https://gitlab-ci.example.com`  \n**GitLab のコンテナレジストリ API URL:** `https://gitlab-ci.example.com:5005`  \n（`redirect_http_to_https=true` により、GitLab Container Registry を有効化しています。）  \n**初期パスワード:** `adminadmin`\n\n<br />\n\n<blockquote class=\"info\">\n<p>【GitLab Container Registry】</p>\n<p>GitLabのコンテナレジストリ。GitLabのコンテナレジストリとは、Docker Hubのようにコンテナをpushできる場所になります。<span style=\"color: red;\"><strong>オフライン運用できるプライベートDockerレジストリ</strong></span>です。</p>\n</blockquote>\n\n```shellsession\n# mkdir /home/admin/gitlab\n# cd /home/admin/gitlab\n# vi docker-compose.yml\n```\n\n```yaml:docker-compose.yml\nservices:\n  web:\n    image: 'gitlab/gitlab-ce:latest' # gitlab/gitlab-ceイメージ最新バージョンをDocker Hubからpull\n    restart: 'always' # 自動起動on\n    hostname: 'gitlab-ci.example.com' # GitLabサーバーホスト名\n    environment:\n      # /etc/gitlab/gitlab.rbの設定を環境変数GITLAB_OMNIBUS_CONFIGに記述\n      GITLAB_OMNIBUS_CONFIG: |\n        external_url 'https://gitlab-ci.example.com' # GitLabのURL\n        registry_external_url 'https://gitlab-ci.example.com:5005' # GitLabのコンテナレジストリAPI URL\n        gitlab_rails['gitlab_ssh_host'] = '192.168.11.6' # GitLab sshホスト名（本記事では使わない。）\n        gitlab_rails['initial_root_password'] = 'adminadmin' # rootユーザーパスワード（後から変更可）\n        gitlab_rails['registry_enabled'] = true # GitLab Container Registry を有効化\n        nginx['redirect_http_to_https'] = true # http://で来たら、https://にリダイレクト\n        nginx['ssl_certificate'] = \"/etc/gitlab/ssl/gitlab-ci.example.com.crt\" # ssl証明書\n        nginx['ssl_certificate_key'] = \"/etc/gitlab/ssl/gitlab-ci.example.com.key\" # ssl秘密鍵\n    ports:\n      - '80:80'\n      - '443:443'\n      - '2222:22' # sshポート（Ubuntu標準の22ポートと被るため、2222に変更）\n      - '5005:5005' # GitLab Container Registryポート（registry_external_urlに合わせる。）\n    volumes:\n      # 永続化したいデータとSSL証明書関係をホスト側で共有\n      - './config:/etc/gitlab'\n      - './logs:/var/log/gitlab'\n      - './data:/var/opt/gitlab'\n      - './ca.crt:/etc/gitlab/ssl/gitlab-ci.example.com.crt'\n      - './ca.key:/etc/gitlab/ssl/gitlab-ci.example.com.key'\n      - './ca.csr:/etc/gitlab/ssl/gitlab-ci.example.com.csr'\n    shm_size: '256m' # コンテナが使用する共有メモリサイズ（適当。）\n```\n\n<br />\n\nssl key を作成します。  \n今回、自己署名証明書作成になります。既に有る場合、`./ca.crt`, `./ca.key`, `./ca.csr` を配置します。\n\n```shellsession\n# openssl genrsa -out ca.key 2048\nGenerating RSA private key, 2048 bit long modulus (2 primes)\n................................................................................................................................................+++++\n.............................+++++\ne is 65537 (0x010001)\n\n# openssl req -new -key ca.key -out ca.csr\nYou are about to be asked to enter information that will be incorporated\ninto your certificate request.\nWhat you are about to enter is what is called a Distinguished Name or a DN.\nThere are quite a few fields but you can leave some blank\nFor some fields there will be a default value,\nIf you enter '.', the field will be left blank.\n-----\nCountry Name (2 letter code) [XX]:JP\nState or Province Name (full name) []:Aichi\nLocality Name (eg, city) [Default City]:Toyota\nOrganization Name (eg, company) [Default Company Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (eg, your name or your server's hostname) []:gitlab-ci.example.com\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\nAn optional company name []:\n\n# echo \"subjectAltName=DNS:*.example.com,IP:192.168.11.6\" > san.txt\n# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt -extfile san.txt\nSignature ok\nsubject=C = JP, ST = Aichi, L = Toyota, O = Default Company Ltd, CN = gitlab-ci.example.com\nGetting Private key\n```\n\n<br />\n\n今回の場合ですが、80 ポートが apache2 に使われていて、起動に失敗するため、apache2 を削除します。\n\n```shellsession\n# lsof -i :80\napache2  947     root    4u  IPv6  44939      0t0  TCP *:http (LISTEN)\napache2 1101 www-data    4u  IPv6  44939      0t0  TCP *:http (LISTEN)\napache2 1102 www-data    4u  IPv6  44939      0t0  TCP *:http (LISTEN)\napache2 1103 www-data    4u  IPv6  44939      0t0  TCP *:http (LISTEN)\napache2 1105 www-data    4u  IPv6  44939      0t0  TCP *:http (LISTEN)\napache2 1108 www-data    4u  IPv6  44939      0t0  TCP *:http (LISTEN)\n# apt list --installed | grep ^apache2-\napache2-bin/now 2.4.41-4ubuntu3.1 amd64 [installed,upgradable to: 2.4.41-4ubuntu3.11]\napache2-data/now 2.4.41-4ubuntu3.1 all [installed,upgradable to: 2.4.41-4ubuntu3.11]\napache2-utils/now 2.4.41-4ubuntu3.1 amd64 [installed,upgradable to: 2.4.41-4ubuntu3.11]\n# apt update\n# apt -y remove apache2-*\n# lsof -i :80\n```\n\n<br />\n\n起動します。\n\n```shellsession\n# docker-compose up -d\n```\n\n<br />\n\nGitLab を hosts に登録します。\n\n```shellsession\n# vi /etc/hosts\n```\n\n```sh\n127.0.0.1 gitlab-ci.example.com\n```\n\n<br />\n\nログインします。（注意：ホストマシンの性能によっては、起動後、数分レベルでしばらく待つ必要があるかもしれません。）  \n`https://gitlab-ci.example.com/`  \nUsername: `root`  \nPassword: `adminadmin`\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image1.png\" alt=\"GitLab ログイン１\" width=\"887\" height=\"628\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image2.png\" alt=\"GitLab ログイン２\" width=\"893\" height=\"551\" loading=\"lazy\"></a>\n\n<br />\n\ndocker クライアントが使う証明書を配置します。  \n配置場所は、`/etc/docker/certs.d/<ホスト名>`  \nディレクトリになります。  \n拡張子が `.crt` であれば、ファイル名は何でも良いようです。\n\n```shellsession\n# cd /home/admin/gitlab\n# mkdir -p /etc/docker/certs.d/gitlab-ci.example.com:5005\n# cp -p ca.crt /etc/docker/certs.d/gitlab-ci.example.com:5005/ca.crt\n```\n\n<br />\n\ndocker クライアントで GitLab Container Registry へのログインを試みます。\n\n```shellsession\n# docker login gitlab-ci.example.com:5005\nUsername: root\nPassword: adminadmin\nAuthenticating with existing credentials...\nWARNING! Your password will be stored unencrypted in /root/.docker/config.json.\nConfigure a credential helper to remove this warning. See\nhttps://docs.docker.com/engine/reference/commandline/login/#credentials-store\n\nLogin Succeeded\n```\n\nGitLab インストールＯＫ！！\n\n<br />\n\n<blockquote class=\"info\">\n<p><code>docker login</code> 後、<code>docker push</code>、<code>docker pull</code> を行うと、<code>gitlab-ci.example.com:5005</code> に対しての操作になります。</p>\n</blockquote>\n\n<br />\n\n# ソースコード push\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd5.png\" alt=\"GitLab CI/CD ソースコードpush 図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nIntelliJ IDEA からソースコードを push します。  \ngit commit → GitLab へ push です。（少し説明を端折ります。）  \nこのやり方である必要はありません。  \nなお、この時点では、パイプラインの設定を一切していませんので、CI/CD に関しては、何も起きません。\n\n<br />\n\nソースコードは、Kotlin ＋ SpringBoot が `demo` ディレクトリにあるものとして、  \nプロジェクト名は、`demo` とします。  \nリポジトリ URL は、`https://gitlab-ci.example.com/gitlab-instance-10d4eff5/demo.git` とします。\n\n<br />\n\n<span style=\"color: red;\">push 先の gitlab-ci.example.com が https:// かつ、自己署名証明書のため、あらかじめ、sslVerify を off にして操作しました。</span>\n\n```shellsession\n$ git config --global http.sslVerify false\n```\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image3.png\" alt=\"git commit → GitLabへpush１\" width=\"950\" height=\"722\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image4.png\" alt=\"git commit → GitLabへpush２\" width=\"1128\" height=\"705\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image5.png\" alt=\"git commit → GitLabへpush３\" width=\"1125\" height=\"704\" loading=\"lazy\"></a>\n\n<br />\n\n# Gitlab Runner インストール\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd6.png\" alt=\"GitLab CI/CD Gitlab Runnerインストール 図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nGitLab CI/CD 実行体 Gitlab Runner をインストールします。こちらも Docker コンテナで実行します。GitLab の docker-compose.yml に一緒に書けば良いですが、あえて個別に起動しています。\n\n<br />\n\ndocker-compose.yml を作成します。  \n`${CI_SERVER_URL}` のような変数の部分は、後で `.env` に値を記述します。\n\n```shellsession\n# mkdir /home/admin/gitlab-runner\n# cd /home/admin/gitlab-runner\n# vi docker-compose.yml\n```\n\n```yaml:docker-compose.yml\nversion: '3'\nservices:\n  runner:\n    image: 'gitlab/gitlab-runner:latest' # gitlab/gitlab-runnerイメージ最新バージョンをDocker Hubからpull\n    restart: 'always' # 自動起動on\n    # 環境変数でgitlab-runner registerのオプション設定\n    environment:\n      - CI_SERVER_URL=${CI_SERVER_URL} # CI/CDをしたいリポジトリがあるサーバー e.g. https://gitlab-ci.example.com\n      - REGISTRATION_TOKEN=${REGISTRATION_TOKEN} # GitLabが発行したトークン（後述）\n      - RUNNER_EXECUTOR=docker # Gitlab Runnerの実行方式（後述）\n      - RUNNER_TAG_LIST=${RUNNER_TAG_LIST} # Gitlab Runnerのタグ（パイプライン設定で紐付けに使うもの。）\n      - RUNNER_NAME=${RUNNER_NAME} # Gitlab Runnerの名前（パイプライン設定では使わない。画面に表示されるもの。）\n      - DOCKER_IMAGE=docker:stable # Gitlab Runnerの中で使うdocker。dockerイメージ安定版をDocker Hubからpull\n      - DOCKER_VOLUMES=/var/run/docker.sock:/var/run/docker.sock # DooD(Docker outside of Docker)でホスト側dockerdを使うため、ホストのソケットと共有（後述）\n      - DOCKER_EXTRA_HOSTS=${DOCKER_EXTRA_HOSTS} # GitLabのコンテナレジストリ名前解決用設定 e.g. gitlab-ci.example.com:192.168.11.6\n    volumes:\n      - ./etc/gitlab-runner/config:/etc/gitlab-runner # 設定が ./etc/gitlab-runner/config/config.toml に永続保存される。\n      - /var/run/docker.sock:/var/run/docker.sock # ホストのソケットと共有（上記のDOCKER_VOLUMES設定に関連）\n      - ./certs:/etc/gitlab-runner/certs # Gitlab Runnerが使う証明書置き場\n```\n\n<br />\n\n`environment:` のところは、この後、GitLab に登録するときに使う、`gitlab-runner register` コマンドのオプションです。  \n通常、`gitlab-runner register` に対話モードで回答したり、以下のようにコマンドラインオプションにしたりすると思いますが、ここでは、`docker-compose.yml` の `environment:` に指定します。\n\n<br />\n\ngitlab-runner registerのオプション指定例：\n\n```shellsession\n$ sudo gitlab-runner register \\\n  --non-interactive \\\n  --url \"https://gitlab.com/\" \\\n  --registration-token \"PROJECT_REGISTRATION_TOKEN\" \\\n・・・\n```\n\n<br />\n\ndocker run＆gitlab-runner registerのオプション指定例：\n\n```shellsession\n$ docker run --rm -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \\\n  --non-interactive \\\n  --executor \"docker\" \\\n  --docker-image alpine:latest \\\n・・・\n```\n\n<br />\n\n.env に値を記述します。\n\n```shellsession\n# vi .env\n```\n\n```sh\nCI_SERVER_URL=https://gitlab-ci.example.com\nREGISTRATION_TOKEN=\"GR1348941qvyWnPYzWbj7-2rFkdyB\"\nRUNNER_TAG_LIST=runner1\nRUNNER_NAME=\"GitLab CI/CD Sample Runner\"\nDOCKER_EXTRA_HOSTS=gitlab-ci.example.com:192.168.11.6\n```\n\n**●`REGISTRATION_TOKEN`** は、GUI に管理者権限でログインして、プロジェクト（リポジトリ）の画面から  \nSettings → CI/CD → Runners → Set up a specific runner for a project に表示されている文字列です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image6.png\" alt=\"REGISTRATION_TOKEN 表示\" width=\"1209\" height=\"549\" loading=\"lazy\"></a>\n\n<br />\n\n**●`DOCKER_EXTRA_HOSTS`** は、docker 内から GitLab を名前解決するのに必要です。  \n<span style=\"color: red;\">注意点として、<code>DOCKER_EXTRA_HOSTS</code> を 127.0.0.1 ではなく、IP アドレスで記述する必要がありました。</span>\n\n<br />\n\n**●`RUNNER_EXECUTOR=docker`**  \nExecutor とは、Runner のジョブ実行方式のことになります。\n\n<br />\n\n**`Executor=docker` ：**  \nDocker コンテナを使って、ジョブを実行していきます。今回、これでやっていきます。  \ndocker コマンドを使えれば良いため、`DOCKER_IMAGE=docker:stable` です。\n\n<br />\n\n**`Executor=Shell` ：**  \nRunner が起動している環境のシェルを使ってジョブを実行します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor1.png\" alt=\"GitLab CI/CD Executor=Shell 図\" width=\"356\" height=\"312\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n**`Executor=SSH` ：**\nRunner が起動している環境から SSH 接続してジョブを実行します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor2.png\" alt=\"GitLab CI/CD Executor=SSH 図\" width=\"601\" height=\"252\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n※図は、あくまでイメージで、今回の環境に即して、ホストが Ubuntu、Runner が Docker 内になっていますが、そうとは限りません。Executor は、他にもありますが、省略します。\n\n<br />\n\n**●`DOCKER_VOLUMES=/var/run/docker.sock:/var/run/docker.sock`**  \n<span style=\"color: red;\"><strong>DooD(Docker outside of Docker)</strong></span> でジョブを実行するため、ホスト側の UNIX ソケットを共有します。  \nそれにより、Runner の docker クライアント（docker コマンド）を使って、ホスト側の dockerd を操作できます。  \nRunner の中の docker からホスト側のイメージを見たり、作ったり、削除したりできるということです。  \n今回、これでやっていきます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor3.png\" alt=\"GitLab CI/CD DooD(Docker outside of Docker) 図\" width=\"496\" height=\"336\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>DinD(Docker in Docker)</strong></span> というのもあります。  \nその場合、Runner の docker クライアント（docker コマンド）を使って、Runner の中にあるコンテナを操作します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/executor4.png\" alt=\"GitLab CI/CD DinD(Docker in Docker) 図\" width=\"497\" height=\"437\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\">Runner が使う証明書を配置します。</span>\n\n```shellsession\n# mkdir certs\n# cp -p /etc/docker/certs.d/gitlab-ci.example.com:5005/ca.crt certs/\n```\n\n<br />\n\nRunner を起動します。\n\n```shellsession\n# docker-compose up -d\n```\n\n<br />\n\n現時点では、単に Runner が起動しただけで、<span style=\"color: red;\"><strong>GitLab に Runner として登録されていませんので、`gitlab-runner register` コマンドで登録します。</strong></span>\n\n<br />\n\nまず、`gitlab-runner register` コマンドで `https://gitlab-ci.example.com/api/v4/runners` にアクセスが行きますので、hosts に登録します。\n\n```shellsession\n# vi /etc/hosts\n```\n\n```sh\n192.168.11.6 gitlab-ci.example.com\n```\n\n<span style=\"color: red;\">注意点として、127.0.0.1 ではなく、IP アドレスで記述する必要がありました。</span>\n\n<br />\n\nRunner を GitLab に登録します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd7.png\" alt=\"GitLab CI/CD RunnerをGitLabに登録 図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n```shellsession\n# docker-compose exec runner gitlab-runner register --non-interactive\n```\n\n<br />\n\nRunner のコンテナ内で gitlab-runner register コマンドを起動しています。  \n`docker-compose exec runner` の `runner` は、`docker-compose.yml` に書いてある名前です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image7.png\" alt=\"runner は、docker-compose.yml に書いてある名前\" width=\"753\" height=\"176\" loading=\"lazy\"></a>\n\n<br />\n\n`docker-compose exec runner gitlab-runner register` だけの場合、Executor などの設定した内容をインタラクティブモード（対話モード）で聞かれますので、`--non-interactive` で聞かれないようにしています。\n\n<br />\n\nプロジェクト → Settings → CI/CD → Runners  \nで確認すると、  \nAvailable specific runners  \nに  \nGitLab CI/CD Sample Runner  \nが追加されています。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image8.png\" alt=\"GitLab CI/CD Sample Runner 追加\" width=\"1209\" height=\"768\" loading=\"lazy\"></a>\n\n<br />\n\n# パイプライン設定\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd8.png\" alt=\"GitLab CI/CD パイプライン設定 図\" width=\"941\" height=\"772\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nCI/CD パイプライン設定は、  \n`.gitlab-ci.yml`  \nに記述します。\n\n<br />\n\nリポジトリに push すれば良いですが、今回は、GUI で登録します。\n\n<br />\n\nリポジトリ画面から  \nCI/CD → Editor → Configure pipeline  \nでとりあえず、そのまま登録してみます。（`echo`と`sleep`しかしていません。）\n\n<br />\n\n...と、その前に、Specific Runner ですので、Runner を指名しないといけません。先ほどセットアップした Runner のタグを runner1 としましたので、全てのジョブに tags で runner1 を指定します。\n\n```yaml\ntags:\n  - runner1\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image9.png\" alt=\"全てのジョブにtagsでrunner1を指定\" width=\"1198\" height=\"813\" loading=\"lazy\"></a>\n\nこのままコミットすると、`.gitlab-ci.yml` に反応して、パイプラインが実行されます。\n\n<br />\n\nView pipeline で見てみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image10.png\" alt=\"View pipeline 画面\" width=\"1201\" height=\"726\" loading=\"lazy\"></a>\n\n<br />\n\nＧｏｏｄ！！\n\n<br />\n\n本番用の設定に更新します。（いきなり長い設定になりますが、説明が必要な場合、コードブロックの下に説明がありますので、スクロールしてください。）\n\n```yaml:.gitlab-ci.yml\nworkflow:\n  rules:\n    - if: $CI_COMMIT_TAG\n      when: never\n    - if: $CI_PIPELINE_SOURCE == \"push\"\n\nvariables:\n  IMAGE_OPENJDK_GRADLE: gradle:7.3.3-jdk17-alpine\n\nstages:\n  - clean\n  - build\n  - test\n  - build-image\n  - publish-image\n  - deploy\n\n.tags_template:\n  tags:\n    - runner1\n\nclean job:\n  extends: .tags_template\n  image: $IMAGE_OPENJDK_GRADLE\n  stage: clean\n  script:\n    - echo \"Cleaning leftovers from previous builds\"\n    - sh $CI_PROJECT_DIR/gradlew clean\n\nbuild job:\n  extends: .tags_template\n  image: $IMAGE_OPENJDK_GRADLE\n  stage: build\n  script:\n    - echo \"Compiling the code...\"\n    - sh $CI_PROJECT_DIR/gradlew bootJar\n    - mkdir -p build/dependency\n    - cd build/dependency\n    - jar -xf ../libs/*.jar\n  artifacts:\n    paths:\n      - build/dependency\n\ntest:\n  extends: .tags_template\n  image: $IMAGE_OPENJDK_GRADLE\n  stage: test\n  script:\n    - echo \"Running unit tests...\"\n    - sh $CI_PROJECT_DIR/gradlew test\n\nbuild image job:\n  extends: .tags_template\n  stage: build-image\n  script:\n    - echo \"Building Docker Image...\"\n    - docker build --build-arg DEPENDENCY=build/dependency -t $CI_REGISTRY_IMAGE:$CI_PIPELINE_IID .\n    - docker build --build-arg DEPENDENCY=build/dependency -t $CI_REGISTRY_IMAGE:latest .\n\n.docker-login: &docker-login-command\n  - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin\n\n.docker-deploy: &docker-deploy-command\n  - docker rm -f spring-boot-kotlin || true\n  - docker run -d --name spring-boot-kotlin -p 8080:8080 --restart always $CI_REGISTRY_IMAGE:latest\n\npublish image job:\n  extends: .tags_template\n  stage: publish-image\n  script:\n    - echo \"Publishing Docker Image...\"\n    - *docker-login-command\n    - docker push $CI_REGISTRY_IMAGE:$CI_PIPELINE_IID\n    - docker push $CI_REGISTRY_IMAGE:latest\n\ndeploy review job:\n  extends: .tags_template\n  stage: deploy\n  script:\n    - echo \"Deploying Review App....\"\n    - *docker-deploy-command\n  environment:\n    name: review\n    url: http://192.168.11.6:8080/api\n\ndeploy production job:\n  stage: deploy\n  script:\n    - echo \"Deploying Production App....\"\n    - *docker-login-command\n    - *docker-deploy-command\n  tags:\n    - runner2\n  environment:\n    name: production\n    url: http://192.168.11.9:8080/api\n  rules:\n    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\n      when: manual\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image11.png\" alt=\".gitlab-ci.ymlの説明\" width=\"929\" height=\"2465\" loading=\"lazy\"></a>\n\nCommit changes でコミットしたら、また CI/CD パイプラインが動き始めます。\n\n<br />\n\nこの時点では、タグ名＝ runner2 の Runner が存在しないため、`deploy production job` を起動すると、エラーになりますが、こうなればひとまず成功です。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image12.png\" alt=\"runner1だけ起動した状態\" width=\"1447\" height=\"512\" loading=\"lazy\"></a>\n\n次は、`deploy production job` も起動できるようにしていきます。\n\n<br />\n\n# Runner 追加\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd9.png\" alt=\"GitLab CI/CD Runner追加 図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nタグ名＝ runner2 の Runner を追加します。引き続き、同じマシンに追加しても良いですが、処理能力がパンクしますので、別の Ubuntu マシンに追加しました。runner1 と同じ手順ですので、細かい説明は省略します。  \n`RUNNER_TAG_LIST=runner2` のところだけ重要で、IP アドレス、証明書関係で対応漏れが無ければＯＫです。\n\n```shellsession\n# apt update\n# apt install -y apt-transport-https ca-certificates curl software-properties-common\n# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n# add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\"\n# apt update\n# apt install -y docker-ce\n# curl -L \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n# chmod +x /usr/local/bin/docker-compose\n# docker-compose --version\ndocker-compose version 1.29.2, build 5becea4c\n# vi /etc/hosts\n```\n\n```sh\n192.168.11.6 gitlab-ci.example.com\n```\n\n```shellsession\n# mkdir /home/admin/gitlab-runner\n# cd /home/admin/gitlab-runner\n# vi .env\n```\n\n```sh\nCI_SERVER_URL=https://gitlab-ci.example.com\nREGISTRATION_TOKEN=\"GR1348941qvyWnPYzWbj7-2rFkdyB\"\nRUNNER_TAG_LIST=runner2\nRUNNER_NAME=\"GitLab CI/CD Sample Runner2\"\nDOCKER_EXTRA_HOSTS=gitlab-ci.example.com:192.168.11.6\n```\n\n```shellsession\n# vi docker-compose.yml\n```\n\n```yaml\nversion: '3'\nservices:\n  runner:\n    image: 'gitlab/gitlab-runner:latest'\n    restart: always\n    environment:\n      - CI_SERVER_URL=${CI_SERVER_URL}\n      - REGISTRATION_TOKEN=${REGISTRATION_TOKEN}\n      - RUNNER_EXECUTOR=docker\n      - RUNNER_TAG_LIST=${RUNNER_TAG_LIST}\n      - RUNNER_NAME=${RUNNER_NAME}\n      - DOCKER_IMAGE=docker:stable\n      - DOCKER_VOLUMES=/var/run/docker.sock:/var/run/docker.sock\n      - DOCKER_EXTRA_HOSTS=${DOCKER_EXTRA_HOSTS}\n    volumes:\n      - ./etc/gitlab-runner/config:/etc/gitlab-runner\n      - /var/run/docker.sock:/var/run/docker.sock\n      - ./certs:/etc/gitlab-runner/certs\n```\n\n```shellsession\n# scp -rp admin@192.168.11.6:/home/admin/gitlab-runner/certs .\n# mkdir -p /etc/docker/certs.d/gitlab-ci.example.com:5005\n# cp -p /home/admin/gitlab-runner/certs/ca.crt /etc/docker/certs.d/gitlab-ci.example.com:5005/ca.crt\n# docker-compose up -d\n# docker-compose exec runner gitlab-runner register --non-interactive\n```\n\n<br />\n\n# パイプライン発動\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/gitlab-cicd10.png\" alt=\"GitLab CI/CD パイプライン発動 図\" width=\"941\" height=\"771\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\ndemo プロジェクトの Kotlin ＋ Spring Boot についての説明は、  \n別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/spring-boot-kotlin-docker\" target=\"_blank\">Kotlin ＋ Spring Boot ＋ Docker ビルド(Gradle, Fat JAR)まで全手順</a>」にありますが、  \nGET /api 　 → 　 Hello world! と返す。  \nPOST /api 　 → 　 POST パラメーターの value の値をそのまま返す。  \nという単純な実装です。\n\n<br />\n\nこれを  \nGET /api 　 → 　 Hello GitLab CI/CD world! と返す。  \nに変更して、  \ndevelop ブランチに push してみます。\n\n<blockquote class=\"warn\">\n<p>テストプログラムの方（<code>src/test/kotlin/com/example/demo/DemoApplicationTests.kt</code>）も変更が必要です。（\"Hello GitLab CI/CD world!\"が返るかどうかテストしている）</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>develop ブランチがいきなり登場しましたが、develop ブランチに開発者が push しているところをイメージしています。ユーザー作成とか、権限調整、ブランチ作成とかの手順は省略しています。</p>\n<p>push で発動して、master のジョブは反応しないということを言いたいから develop ブランチに push です。</p>\n</blockquote>\n\n<br />\n\ndevelop ブランチを push した結果、`deploy production job` はパイプラインにありません。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image13.png\" alt=\"developブランチ パイプライン\" width=\"1415\" height=\"163\" loading=\"lazy\"></a>\n\n以下の条件があり、master ブランチへの push しか有効になっていないからです。\n\n```yaml\nrules:\n  - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\n```\n\n<br />\n\nデプロイされたアプリを動作確認します。\n\n```shellsession\n$ curl http://localhost:8080/api\nHello GitLab CI/CD world!\n```\n\n<br />\n\nヨシ！\n\n<br />\n\nマージリクエストを出して、master へ push します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image14.png\" alt=\"masterへpushしたときのパイプライン\" width=\"950\" height=\"502\" loading=\"lazy\"></a>\n\n<br />\n\nmaster のジョブも動きますが、master のジョブは、\n\n```yaml\nrules:\n  - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\n    when: manual\n```\n\nで `when: manual` のため、手動で実行しないと動きません。\n\n<br />\n\n手動で起動します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image15.png\" alt=\"deploy production job手動起動１\" width=\"1262\" height=\"128\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image16.png\" alt=\"deploy production job手動起動２\" width=\"1447\" height=\"221\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image17.png\" alt=\"deploy production job手動起動３\" width=\"1335\" height=\"781\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/image18.png\" alt=\"deploy production job手動起動４\" width=\"1450\" height=\"218\" loading=\"lazy\"></a>\n\n<br />\n\n正常に終わったのを確認して、デプロイされたアプリを動作確認します。\n\n<br />\n\n```shellsession\n$ curl http://192.168.11.9:8080/api\nHello GitLab CI/CD world!\n```\n\n<br />\n\nヨシ！！\n\n<br />\n","description":"アプリ実装→GitLab CI/CD パイプライン発動→自動テスト／ビルド→自動Docker ビルド→自動デプロイ　を実現する全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"lfchvnmhahxb","createdAt":"2022-06-14T12:17:33.291Z","updatedAt":"2022-06-14T12:17:33.291Z","publishedAt":"2022-06-14T12:17:33.291Z","revisedAt":"2022-06-14T12:17:33.291Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-ci-cd/ITC_Engineering_Blog.png","alt":"Kotlin＋Spring Bootのアプリを GitLab CI/CD によるDockerデプロイまで全手順","width":1200,"height":630}],"seo_authors":[]},{"id":"lua-resty-openidc","createdAt":"2021-12-28T13:04:14.959Z","updatedAt":"2022-01-07T12:54:04.626Z","publishedAt":"2021-12-28T13:04:14.959Z","revisedAt":"2022-01-07T12:54:04.626Z","title":"OpenResty lua-resty-openidc phpでSSO Webアプリ環境作成","category":{"id":"9iy1ks71tv7n","createdAt":"2021-05-31T13:08:18.404Z","updatedAt":"2021-08-31T12:04:47.612Z","publishedAt":"2021-05-31T13:08:18.404Z","revisedAt":"2021-08-31T12:04:47.612Z","topics":"Nginx","logo":"/logos/Nginx.png","needs_title":false},"topics":[{"id":"9iy1ks71tv7n","createdAt":"2021-05-31T13:08:18.404Z","updatedAt":"2021-08-31T12:04:47.612Z","publishedAt":"2021-05-31T13:08:18.404Z","revisedAt":"2021-08-31T12:04:47.612Z","topics":"Nginx","logo":"/logos/Nginx.png","needs_title":false},{"id":"ab6-jq_sti-0","createdAt":"2021-12-28T12:15:41.009Z","updatedAt":"2021-12-28T12:15:41.009Z","publishedAt":"2021-12-28T12:15:41.009Z","revisedAt":"2021-12-28T12:15:41.009Z","topics":"Lua","logo":"/logos/Lua.gif","needs_title":false},{"id":"lgiabhpmz","createdAt":"2021-11-25T13:17:57.984Z","updatedAt":"2021-11-25T13:17:57.984Z","publishedAt":"2021-11-25T13:17:57.984Z","revisedAt":"2021-11-25T13:17:57.984Z","topics":"OpenID Connect","logo":"/logos/OpenIDConnect.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false}],"content":"# はじめに\n\nOpenResty と lua-resty-openidc を使って、Apache ではなく、Nginx 系のシングルサインオン（SSO）の Web アプリ動作環境を作成しましたので、その手順を紹介します。構成は、以下です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/OpenResty-lua-resty-openidc.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/OpenResty-lua-resty-openidc.png\" alt=\"OpenResty と lua-resty-openidc 構成図\" width=\"702px\" height=\"542px\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nSSO は、OpenID Connect の仕組みを利用し、OpenID Connect の OpenID Provider(OP)/Identity Provider(IdP)は、GitLab を利用します。<span style=\"color: red;\">GitLab を OpenID Provider として利用する手順は、以下の別記事を参考にしてください。この記事では、GitLab 側の説明は省略します。</span>  \n「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-as-openid-provider\" target=\"_blank\">GitLab as OpenID Connect identity provider をやってみた</a>」\n\n<blockquote class=\"warn\">\n<p>SSO、OpenID Connect とは何かの説明は省略します。</p>\n</blockquote>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS（今回作業する側）</code></p>\n<p>　<code>nginx openresty/1.19.9.1</code></p>\n<p>　<code>PHP 8.0.14</code></p>\n<p><code>Ubuntu 20.04.2 LTS（OpenID Provider側）</code></p>\n<p>　<code>GitLab v13.11.2</code></p>\n</blockquote>\n\n<br />\n\n# 用語紐解き\n\nOpenID Connect の Relying Party(RP)として、これまでの記事では、mod_auth_openidc を使ってきました。  \nmod_auth_openidc は、Apache 系のモジュールですので、lua-resty-openidc を使えば nginx 系で Relying Party(RP)の機能が実現できることが分かりました。  \n備忘録としても、こうなった経緯と、用語を一旦整理します。\n\n<br />\n\n**● 経緯**  \nnginx でも mod_auth_openidc みたいなことをしたい。  \n↓  \nlua-resty-openidc を使おう！  \n↓  \nlua-resty-openidc の Lua とは、プログラミング言語のこと。  \n↓  \nlua-resty-openidc は、Lua で OpenID Connect の処理が書いてある nginx に組み込むライブラリ  \n↓  \nlua-resty-openidc の公式サイトでは「いろいろ依存しているけど、OpenResty を使えば悩むこと無いよ。」と言っている。  \n↓  \nOpenResty は、Lua 実行環境等いろいろ組み込まれた nginx のこと。（ビルドで同じようなものを構築できない事はない。）  \n↓  \nlua-resty-openidc は、Lua パッケージ管理システムで管理されている。  \n↓  \nパッケージ管理システムは、OPM と LuaRocks がある。  \n↓  \nOpenResty 公式サイトでは、「OpenResty は独自のパッケージマネージャーである OPM を提供しているため、OpenResty で LuaRocks を使用することは強くお勧めしません。」と言っている。  \n↓  \nパッケージ管理システムは、OPM に決まり。  \n↓  \n`opm install zmartzone/lua-resty-openidc`  \nだけで依存関係を自動で解消しつつ、lua-resty-openidc をインストール。  \nビルドしなくて良かった...。  \n\n<br />\n\n**● 用語**  \n\n<blockquote class=\"info\">\n<p><strong>【 lua-resty-openidc 】</strong></p>\n<p>lua-resty-openidc は、OpenID Connect Relying Party（RP）、OAuth 2.0 Resource Server（RS）機能を実装する Nginx 用のライブラリです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 Lua 】</strong></p>\n<p>Lua（ルア）はスクリプト言語およびその処理系のことです。</p>\n<p>手続き型言語として、またプロトタイプベースのオブジェクト指向言語としても利用することができ、関数型言語としての要素も併せ持っています。</p>\n<p>Lua は、C 言語のホストプログラムに組み込まれることを目的に設計されており、高速な動作と、高い移植性、組み込みの容易さが特徴です。</p>\n<p>コルーチン（coroutine）を持っています。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 コルーチン（coroutine） 】</strong></p>\n<p>コルーチンとはいったん処理を中断した後、続きから処理を再開できるプログラミングの構造のことです。※Lua だけの用語ではないです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 OpenResty 】</strong></p>\n<p>OpenResty は、lua-nginx-module を始めとする多数のサードパーティモジュールを内包した Nginx であり、Web プラットフォームです。</p>\n<p>既存の Nginx C モジュールと Lua モジュールを使用して、高性能な Web アプリケーションを構築できます。</p>\n<p>LuaJIT エンジンを使用して Lua スクリプトを実行できます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 lua-nginx-module 】</strong></p>\n<p>C 言語で書かれた Nginx 拡張機能です。lua-nginx-module を使うと Lua のコードを通して Nginx を制御できます。Nginx 設定ファイルで扱える変数の定義/読み書きや、HTTP リクエストの操作、mysqld,memcached,redis といったストレージを組み合わせて使う方法もあります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 LuaJIT 】</strong></p>\n<p>LuaJIT は、Lua プログラミング言語用の Just-In-Time コンパイラ（JIT）です。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 JIT 】</strong></p>\n<p>Just-In-Time Compiler 　実行時コンパイラのことです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 LuaRocks 】</strong></p>\n<p>Lua プログラミング言語のパッケージマネージャーです。HTTP 通信機能など、Lua で作られた追加機能を簡単にインストールできます。node で言う npm、Python で言う pip、Perl で言う CPAN のようなものです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 OPM 】</strong></p>\n<p>OpenResty Package Manager の略称です。OpenResty 公式の Lua プログラミング言語のパッケージマネージャーです。</p>\n</blockquote>\n\n<br />\n\n**●lua-resty-openidc の依存関係**  \nlua-resty-openidc は、以下に依存しています。OpenResty をインストールし、opm で lua-resty-openidc をインストールすると、自動的に依存関係は解決されます。  \n\n・`Nginx`  \n\n<br />\n\n・`ngx_devel_kit`  \nC 言語で書かれた Nginx 拡張機能です。Nginx モジュール開発者が作成する必要のあるコードを削減するために設計されたものです。  \n\n<br />\n\n・`Lua or LuaJIT`  \nLua の実行時コンパイラです。  \n\n<br />\n\n・`lua-nginx-module`  \n上記に説明があります。  \n\n<br />\n\n・`lua-cjson`  \nLua で json を扱うための Lua パッケージです。  \n\n<br />\n\n・`lua-resty-string`  \nString ユーティリティとハッシュ関数の Lua パッケージです。  \n例えば、整数値の文字列型データを int 型の数値データに変換するときに使う関数 atoi やハッシュ生成の md5 などが実装されています。\n\n<br />\n\n・`lua-resty-http`  \nHTTP クライアント Lua パッケージです。  \n\n<br />\n\n・`lua-resty-session`  \nセッション管理を行う Lua パッケージです。  \n\n<br />\n\n・`lua-resty-jwt`  \nJWT を扱う処理を実装した Lua パッケージです。  \n\n<blockquote class=\"info\">\n<p>lua-resty-jwt に関して、公式ページには、トークンの検証に\"remote introspection\"を使った OAuth 2.0 Resource Server の場合、必須ではないと書かれています。OPM では自動でインストールされます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 JWT 】</strong></p>\n<p>JSON Web Token の略称です。認証情報などの属性情報（Claim）が JSON データ構造で収められてるのをトークンと言いますが、その仕様のことです。</p>\n</blockquote>\n\n<br />\n\n# OpenResty インストール\nタイムゾーンを Asia/Tokyo、LANG=ja_JP.UTF8 にします。（必須ではありません。）  \n\n```shellsession\n# timedatectl set-timezone Asia/Tokyo\n# apt install -y language-pack-ja\n# update-locale LANG=ja_JP.UTF8\n```\n\n<br />\n\nOpenResty をインストールします。  \n\n```shellsession\n# apt update\n# apt -y install --no-install-recommends wget gnupg ca-certificates\n# wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -\n```\n\n`--no-install-recommends`はお勧めで入れておいた方が良い apt もインストールされるのを防ぎます。  \n`ca-certificates`は、`https://`で`wget`するために必要です。（最初から入っている可能性が高いです。）  \n`gnupg`は、暗号化ソフトで、無しでも問題無かったのですが、<a href=\"https://blog.openresty.com/en/ubuntu20-or-install/\" target=\"_blank\">公式ブログの手順</a>によると、入れているので、入れておきます。  \n`wget -O -`により、ファイルの主力先を標準出力（-O の後のマイナス記号）とし、パイプで、直接 apt-key に渡しています。  \n`apt-key add`により、`openresty`を信頼された apt パッケージとして登録します。\n\n```shellsession\n# echo \"deb http://openresty.org/package/ubuntu $(lsb_release -sc) main\" > openresty.list\n# cp openresty.list /etc/apt/sources.list.d/\n# apt update\n# apt -y install --no-install-recommends openresty\n# which openresty\n/usr/bin/openresty\n# file `which openresty`\n/usr/bin/openresty: symbolic link to ../local/openresty/nginx/sbin/nginx\n# openresty -V\nnginx version: openresty/1.19.9.1\nbuilt with OpenSSL 1.1.1k  25 Mar 2021 (running with OpenSSL 1.1.1l  24 Aug 2021)\nTLS SNI support enabled\nconfigure arguments: --prefix=/usr/local/openresty/nginx --with-cc-opt='-O2 -DNGX_LUA_ABORT_AT_PANIC -I/usr/local/openresty/zlib/include -I/usr/local/openresty/pcre/include -I/usr/local/openresty/openssl111/include' --add-module=../ngx_devel_kit-0.3.1 --add-module=../echo-nginx-module-0.62 --add-module=../xss-nginx-module-0.06 --add-module=../ngx_coolkit-0.2 --add-module=../set-misc-nginx-module-0.32 --add-module=../form-input-nginx-module-0.12 --add-module=../encrypted-session-nginx-module-0.08 --add-module=../srcache-nginx-module-0.32 --add-module=../ngx_lua-0.10.20 --add-module=../ngx_lua_upstream-0.07 --add-module=../headers-more-nginx-module-0.33 --add-module=../array-var-nginx-module-0.05 --add-module=../memc-nginx-module-0.19 --add-module=../redis2-nginx-module-0.15 --add-module=../redis-nginx-module-0.3.7 --add-module=../ngx_stream_lua-0.0.10 --with-ld-opt='-Wl,-rpath,/usr/local/openresty/luajit/lib -L/usr/local/openresty/zlib/lib -L/usr/local/openresty/pcre/lib -L/usr/local/openresty/openssl111/lib -Wl,-rpath,/usr/local/openresty/zlib/lib:/usr/local/openresty/pcre/lib:/usr/local/openresty/openssl111/lib' --with-pcre-jit --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_v2_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --with-http_stub_status_module --with-http_realip_module --with-http_addition_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-http_gzip_static_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-threads --with-stream --with-http_ssl_module\n# /usr/local/openresty/luajit/bin/luajit -v\nLuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2021 Mike Pall. https://luajit.org/\n```\n\n`--add-module=../ngx_devel_kit-0.3.1`  \n`--add-module=../ngx_lua-0.10.20`  \n` --with-http_ssl_module`  \nにより、lua-resty-openidcの要件に含まれるものが組み込まれたnginxと分かります。  \n`LuaJIT`もインストールされています。\n\n<br />\n\n`openresty`を起動します。\n\n```shellsession\n# systemctl start openresty\n# ps aux|grep nginx\nroot        9604  0.0  0.0  11796  1364 ?        Ss   14:25   0:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx -g daemon on; master_process on;\nnobody      9605  0.0  0.1  12476  4028 ?        S    14:25   0:00 nginx: worker process\nroot        9915  0.0  0.0  18720   724 pts/0    S+   14:26   0:00 grep --color=auto nginx\n```\n\nopenresty サービス＝`/usr/local/openresty/nginx/sbin/nginx`と分かります。\n\n```shellsession\n# apt -y install curl\n# curl 127.0.0.1/\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n  <head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <title>Apache2 Debian Default Page: It works</title>\n・・・（略）・・・\n```\n\nnginx は正常に起動しています。  \n\n<br />\n\n続いて、`resty`コマンドをインストールします。  \n`resty`コマンドは、Lua のスクリプトを直接起動できるコマンドユーティリティ（Resty CLI）です。必須ではありません。\n\n```shellsession\n# apt -y install openresty-resty\n# which resty\n/usr/bin/resty\n# resty -e 'print(\"Hello Resty\")'\nHello Resty\n```\n\n<br />\n\nパッケージ管理の`opm`コマンドをインストールします。\n\n```shellsession\n# apt -y install openresty-opm\n# opm list\n```\n\n最初、パッケージは、何も入っていません。  \n`lua-resty-openidc`パッケージをインストールします。\n\n```shellsession\n# opm install zmartzone/lua-resty-openidc\n# opm list\nledgetech/lua-resty-http       0.16.1\ncdbattags/lua-resty-jwt        0.2.0\nzmartzone/lua-resty-openidc    1.7.5\nopenresty/lua-resty-string     0.11\njkeys089/lua-resty-hmac        0.06\nbungle/lua-resty-session       3.8\n```\n\n<span style=\"color:red;\"><strong>`lua-resty-openidc`の依存パッケージも適切なバージョンで同時にインストールされます。</strong></span>\n\n<br />\n\n# php インストール\n\nWebアプリ実行用に、php8をインストールします。（今回の記事の場合、 Webアプリ＝phpinfo()実行 だけです。）  \n\n<blockquote class=\"info\">\n<p>「<a href=\"https://itc-engineering-blog.netlify.app/blogs/i-hna4_wx\" target=\"_blank\">Ubuntu 20.04.2.0 に apache2,php,postgresql をインストール</a>」に詳しい説明付きの手順がありますので、詳しい説明は、端折ります。</a>\n</blockquote>\n\n```shellsession\n# apt update\n# add-apt-repository ppa:ondrej/php\n# apt update\n# apt -y install php8.0 php8.0-gd php8.0-mbstring php8.0-common php8.0-curl\n# php -v\nPHP 8.0.14 (cli) (built: Dec 20 2021 21:22:57) ( NTS )\n```\n\napache2 が同時インストールされますが、今回要らないため、削除します。  \n\n```shellsession\n# apt list --installed | grep apache2\n\nWARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n\napache2-bin/focal-updates,now 2.4.41-4ubuntu3.8 amd64 [インストール済み、自動]\napache2-data/focal-updates,focal-updates,now 2.4.41-4ubuntu3.8 all [インストール済み、自動]\napache2-utils/focal-updates,now 2.4.41-4ubuntu3.8 amd64 [インストール済み、自動]\napache2/focal-updates,now 2.4.41-4ubuntu3.8 amd64 [インストール済み、自動]\nlibapache2-mod-php8.0/focal,now 8.0.14-1+ubuntu20.04.1+deb.sury.org+1 amd64 [インストール済み、自動]\n# apt -y remove apache2-*\n```\n\n<br />\n\nphp-fpmをインストールします。  \n\n<blockquote class=\"info\">\n<p>php-fpm については、「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-nginx-php\" target=\"_blank\">GitLab バンドル nginx を利用して php の独自 Web アプリを同居させる手順</a>」に詳しい説明付きの手順がありますので、詳しい説明は、端折ります。</p>\n</blockquote>\n\n```shellsession\n# apt install -y php-fpm\n# vi /etc/php/8.0/fpm/pool.d/www.conf\nlisten = /run/php/php8.0-fpm.sock\n# vi /usr/local/openresty/nginx/conf/fastcgi_params\n```\n\n<br />\n\n`SCRIPT_FILENAME`を追加します。追加しないと、PHP FPM から見たスクリプトの場所が分からなくなり、以下のエラーになります。  \n<span style=\"color: red;\">`[error] 14457#14457: *4 FastCGI sent in stderr: \"Primary script unknown\" while reading response header from upstream, client: 192.168.11.5, server: webapp.example.com, request: \"GET /info.php HTTP/1.1\", upstream: \"fastcgi://unix:/run/php/php8.0-fpm.sock:\", host: \"webapp.example.com\"`</span>  \n（画面の表示は、「File not found.」）\n\n```nginx\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\n↓\nfastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\n```\n\n<br />\n\n`openresty`が`nobody:nogroup`で起動するため、nobody:nogroup でドキュメントルートを作成しておきます。\n\n```shellsession\n# mkdir -p /opt/webapp/www/html\n# chown -R nobody:nogroup /opt/webapp\n# mkdir /var/log/webapp\n```\n\nphp-fpm のユーザーも`nobody:nogroup`に合わせます。  \n合わせないと、以下のエラーになります。  \n<span style=\"color: red;\">`[crit] 14457#14457: *1 connect() to unix:/run/php/php8.0-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 192.168.11.5, server: webapp.example.com, request: \"GET /info.php HTTP/1.1\", upstream: \"fastcgi://unix:/run/php/php8.0-fpm.sock:\", host: \"webapp.example.com\"`</span>  \n（画面の表示は、「502 Bad Gateway」）\n\n```shellsession\n# vi /etc/php/8.0/fpm/pool.d/www.conf\n```\n\n```nginx\nuser = nobody\ngroup = nogroup\nと\nlisten.owner = nobody\nlisten.group = nogroup\n```\n\n<br />\n\nWebアプリ の nginx conf を作成します。  \nここでは、ホスト名を`webapp.example.com`  \nドキュメントルートを`/opt/webapp/www/html`  \nとします。\n\n```shellsession\n# vi /usr/local/openresty/nginx/conf/webapp.conf\n```\n\n```nginx\nserver\n{\n  listen 80;\n  server_name webapp.example.com;\n  access_log /var/log/webapp/access.log;\n  error_log /var/log/webapp/error.log;\n\n  root /opt/webapp/www/html;\n\n  location /\n  {\n    index index.html index.htm index.php;\n  }\n\n  location ~ [^/]\\.php(/|$)\n  {\n    fastcgi_split_path_info ^(.+?\\.php)(/.*)$;\n    if (!-f $document_root$fastcgi_script_name)\n    {\n      return 404;\n    }\n\n    client_max_body_size 100m;\n\n    # Mitigate https://httpoxy.org/ vulnerabilities\n    fastcgi_param HTTP_PROXY \"\";\n\n    # fastcgi_pass 127.0.0.1:9000;\n    fastcgi_pass unix:/run/php/php8.0-fpm.sock;\n    fastcgi_index index.php;\n\n    # include the fastcgi_param setting\n    include fastcgi_params;\n\n    # SCRIPT_FILENAME parameter is used for PHP FPM determining\n    #  the script name. If it is not set in fastcgi_params file,\n    # i.e. /etc/nginx/fastcgi_params or in the parent contexts,\n    # please comment off following line:\n    # fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;\n  }\n}\n```\n\n<br />\n\nWebアプリ の conf を include します。（`include webapp.conf`行追加）\n\n```shellsession\n# vi /usr/local/openresty/nginx/conf/nginx.conf\n```\n\n```nginx\n・・・（略）・・・\n    #    location / {\n    #        root   html;\n    #        index  index.html index.htm;\n    #    }\n    #}\n\ninclude webapp.conf;\n}\n```\n\n<br />\n\nphp.ini の調整（任意）と自ホストの名前解決ができない場合、hosts の調整を行います。\n\n```shellsession\n# vi /etc/php/8.0/fpm/php.ini\n```\n\n```sh\ndisplay_errors = On\ndisplay_startup_errors = On\n```\n\n```shellsession\n# vi /etc/hosts\n```\n\n```sh\n192.168.12.200 webapp.example.com\n```\n\n<br />\n\nopenrestyとphp-fpmを再起動します。  \n\n```shellsession\n# systemctl restart openresty\n# systemctl restart php8.0-fpm\n```\n\n<br />\n\nドキュメントルートに`phpinfo()`を出力させる info.php を配置して、動作確認します。（この時点では、Open ID Connect の SSO 認証無しです。）\n\n```shellsession\n# vi /opt/webapp/www/html/info.php\n```\n\n```php\n<?php\n  phpinfo();\n```\n\n`http://webapp.example.com/info.php`にアクセスします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image1.png\" alt=\"webapp.example.com/info.php\" width=\"1002\" height=\"344\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！\n\n<br />\n\n# https 化 openidc 設定追加\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\">OpenID Provider 側の説明は省略します。ここでは、以下を実施済みとします。</span></p>\n<p>「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-as-openid-provider\" target=\"_blank\">GitLab as OpenID Connect identity provider をやってみた</a>」</p>\n</blockquote>\n\n<br />\n\nWebアプリ サーバーを`http://`→`https://`に変更します。  \n\n<br />\n\n自己署名証明書を作成します。\n\n<blockquote class=\"info\">\n<p>自己署名証明書作成については、「<a href=\"https://itc-engineering-blog.netlify.app/blogs/sslcert\" target=\"_blank\">CentOS8 & Apache の自己署名証明書作成と証明書エラー回避</a>」に詳しい説明付きの手順がありますので、詳しい説明は、端折ります。</p>\n</blockquote>\n\n```shellsession\n# openssl genrsa -out ca.key 2048\n# openssl req -new -key ca.key -out ca.csr\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:webapp.example.com\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\nAn optional company name []:\n# echo \"subjectAltName=DNS:*.example.com,IP:192.168.12.200\" > san.txt\n# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt -extfile san.txt\nSignature ok\nsubject=C = JP, ST = Aichi, L = Toyota, O = Default Company Ltd, CN = webapp.example.com\nGetting Private key\n# mkdir -p /etc/pki/tls/certs\n# mkdir /etc/pki/tls/private\n# cp ca.crt /etc/pki/tls/certs/ca.crt\n# cp ca.key /etc/pki/tls/private/ca.key\n# cp ca.csr /etc/pki/tls/private/ca.csr\n```\n\n<br />\n\nGitLab as OpenID Connect identity provider の URL を名前解決するため、hosts の調整を行います。\n\n```shellsession\n# vi /etc/hosts\n```\n\n```sh\n192.168.12.200 gitlab-test.itccorporation.jp\n```\n\n<br />\n\n`webapp.conf`の設定を変更します。\n\n```shellsession\n# vi /usr/local/openresty/nginx/conf/webapp.conf\n```\n\n```nginx\nresolver 127.0.0.53 ipv6=off;\nlua_package_path '$prefixlua/?.lua;;';\nlua_ssl_trusted_certificate /etc/pki/tls/certs/ca.crt;\n\nserver\n{\n  #listen 80;\n  listen 443 ssl;\n  ssl_certificate /etc/pki/tls/certs/ca.crt;\n  ssl_certificate_key /etc/pki/tls/private/ca.key;\n  server_name webapp.example.com;\n  access_log /var/log/webapp/access.log;\n  error_log /var/log/webapp/error.log;\n\n  root /opt/webapp/www/html;\n  index index.html index.htm index.php;\n  access_by_lua_block {\n    local opts = {\n      ssl_verify = \"no\",\n      scope = \"openid email\",\n      session_contents = {id_token=true},\n      use_pkce = true,\n      redirect_uri = \"https://webapp.example.com/oidc_callback\",\n      discovery = \"https://gitlab-test.itccorporation.jp/.well-known/openid-configuration\",\n      client_id = \"48b10a860b20a2eec4a7682bb88fd51fd786e78d6d84a0aabe4beafce9b50952\",\n      client_secret = \"1aa56da3e07bbc7779406856023c4c5b34371bc64fef8d8a386aef64cc04eba1\",\n    }\n    local res, err = require(\"resty.openidc\").authenticate(opts)\n\n    if err then\n      ngx.status = 500\n      ngx.say(err)\n      ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)\n    end\n\n    ngx.req.set_header(\"X-USER\", res.id_token.email)\n  }\n\n  location ~ [^/]\\.php(/|$)\n  {\n    fastcgi_split_path_info ^(.+?\\.php)(/.*)$;\n    if (!-f $document_root$fastcgi_script_name)\n    {\n      return 404;\n    }\n\n    client_max_body_size 100m;\n\n    # Mitigate https://httpoxy.org/ vulnerabilities\n    fastcgi_param HTTP_PROXY \"\";\n\n    # fastcgi_pass 127.0.0.1:9000;\n    fastcgi_pass unix:/run/php/php8.0-fpm.sock;\n    fastcgi_index index.php;\n\n    # include the fastcgi_param setting\n    include fastcgi_params;\n\n    # SCRIPT_FILENAME parameter is used for PHP FPM determining\n    #  the script name. If it is not set in fastcgi_params file,\n    # i.e. /etc/nginx/fastcgi_params or in the parent contexts,\n    # please comment off following line:\n    # fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;\n  }\n}\n```\n\n**【webapp.conf追加部分説明】**\n\n```nginx\nresolver 127.0.0.53 ipv6=off;\n```\n\n`discovery = \"https://gitlab-test.itccorporation.jp/.well-known/openid-configuration\"`部分で<span style=\"color: red;\"><strong>/etc/hosts に書いただけでは、名前解決されず、Ubuntu のデフォルトの DNS サーバー`127.0.0.53`の設定が必要でした。さらに、ipv6=off にしないと、ipv6 で名前解決しようとしてエラーになりました。</strong></span>  \nエラー内容：  \n<span style=\"color: red;\">`[error] 1892#1892: *9 [lua] openidc.lua:577: openidc_discover(): accessing discovery url (https://gitlab-test.itccorporation.jp/.well-known/openid-configuration) failed: no resolver defined to resolve \"gitlab-test.itccorporation.jp\", client: 192.168.11.5, server: webapp.example.com, request: \"GET / HTTP/1.1\", host: \"webapp.example.com\"`</span>\n\n<br />\n\n```nginx\nlua_package_path '$prefixlua/?.lua;;';\n```\n\nlua パッケージ読み取り先パスの設定です。今回の場合、`require(\"resty.openidc\")`のところが読み取っているところです。  \n最初、どこかのサイトに書いてあったのを真似して、  \n`'./lua/?.lua;;';`  \nあるいは、  \n`'~/lua/?.lua;;';`  \nと設定して、  \n<span style=\"color: red;\"><strong>どこかからの相対パス＋標準パスのつもりで書きましたが、`./`や`~/`は、どこにも該当しませんでした。</strong></span>  \n`lua_package_path '$prefixlua/?.lua;;';`  \nの場合、  \n`$prefix`が`/usr/local/openresty/nginx/`に置き換わり、  \n`/usr/local/openresty/nginx/lua/*.lua`  \nが読み取られるようになりました。  \n<span style=\"color: red;\">`?`の部分は、読み取りたいパッケージ名に置き換わります。（`*`のような意味です。）</span>  \n<span style=\"color: red;\">なお、\";;\"はデフォルトの読み取り先を意味し、誤植ではありません。</span>  \n今回のインストールでは、以下がデフォルトの読み取り先のようでした。\n\n```sh\n/usr/local/openresty/site/lualib/resty/openidc.ljbc\n/usr/local/openresty/site/lualib/resty/openidc/init.ljbc\n/usr/local/openresty/lualib/resty/openidc.ljbc\n/usr/local/openresty/lualib/resty/openidc/init.ljbc\n/usr/local/openresty/site/lualib/resty/openidc.lua\n/usr/local/openresty/site/lualib/resty/openidc/init.lua\n/usr/local/openresty/lualib/resty/openidc.lua\n/usr/local/openresty/lualib/resty/openidc/init.lua\n./resty/openidc.lua\n/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/resty/openidc.lua\n/usr/local/share/lua/5.1/resty/openidc.lua\n/usr/local/share/lua/5.1/resty/openidc/init.lua\n/usr/local/openresty/luajit/share/lua/5.1/resty/openidc.lua\n/usr/local/openresty/luajit/share/lua/5.1/resty/openidc/init.lua\n/usr/local/openresty/site/lualib/resty/openidc.so\n/usr/local/openresty/lualib/resty/openidc.so\n./resty/openidc.so\n/usr/local/lib/lua/5.1/resty/openidc.so\n/usr/local/openresty/luajit/lib/lua/5.1/resty/openidc.so\n/usr/local/lib/lua/5.1/loadall.so\n/usr/local/openresty/site/lualib/resty.so\n/usr/local/openresty/lualib/resty.so\n./resty.so\n/usr/local/lib/lua/5.1/resty.so\n/usr/local/openresty/luajit/lib/lua/5.1/resty.so\n/usr/local/lib/lua/5.1/loadall.so\n```\n\n`$prefixlua/?.lua`部分の設定は必要ありません。そもそも、`lua_package_path`をあえて設定する必要はありません。\n\n<br />\n\n```nginx\nlua_ssl_trusted_certificate /etc/pki/tls/certs/ca.crt;\n```\n\nLua から`https://`にアクセスするための証明書置き場の設定です。（今回、証明書エラーを無視するため、適当に指定しています。）\n\n<br />\n\n```nginx\n  #listen 80;\n  listen 443 ssl;\n  ssl_certificate /etc/pki/tls/certs/ca.crt;\n  ssl_certificate_key /etc/pki/tls/private/ca.key;\n```\n\n`https://`で接続されるための設定です。\n\n<br />\n\n```nginx\n  access_by_lua_block {\n    local opts = {\n      ssl_verify = \"no\",\n      scope = \"openid email\",\n      session_contents = {id_token=true},\n      use_pkce = true,\n      redirect_uri = \"https://webapp.example.com/oidc_callback\",\n      discovery = \"https://gitlab-test.itccorporation.jp/.well-known/openid-configuration\",\n      client_id = \"48b10a860b20a2eec4a7682bb88fd51fd786e78d6d84a0aabe4beafce9b50952\",\n      client_secret = \"1aa56da3e07bbc7779406856023c4c5b34371bc64fef8d8a386aef64cc04eba1\",\n    }\n    local res, err = require(\"resty.openidc\").authenticate(opts)\n\n    if err then\n      ngx.status = 500\n      ngx.say(err)\n      ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)\n    end\n\n    ngx.req.set_header(\"X-USER\", res.id_token.email)\n  }\n```\n\nopts の`ssl_verify = \"no\",`は、  \n`discovery = \"https://gitlab-test.itccorporation.jp/.well-known/openid-configuration\",`の URL が自己署名証明書のサイトのため、自己署名でも構わず接続するために必要です。設定しない場合、以下のエラーになります。  \n<span style=\"color: red;\">`[error] 1884#1884: *3 [lua] openidc.lua:577: openidc_discover(): accessing discovery url (https://gitlab-test.itccorporation.jp/.well-known/openid-configuration) failed: 18: self signed certificate, client: 192.168.11.5, server: webapp.example.com, request: \"GET / HTTP/1.1\", host: \"webapp.example.com\"`</span>\n\n<br />\n\nopts の`scope = \"openid email\",`は、  \nアプリケーションがどこまでの情報（アクセストークン）を要求するかの設定です。  \n今回の場合、GitLab as OpenID Connect identity provider 側で、openid email の要求を許可したとして、この設定になります。\n\n<br />\n\nopts の`session_contents = {id_token=true},`は、  \nセッション情報にどれだけの情報を載せるかの設定です。  \n`id_token, enc_id_token, user, access_token`が指定できるようですが、`id_token`だけあれば良いので、`id_token=true`としています。\n\n<br />\n\nopts の`use_pkce = true,`は、  \nPKCE を有効にします。PKCE は認可コードの横取り攻撃の対策として定義されています。必須ではありません。\n\n<br />\n\nopts の`redirect_uri = \"https://webapp.example.com/oidc_callback\",`は、  \nGitLab as OpenID Connect identity provider 側で設定した、コールバック URL です。\n\n<br />\n\nopts の`client_id = \"48b10a860b20a2eec4a7682bb88fd51fd786e78d6d84a0aabe4beafce9b50952\",`は、  \nGitLab as OpenID Connect identity provider 側で設定した、アプリケーション ID です。\n\n<br />\n\nopts の`client_secret = \"1aa56da3e07bbc7779406856023c4c5b34371bc64fef8d8a386aef64cc04eba1\",`は、  \nGitLab as OpenID Connect identity provider 側で設定した、秘密です。\n\n<br />\n\n`local res, err = require(\"resty.openidc\").authenticate(opts)`は、  \nlua-resty-openidc を読み込んで認証処理を実行しています。\n\n<br />\n\n`if err then`のところは、エラーが発生した場合、500 Internal Server Error を返しています。\n\n<br />\n\n`ngx.req.set_header(\"X-USER\", res.id_token.email)`のところは、認証が成功して id_token から得られた email を X-USER-ヘッダにセットしています。\n\n<br />\n\nopenresty を再起動して、動作確認します。\n\n```shellsession\n# systemctl restart openresty\n```\n\n<br />\n\n`https://webapp.example.com/info.php`にアクセスします。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image2.png\" alt=\"https SSO webapp.example.com/info.php １\" width=\"970\" height=\"502\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image3.png\" alt=\"https SSO webapp.example.com/info.php ２\" width=\"1023\" height=\"662\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/image4.png\" alt=\"https SSO webapp.example.com/info.php ３\" width=\"1033\" height=\"663\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！\n","description":"OpenResty と lua-resty-openidc を使って、Apache ではなく、Nginx 系のシングルサインオン（SSO）の Web アプリ動作環境を作成しましたので、その手順を紹介します。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"4m0nfirrz","createdAt":"2021-12-28T13:01:54.852Z","updatedAt":"2021-12-28T13:01:54.852Z","publishedAt":"2021-12-28T13:01:54.852Z","revisedAt":"2021-12-28T13:01:54.852Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/lua-resty-openidc/ITC_Engineering_Blog.png","alt":"OpenResty lua-resty-openidc phpでSSO Webアプリ環境作成","width":1200,"height":630}],"seo_authors":[]},{"id":"nextjs-rss-atom","createdAt":"2022-02-02T02:52:28.151Z","updatedAt":"2022-02-02T02:52:28.151Z","publishedAt":"2022-02-02T02:52:28.151Z","revisedAt":"2022-02-02T02:52:28.151Z","title":"Next.jsのブログにRSS／Atomフィード組み込み→Googleサチコに登録","category":{"id":"9acgdtf6pf","createdAt":"2021-06-03T13:51:31.714Z","updatedAt":"2021-08-31T12:04:14.034Z","publishedAt":"2021-06-03T13:51:31.714Z","revisedAt":"2021-08-31T12:04:14.034Z","topics":"Next.js","logo":"/logos/NextJS.png","needs_title":false},"topics":[{"id":"9acgdtf6pf","createdAt":"2021-06-03T13:51:31.714Z","updatedAt":"2021-08-31T12:04:14.034Z","publishedAt":"2021-06-03T13:51:31.714Z","revisedAt":"2021-08-31T12:04:14.034Z","topics":"Next.js","logo":"/logos/NextJS.png","needs_title":false}],"content":"# はじめに\n\nNext.js のブログシステム（GitHub リポジトリ：<a href=\"https://github.com/itc-lab/itc-blog\" target=\"_blank\">itc-lab/itc-blog</a>）を改良して、RSS/Atom フィードの XML を生成し、置くようにしました。  \n<a href=\"https://www.npmjs.com/package/feed\" target=\"_blank\">feed - npm</a>（GitHub リポジトリ：<a href=\"https://github.com/jpmonette/feed\" target=\"_blank\">jpmonette/feed</a>）を使って、RSS/Atom フィードの仕様をほとんど知らないまま、作成できました。\n今回、実装内容と、Google Search Console、Microsoft Bing Webmaster Tools に登録した結果について書きたいと思います。\n\n<blockquote class=\"info\">\n<p>タイトルの\"Googleサチコ\"とは、Google Search Consoleのことです。タイトルが長くなるため、サチコ表記です。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【Google Search Consoleのインデックス登録について】</strong></p>\n<p>2021年10月22日更新の記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/next-sitemap2\" target=\"_blank\">サイトマップ登録が「取得できませんでした」になる件の続報</a>」で、サイトマップがやっと認識されて、喜んでいましたが、その時と、2021年 - 2022年年末年始に数件登録されただけで、<strong>新規記事が一向にインデックス登録されなくなりました。(2022/02現在)</strong></p>\n<p>RSS/Atom フィードのXMLを生成するようにしたのは、送信しておけば、少しでもインデックス登録促進の効果が有ると思ったからです。</p>\n</blockquote>\n\n<br />\n\n# XML 例\n\n本ブログの XML を抜粋すると、以下のようになります。\n\n<br />\n\n`feed.xml`：\n\n```xml\n<rss\n  xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n  xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" version=\"2.0\">\n  <channel>\n    <title>ITC Engineering Blog</title>\n    <link>https://itc-engineering-blog.netlify.app/</link>\n    <description>豊田市のシステム開発会社 株式会社アイティーシーの技術情報発信ブログです。過去の知見、新たな知見を公開しています。ブログ自体のソースコードも公開しています。ご意見、ご感想は、お気軽にツイッターアカウントまで！</description>\n    <lastBuildDate>Tue, 01 Feb 2022 09:54:32 GMT</lastBuildDate>\n    <docs>https://validator.w3.org/feed/docs/rss2.html</docs>\n    <generator>Feed for Node.js</generator>\n    <image>\n      <title>ITC Engineering Blog</title>\n      <url>https://itc-engineering-blog.netlify.app/favicon-32x32.png</url>\n      <link>https://itc-engineering-blog.netlify.app/</link>\n    </image>\n    <copyright>All rights reserved 2022, 株式会社アイティーシー</copyright>\n    <item>\n      <title>\n        <![CDATA[ Ubuntu 20.04 LTSのオンプレにKubernetes環境構築からnginx Pod稼働まで ]]>\n      </title>\n      <link>https://itc-engineering-blog.netlify.app/blogs/ubuntu-kubernetes</link>\n      <guid>https://itc-engineering-blog.netlify.app/blogs/ubuntu-kubernetes</guid>\n      <pubDate>Mon, 31 Jan 2022 12:55:33 GMT</pubDate>\n      <description>\n        <![CDATA[ Ubuntu 20.04 LTS のオンプレミス環境に Kubernetes 環境を構築しました。マスターノード、ワーカーノードを作成し、ワーカーノードにNginx Podを追加しました。そこまでの全手順です。 ]]>\n      </description>\n      <content:encoded>\n        <![CDATA[ Ubuntu 20.04 LTS のオンプレミス環境に Kubernetes 環境を構築しました。マスターノード、ワーカーノードを作成し、ワーカーノードにNginx Podを追加しました。そこまでの全手順です。 ]]>\n      </content:encoded>\n      <author>*****@itccorporation.jp (株式会社アイティーシー)</author>\n    </item>\n    以降、同じように、\n    <item>\n      ・・・\n    </item>\n    を全ブログ記事分\n  </channel>\n</rss>\n```\n\n<br />\n\n`atom.xml`：\n\n```xml\n<feed\n  xmlns=\"http://www.w3.org/2005/Atom\">\n  <id>https://itc-engineering-blog.netlify.app/</id>\n  <title>ITC Engineering Blog</title>\n  <updated>2022-02-01T09:54:32.395Z</updated>\n  <generator>Feed for Node.js</generator>\n  <author>\n    <name>株式会社アイティーシー</name>\n    <email>*****@itccorporation.jp</email>\n    <uri>https://twitter.com/blog_itc</uri>\n  </author>\n  <link rel=\"alternate\" href=\"https://itc-engineering-blog.netlify.app/\"/>\n  <link rel=\"self\" href=\"https://itc-engineering-blog.netlify.app/rss/atom.xml\"/>\n  <subtitle>豊田市のシステム開発会社 株式会社アイティーシーの技術情報発信ブログです。過去の知見、新たな知見を公開しています。ブログ自体のソースコードも公開しています。ご意見、ご感想は、お気軽にツイッターアカウントまで！</subtitle>\n  <logo>https://itc-engineering-blog.netlify.app/favicon-32x32.png</logo>\n  <icon>https://itc-engineering-blog.netlify.app/favicon-32x32.png</icon>\n  <rights>All rights reserved 2022, 株式会社アイティーシー</rights>\n  <entry>\n    <title type=\"html\">\n      <![CDATA[ Ubuntu 20.04 LTSのオンプレにKubernetes環境構築からnginx Pod稼働まで ]]>\n    </title>\n    <id>https://itc-engineering-blog.netlify.app/blogs/ubuntu-kubernetes</id>\n    <link href=\"https://itc-engineering-blog.netlify.app/blogs/ubuntu-kubernetes\"/>\n    <updated>2022-01-31T12:55:33.946Z</updated>\n    <summary type=\"html\">\n      <![CDATA[ Ubuntu 20.04 LTS のオンプレミス環境に Kubernetes 環境を構築しました。マスターノード、ワーカーノードを作成し、ワーカーノードにNginx Podを追加しました。そこまでの全手順です。 ]]>\n    </summary>\n    <content type=\"html\">\n      <![CDATA[ Ubuntu 20.04 LTS のオンプレミス環境に Kubernetes 環境を構築しました。マスターノード、ワーカーノードを作成し、ワーカーノードにNginx Podを追加しました。そこまでの全手順です。 ]]>\n    </content>\n    <author>\n      <name>株式会社アイティーシー</name>\n      <email>*****@itccorporation.jp</email>\n      <uri>https://twitter.com/blog_itc</uri>\n    </author>\n    <contributor>\n      <name>株式会社アイティーシー</name>\n      <email>*****@itccorporation.jp</email>\n      <uri>https://twitter.com/blog_itc</uri>\n    </contributor>\n  </entry>\n  以降、同じように、\n  <entry>\n    ・・・\n  </entry>\n  を全ブログ記事分\n</feed>\n```\n\n<br />\n\n# 実装方法\n\nまず、`feed`の`npm install`が必要です。\n\n```shell-session\n# npm install feed\n```\n\nyarn の場合：\n\n```shell-session\n# yarn add feed\n```\n\nデータの取れ方は、それぞれだと思いますので、あくまで一例です。  \n`components/RSS.tsx` を 新規追加して、  \n以下のように、実装しました。\n\n<br />\n\n`component/RSS.tsx`：\n\n```typescript\nimport fs from 'fs';\n\nimport { Feed } from 'feed';\n\nimport { IBlogService, BlogService } from '@utils/BlogService'; //既存実装 ブログデータ取得用\nimport { IBlog, MicroCmsResponse } from '@/types/interface'; //既存実装 ブログデータ取得用\nimport settings from '@settings.yml'; //既存実装 設定取得用\n//import { renderToString } from 'react-dom/server';//HTMLの出力は中止。（\"試みてやめたこと\"で後述）\n//import { Markdown } from '@components/Markdown';//HTMLの出力は中止。（\"試みてやめたこと\"で後述）\n//import React from 'react';//HTMLの出力は中止。（\"試みてやめたこと\"で後述）\n\nexport async function generateRssFeed(): Promise<void> {\n  //asyncかつ、何も返さないため、型は、Promise<void>\n  if (process.env.NODE_ENV === 'development') {\n    //npm run devで発動しないようにする。\n    return;\n  }\n\n  const service: IBlogService = new BlogService(); //既存実装 ブログデータ取得用\n  const posts: MicroCmsResponse<IBlog> = await service.getBlogs(9999, 1); //ヘッドレスCMSから全コンテンツ取得\n  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL\n    ? `${process.env.NEXT_PUBLIC_BASE_URL}/`\n    : 'https://itc-engineering-blog.netlify.app/'; //URLの末尾に/が無いと、W3C Feed Validation Serviceで警告になるため、/付き\n  const date = new Date();\n  const author = {\n    name: settings.rss.author.name, //Authorの値決め。YAMLの設定で可変。\n    email: settings.rss.author.email,\n    link: settings.rss.author.link, //link = ツイッターのURL（例：https://twitter.com/blog_itc）にした。\n  };\n\n  const feed = new Feed({\n    //feedインスタンス初期化\n    title: settings.general.name, //ブログタイトル。「ITC Engineering Blog」が設定してある。\n    description: settings.general.description, //ブログ説明文。「ITC Engineering Blog」の紹介文が設定してある。\n    id: baseUrl, //一意になれば良いと思うので、トップ画面のURL\n    link: baseUrl, //トップ画面のURL\n    image: `${baseUrl}favicon-32x32.png`, //適当。（目的が本気でフィードする目的ではないため。）\n    favicon: `${baseUrl}favicon-32x32.png`, //適当。ただし、favicon.icoは、NGとの情報有り。\n    copyright: `All rights reserved ${date.getFullYear()}, ${\n      settings.rss.author.name\n    }`, //例に書いてあった通り。\n    updated: date, //現在日時。Next.jsの場合、ビルド日時。\n    generator: 'Feed for Node.js', //指定しない場合は、'Feed for Node.js'のため、書いても書かなくても同じ。\n    feedLinks: {\n      rss2: `${baseUrl}rss/feed.xml`,\n      json: `${baseUrl}rss/feed.json`,\n      atom: `${baseUrl}rss/atom.xml`,\n    }, //どの種類のフィードをどこに配置するか。\n    author, //上でセットしたauthor。author: author, の意味。\n  });\n\n  posts.contents.forEach((post) => {\n    //posts.contetntsには全記事情報が入っている。一つずつループ。\n    const url = `${baseUrl}blogs/${post.id}`; //記事タイトル。<title>タグや、metaタグのtitleと同じ。\n    const update_timestamp =\n      (post.reflect_updatedAt && post.updatedAt) ||\n      (post.reflect_revisedAt && post.revisedAt) ||\n      post.publishedAt; //reflect_*は、更新日時を反映するかのフラグ。反映しないなら、publishedAt（初回公開日時）\n    feed.addItem({\n      title: post.title, //記事タイトル。<title>タグや、metaタグのtitleと同じ。\n      id: url, //一意になれば良いと思うので、記事のURL\n      link: url, //記事のURL\n      description: post.description, //記事説明。metaタグのdescriptionと同じ。\n      content: post.description, //記事説明。metaタグのdescriptionと同じ。（\"試みてやめたこと\"で後述）\n      //content: renderToString(<Markdown source={post.content} />),//HTMLの出力は中止。（\"試みてやめたこと\"で後述）\n      author: [author], //上でセットしたauthor。\n      contributor: [author], //上でセットしたauthor。\n      date: new Date(update_timestamp), //上で決めた更新日時か初回公開日時\n    });\n  });\n\n  fs.mkdirSync('./public/rss', { recursive: true }); //./public/rssディレクトリ作成\n  fs.writeFileSync('./public/rss/feed.xml', feed.rss2()); //rss2ファイル出力\n  fs.writeFileSync('./public/rss/atom.xml', feed.atom1()); //atom1ファイル出力\n  fs.writeFileSync('./public/rss/feed.json', feed.json1()); //json形式ファイル出力（今回特に必要無い。）\n}\n```\n\n上記`generateRssFeed()`を実行すると、  \n`public/rss/feed.xml`  \n`public/rss/atom.xml`  \n`public/rss/feed.json`  \nが書き込まれるので、  \n<span style=\"color: red;\"><strong>ビルド中に１回実行</strong></span>しないといけません。  \nindex.tsx 等のトップ画面の`getStaticProps()`内で実行するのが普通だと思いますが、本ブログの場合、以下のようにトップ画面(`pages/index.ts`)が一覧画面(`list/[[...slug]]`)の表示と一体化してしまって、一覧画面で`getStaticProps()`を実行しなければならなくなりました。\n\n<br />\n\n`pages/index.ts`：\n\n```typescript\nimport Page, { getStaticProps } from './list/[[...slug]]';\n\nexport default Page;\n\nexport { getStaticProps };\n```\n\n一覧画面の`getStaticProps()`では、\"現在何ページ目\"、\"選択された関連技術\"（カテゴリ）のパラメータが渡ってくるのですが、トップ画面は、どちらも渡って来ない仕様です。（URL にパス=slug が無いため。）何も渡ってこなかったら、`generateRssFeed()`を実行としました。<span style=\"color: red;\">そうしないと、何回も`generateRssFeed()`が実行されることになります。</span>（何回も実行されても良いのですが、書き込まれるフィードファイルは毎回同じ内容で、時間とリソースの無駄です。）\n\n```typescript\nexport const getStaticProps: GetStaticProps<Props, Slug> = async ({\n  params,\n}) => {\n  if (!params) await generateRssFeed();\n```\n\n<br />\n\n**試みてやめたこと**\n\n```typescript\nimport { renderToString } from 'react-dom/server';\nimport { Markdown } from '@components/Markdown';\nimport React from 'react';\n・・・\n    feed.addItem({\n・・・\n      content: renderToString(<Markdown source={post.content} />),\n・・・\n```\n\nとすると、ブログ本文の markdown → React DOM → HTML の文字列 となり、HTML を渡せるのですが、XML が巨大になりました。  \n加えて、<a href=\"https://validator.w3.org/feed/\" target=\"_blank\">W3C Feed Validation Service</a> でチェックすると、エラーや警告が表示されて、つぶせそうになく、たとえ潰し切ったとしてもメリットは無さそうですので、`content:`には、description（記事の短い説明文）を適用することにしました。\n\n<br />\n\n**React DOM → HTML について**  \nReact DOM → HTML 文字列変換は、`react-dom/server`の`renderToString()`にて、実現できます。  \nただ、それだけの場合、`eslint`（リンター、文法チェッカー）が以下のエラーを出力します。\n\n<span style=\"color: red;background-color: cornsilk;\">'React' must be in scope when using JSX</span>\n\n`import React from 'react';`  \nを追加したら、収まりました。\n\n<br />\n\n# Validation チェック\n\n最初、記事本文を xml に載せていたため、<a href=\"https://validator.w3.org/feed/\" target=\"_blank\">W3C Feed Validation Service</a> でのチェック結果で、いろいろ出力されました。  \n備忘録的に列挙しようと思います。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image1.png\" alt=\"W3C Feed Validation Service XML parsing error\" width=\"747\" height=\"137\" loading=\"lazy\"></a>\n\n<strong><span style=\"color: red;background-color: cornsilk;\">XML parsing error: <unknown>:9131:28: not well-formed (invalid token)</span></strong>\n\n記事本文の一か所、制御コードが書かれていました。（ターミナルのエラー内容をコピペしたため。）\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image2.png\" alt=\"W3C Feed Validation Service not in canonical form\" width=\"1226\" height=\"99\" loading=\"lazy\"></a>\n\n<strong><span style=\"color: red;background-color: cornsilk;\">Identifier \"`https://itc-engineering-blog.netlify.app`\" is not in canonical form (the canonical form would be \"`https://itc-engineering-blog.netlify.app/`\")</span></strong>\n\n`<id>https://itc-engineering-blog.netlify.app</id>` と、末尾 / 無しの URL は警告になりました。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image3.png\" alt=\"W3C Feed Validation Self reference doesn't match document location\" width=\"558\" height=\"81\" loading=\"lazy\"></a>\n\n<strong><span style=\"color: red;background-color: cornsilk;\">Self reference doesn't match document location</span></strong>\n\nデバッグ中の XML を検査したため、 `<link rel=\"self\" href=\"https://itc-engineering-blog.netlify.app/rss/atom.xml\"/>` の部分で、`https://itc-engineering-blog.netlify.app/rss/atom.xml`がまだ存在していなく、警告になっていました。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image4.png\" alt=\"W3C Feed Validation style attribute contains potentially dangerous content\" width=\"1653\" height=\"119\" loading=\"lazy\"></a>\n\n<strong><span style=\"color: red;background-color: cornsilk;\">style attribute contains potentially dangerous content: color:#f8f8f2;background:#272822;text-shadow:0 1px rgba(0, 0, 0, 0.3);font-family:Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;border-radius:0.3em</span></strong>\n\n`<content type=\"html\">` 内で使ってはいけない CSS style を使っているため、警告になっていました。\n\n<br />\n\n他にも有りましたが、前述の通り、記事の短い説明文を適用することにしたため、以下のようになりました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image5.png\" alt=\"W3C Feed Validation atom.xml\" width=\"844\" height=\"201\" loading=\"lazy\"></a>\n\n`atom.xml` に関しては、完璧で、完璧であることの証明にバナーを掲載できるようです。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image6.png\" alt=\"W3C Feed Validation feed.xml\" width=\"1047\" height=\"235\" loading=\"lazy\"></a>\n\n`feed.xml` に関しては、一つだけ警告がありました。  \n<strong><span style=\"color: red;background-color: cornsilk;\">Missing atom:link with rel=\"self\"</span></strong>  \n`<link rel=\"self\" href=\"https://itc-engineering-blog.netlify.app/rss/atom.xml\"/>` が書かれていないからだと思いますが、合格はしていますし、そこまでこだわることないと見て、ここまでで登録作業に入りました。\n\n<br />\n\n# Google Search Console\n\n今まで認識されていた `site-map.xml` を削除して、`sitemap.xml`、`atom.xml`、`feed.xml` を登録しました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image7.png\" alt=\"Google Search Console site-map.xmlを削除\" width=\"1390\" height=\"302\" loading=\"lazy\"></a>\n\n...が、やはりというかなんというか、「取得できませんでした」、「サイトマップを読み込めませんでした」となり、数時間経っても認識されませんでした。認識されたら、ここの記述を更新しようと思います。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image8.png\" alt=\"Google Search Console 取得できませんでした\" width=\"994\" height=\"520\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image9.png\" alt=\"Google Search Console サイトマップを読み込めませんでした\" width=\"1006\" height=\"316\" loading=\"lazy\"></a>\n\n<br />\n\n# Bing Webmaster Tools\n\nMicrosoft Bing Webmaster Tools も`site-map.xml` を削除して、`sitemap.xml`、`atom.xml`、`feed.xml` を登録しました。  \nこちらは、「処理中」になり、数分後見たら、３つとも登録されていました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image10.png\" alt=\"Microsoft Bing Webmaster Tools 登録成功\" width=\"991\" height=\"327\" loading=\"lazy\"></a>\n\n<br />\n\nちなみに、Bing の方は、数日待つものもありましたが、既に全記事インデックス登録されていますので、インデックス登録促進の効果は分からないです。\n\n<br />\n\n<blockquote class=\"info\">\n<p><strong>【Bingでの事件について】</strong></p>\n<p>ブログ記事にはしていませんでしたが、一時期、インデックスが減少していき、ゼロになり、新規インデックス登録申請がブロックされるような挙動をしました。その時、心当たりが全く無く、英語で問い合わせてみました。</p>\n<p><a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/image11.png\" alt=\"Microsoft Bing Webmaster Tools 登録成功\" width=\"991\" height=\"327\" loading=\"lazy\"></a></p>\n<p>（２週間くらい音沙汰無しのため、再度問い合わせ）「いつになったら返事してもらえる？」</p>\n<p>↓</p>\n<p>（数日後）MS「これ(Bing Webmaster Guidelines)をチェックして欲しい（定型文）」</p>\n<p>↓</p>\n<p>「全てチェックした。何がダメなのかそちらで分からないのか？」</p>\n<p>うんぬんと何回かやり取りしたら、人間らしき文章に代わり、何か解除されたらしく、わずか数日後、インデックス登録申請ができるようになりました。</p>\n<p><span style=\"color: red;\">あくまで推測ですが、MSの中の人でも明確な原因は分からないけれど、何らかの状態をリセットすることはできるようでした。</span></p>\n</blockquote>\n","description":"Next.js のブログシステム（GitHub リポジトリ：https://github.com/itc-lab/itc-blog）を改良して、RSS/Atom フィードの XML を生成し、置くようにしました。→Googleサチコに登録した結果をブログ記事にしました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"whqf4s0rhirl","createdAt":"2022-02-02T02:05:04.509Z","updatedAt":"2022-02-02T02:05:04.509Z","publishedAt":"2022-02-02T02:05:04.509Z","revisedAt":"2022-02-02T02:05:04.509Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-rss-atom/ITC_Engineering_Blog.png","alt":"Next.jsのブログにRSS／Atomフィード組み込み→Googleサチコに登録","width":1200,"height":630}],"seo_authors":[]},{"id":"0tm_tm3nk8bj","createdAt":"2021-05-17T14:09:10.079Z","updatedAt":"2021-07-16T10:14:55.102Z","publishedAt":"2021-05-17T14:09:10.079Z","revisedAt":"2021-07-16T10:14:55.102Z","title":"GitLab プロジェクト作成→commit→push ssh接続","category":{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},"topics":[{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},{"id":"60cnyaw1v9","createdAt":"2021-05-17T14:06:27.679Z","updatedAt":"2021-08-31T12:04:56.824Z","publishedAt":"2021-05-17T14:06:27.679Z","revisedAt":"2021-08-31T12:04:56.824Z","topics":"Git","logo":"/logos/Git.png","needs_title":false},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\nUbuntu 20.04.2.0 にインストールした GitLab を使い、  \n・[初期設定](#anchor1)  \n・[ユーザー登録](#anchor2)  \n・[プロジェクト\\(リポジトリ\\)作成](#anchor3)  \n・[ファイル新規作成](#anchor4)  \n・[git clone,commit,push](#anchor5)  \n・[マージリクエスト\\(プルリクエスト\\)](#anchor6)  \n・[SSH接続](#anchor7)  \nまでチュートリアル的に一通り行います。  \n\n<br />\n\nclone→commit→push→マージまで以下の図のような流れで進めます。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/Git_Flow05.svg)  \n\n<br />\n\n各バージョンは、以下の通りです。\n```sh\n# gitlab-rake gitlab:env:info\nGitLab information\nVersion:        13.11.2\nGitLab Shell\nVersion:        13.17.0\n# git --version\ngit version 2.25.1\n# openssl version\nOpenSSL 1.1.1f  31 Mar 2020\n```\nWebブラウザの画面は、全てWindows10のFireFox 78.10.1esr (64 ビット)を使用しています。  \n\n<br />\n\nユーザーは、  \n\n| ＩＤ | ユーザー名 | 役割 |\n| ---- | ---- | ---- |\n| asan | Aさん(A San) | 最終コミット責任者的な位置付け |\n| bsan | Bさん(B San) | プログラマーの上司的な位置付け・レビュアー |\n| csan | Cさん(C San) | プログラマー |\n\nとします。  \n※実際には、厳密な権限付与管理が必要ですので、あくまで簡易的なイメージです。\n\n<br />\n\nURLは、  \nht<span>tp:/</span>/gitlab.itccorporation.jp  \nとします。※インターネット上に存在しません。  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# 初期設定\n  \nAdministrator（root）でログインします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image1.png)  \n\n<br />\n\n## 言語設定\n右上のアバター（ユーザーアイコン画像）部分をクリックします。 \n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image2.png)  \n\n<br />\n\n「Preferences」をクリックします。  \n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image3.png)  \n\n<br />\n\n「Language」を Japanese - 日本語 に変更し、「Save changes」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image4.png)  \n\n<br />\n\n## タイムゾーン設定\n左側の「ユーザー設定」（英語の場合「User Settings」）から「プロフィール」をクリックして、時間の設定 - Time zone を [UTC + 9] Tokyo に設定します。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image5.png)  \n\n<br />\n\n下の方にスクロールして、「プロファイル設定を更新」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image6.png)  \n\n<br />\n\n## その他調整\n左側の「ユーザー設定」から「メール」をクリックして、「`admin@example.com`」の右のゴミ箱ボタンをクリックして、削除します。  \n※今回の場合は、初期設定されていましたので、削除しますが、削除は必要無いかもしれません。いずれにしても一度確認した方が良いと思います。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image7.png)  \n\n<br />\n\n「Open registration is enabled on your instance.  \nインスタンスのユーザー登録をカスタマイズ/無効化する方法の詳細を確認する。」  \nと表示されている部分の「View setting」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image8.png)  \n\n<br />\n\n「Sign-up enabled」のチェックボックスを外します。  \n<blockquote class=\"info\">\n<p>デフォルトはチェック有りです。</p>\n<p>このままの場合、トップ画面から誰でもユーザー登録作業に入れます。</p>\n<p>今回、管理者がユーザー登録しますので、チェックを外します。</p>\n<p>「Open registration is enabled on your instance.  </p>\n<p>インスタンスのユーザー登録をカスタマイズ/無効化する方法の詳細を確認する。」</p>\n<p>は、これを警告しているようです。</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image9.png)  \n\n<br />\n\n下の方にスクロールして、「Save changes」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image10.png)  \n\n<br />\n\n「To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in your settings.」  \nと表示されている部分の「Don't send usage data」ボタンをクリックします。\n<blockquote class=\"info\">\n<p>利用状況をGitLabに送信するかどうかになります。今回は、「送信しない」としました。</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image11.png)  \n\n<br />\n\n警告が消えました。「Do you want to customize this page?」は、画面をカスタマイズするかどうかですので、Xボタンをクリックして閉じます。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image12.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image13.png)  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# ユーザー登録\n## Administrator(root)の操作\n上の工具のマーク「管理者エリア」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image14.png)  \n\n<br />\n\n「新規ユーザー」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image15.png)  \n\n<br />\n\n名前：A San  \nUsername：asan  \nメール：[メールアドレス]  \nを入力して、下へスクロールし、「Create user」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image16.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image17.png)  \n\n<br />\n\n## Aさんの操作\n登録したAさん宛てにメールが送信されますので、  \nAさんが Click here to set your password: のリンクをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image18.png)  \n\n<br />\n\nパスワードを入力します。※以降、Aさんのログインパスワード、push等のパスワードがこのパスワードになります。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image19.png)  \n\n<br />\n\nAさん宛てにメールが送信されます\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image20.png)  \n\n<br />\n\nAさんについても、Administrator(root)で行った初期設定を行います。（手順は省略）\n\n・・・Bさん、CさんもAさんと同じ方法で登録したものとして続けます。\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# プロジェクト(リポジトリ)作成\nAさんでログインします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image21.png)  \n\n<br />\n\n## グループ作成\n「Create a group」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image22.png)  \n\n<br />\n\nグループ名：Group1  \nグループURL：group1  \nとします。  \n「可視性レベル」は、今回、社内の限られたメンバーで扱うため、プライベートを選択します。\n\n<blockquote class=\"warn\">\n<p>グループの作成は必須ではありません。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【グループの役割】</p>\n<p>プロジェクト（リポジトリ）をまとめて管理する入れ物のようなものになります。ユーザの権限をまとめて管理できます。例えば、Group1にproject1,project2,project3とプロジェクト（リポジトリ）を作成して、Group1にユーザの権限を設定すると、project1,project2,project3全体に設定した権限が影響します。</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image23.png)  \n\n<br />\n\n## プロジェクト作成\n「新規プロジェクト」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image24.png)  \n\n<br />\n\n「Create blank project」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image25.png)  \n\n<br />\n\nプロジェクト名：project1  \nProject slug：project1  \nプロジェクトの説明(オプション)：テストプロジェクト  \nを入力し、「プロジェクトを作成」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image26.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image27.png)  \n\n<br />\n\n## アクセス権付与\nグループ→所属グループ→Group1　をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image28.png)  \n\n<br />\n\n左側から「メンバー」をクリックして、「GitLabメンバーまたはメールアドレス」のところをクリックして、メンバーを選択します。  \n今回、Aさんが作成したグループですので、残りのBさん、Cさんを選択します。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image30.png)  \n\n<br />\n\n「役割(権限)を選択してください」のところをクリックして、「Developer」を選択します。\n<blockquote class=\"warn\">\n<p>役割(権限)は、Guest -> Reporter -> Developer -> Maintainer -> Owner の順にできることが多くなっていきます。</p> \n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image31.png)  \n\n<br />\n\n「招待」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image32.png)  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# ファイル新規作成\n## GUIから新規ファイルをcommit\nAさんでログインして、上のメニューから「プロジェクト」→「あなたのプロジェクト」→「Group1 / project1」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image33.png)  \n\n<br />\n\n「新規ファイル」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image34.png)  \n\n<br />\n\n「新規ファイル」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image35.png)  \n\n<br />\n\n名前：hello.c を入力して、「ファイルを作成」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image36.png)  \n\n<br />\n\n```c\n#include <stdio.h>\n\nmain()\n{\n  printf(\"Hello World\\n\");\n}\n```\nを入力して、「コミット」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image37.png)  \n\n<br />\n\n「コミット」をクリックします。\n※ここでコミットメッセージを入力できますが、デフォルトで入力されている「アップデート hello.c ファイル」のままにしました。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image38.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image39.png)  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor5\"></a>\n\n# git clone,commit,push\n## clone\nCさんでログインして、「Group1 / project1」の画面の「クローン」をクリックします。  \n「HTTP でクローン」のURLをコピーします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image40.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/terminal.png) \n\nSSHで作業用サーバーにログインして、コマンドラインで作業します。  \n\n<blockquote class=\"warn\">\n<p>今回、Ubuntu 20.04.2.0にログインして作業しました。</p>\n<p>gitがインストールされていなかったため、</p>\n<code># apt install git</code>\n<p>が必要でした。</p>\n</blockquote>\n\nGitLabで設定したメールアドレス、名前を設定します。  \n```sh\n$ git config --global user.email \"csan@mailserver.example.com\"\n$ git config --global user.name \"C San\"\n```\n\n<span style=\"color: red; \">`csan@mailserver.example.com` は、Cさんのメールアドレスにしてください。</span>\n\n<blockquote class=\"info\">\n<p>一度設定したら記憶されます。</p>\n<p>設定しない場合、commitの時に以下のエラーになります。</p>\n</blockquote>\n\n```sh\n*** Please tell me who you are.\n\nRun\n\n  git config --global user.email \"you@example.com\"\n  git config --global user.name \"Your Name\"\n\nto set your account's default identity.\nOmit --global to set the identity only in this repository.\n\nfatal: unable to auto-detect email address (got 'csan@ubuntu.(none)')</code>\n```\n\n<br />\n\ngit cloneでソースコードを開発環境にコピーします。\n```sh\n$ git clone http://gitlab.itccorporation.jp/group1/project1.git\n```\n⇒カレントディレクトリにproject1ディレクトリが作成されます。\n\n<br />\n\n【 現在の状態イメージ 】\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/Git_Flow01.svg)\n\n<br />\n\n## ブランチ作成\n開発用にブランチを作成します。※ブランチの作成は必須ではありませんが、通常masterに直接pushはしないと思うのと、この後のマージリクエストにも繋がっていきますので、作成します。\n\n```sh\n$ git branch develop\n```\n\n<br />\n\n現在のブランチを確認します。\n```sh\n$ git branch\n  develop\n* master\n```\n\n<br />\n\nブランチを移動します。\n```sh\n$ git checkout develop\nSwitched to branch 'develop'\n```\n\n<blockquote class=\"info\">\n<code>$ git checkout -b ブランチ名</code>\n<p>にてブランチ作成とブランチ移動両方行われます。</p>\n</blockquote>\n\n<br />\n\n【 現在の状態イメージ 】\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/Git_Flow02.svg)\n\n<br />\n\n\n## ブランチでcommit\nソースコードを修正します。\n```sh\n$ vi hello.c\n```\n```c\n#include <stdio.h>\n\nmain()\n{\n  printf(\"Hello World!!!!!\\n\");\n}\n```\n※!!!!!追記\n\n<br />\n\ngit add でcommit対象を追加します。\n```sh\n$ git add hello.c\n```\n\n<blockquote class=\"info\">\n<code>$ git add .</code>\n<p>や</p>\n<code>$ git add -A</code>\n<p>等でも良いです。</p>\n</blockquote>\n\n<br />\n\ncommitします。\n```sh\n$ git commit -m 'fix: add !!!!!'\n```\n\n<br />\n\n【 現在の状態イメージ 】\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/Git_Flow03.svg)\n\n<br />\n\n## ブランチの変更をプッシュ\nGitLabのサーバーへpushします。\n```sh\n$ git push origin develop\n```\n\n<blockquote class=\"info\">\n<p>originというリモートリポジトリのdevelopブランチにpush（アップロード）するという意味になります。</p>\n<p>origin : push先のリモートリポジトリの名前です。今回の場合、http://gitlab.itccorporation.jp/group1/project1.git がそれに相当します。</p>\n<p>develop : push先のブランチ名です。</p>\n</blockquote>\n\n<br />\n\n【 現在の状態イメージ 】\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/Git_Flow04.svg)\n\n<br />\n\n<a class=\"anchor\" id=\"anchor6\"></a>\n\n# マージリクエスト(プルリクエスト)\n## マージリクエスト送信\nCさんでログインします。  \nすると、「マージリクエストを作成」ボタンが表示されていますので、クリックして、マージリクエスト（プルリクエスト）を行います。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image41.png)  \n\n<br />\n\nDescriptionにレビュワー向けの説明を記入します。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image42.png)  \n\n<br />\n\nAssignee：Aさん  \nReviewer：Bさん  \nとします。\n\n<blockquote class=\"info\">\n<p>Assignees - マージ担当者を設定します。</p>\n<p>Reviewers - レビューする人を設定します。</p>\n<p>※運用方針によって、役割は異なります。</p>\n</blockquote>\n\nMilestone, Labels は今回は無しとします。  \nマージオプションは、「Delete source branch when merge request is accepted.」にチェックを入れます。チェックが有る場合、マージ完了時、developブランチが消えます。  \n「Squash commits when merge request is accepted.」は、他のコミットも統合するかどうかですが、今回は関係無いため、チェック無しにします。\n\n<br />\n\n「Create マージリクエスト」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image43.png)  \n\n<br />\n\nAさん、Bさん宛てにメールが送信されます。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image44.png)  \n\n<br />\n\n## マージリクエスト承認\nBさんでログインして、メールのURLのところへ行きます。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image45.png)  \n\n<br />\n\n「変更」をクリックして、変更箇所を確認します。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image46.png)  \n\n<br />\n\n<blockquote class=\"info\">\n<p>以下のようにWeb IDEでも変更箇所を確認できます。</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image45_2.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image47.png)  \n\n<br />\n\n「承認」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image48.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image49.png)  \n\n<br />\n\n## マージ\nAさんでログインして、メールのURLのところへ行きます。\n（Bさんと同じように差分を確認して、）「追加で承認する」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image50.png)  \n\n<br />\n\n「マージ」ボタンをクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image51.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image52.png)  \n\n<br />\n\nBさん、Cさんにメールが送信されます。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image53.png)  \n\n<br />\n\nCさんでログインして、masterブランチの「コミット」「変更」を見ると、反映されているのが分かります。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image54.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image55.png)  \n\n<br />\n\n【 現在の状態イメージ 】\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/Git_Flow05.svg)\n\n<br />\n\n<a class=\"anchor\" id=\"anchor7\"></a>\n\n# SSH接続\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/terminal.png) \n\nSSHで作業用サーバーにログインして、コマンドラインで作業します。  \n\n<br />\n\n## SSH 鍵作成\n端末にログインして、SSH秘密鍵、公開鍵を作成します。\n\n```sh\n$ ssh-keygen -t ed25519\nGenerating public/private ed25519 key pair.\nEnter file in which to save the key (/home/csan/.ssh/id_ed25519):\nCreated directory '/home/csan/.ssh'.\nEnter passphrase (empty for no passphrase):<空欄のままエンター>\nEnter same passphrase again:<空欄のままエンター>\nYour identification has been saved in /home/csan/.ssh/id_ed25519\nYour public key has been saved in /home/csan/.ssh/id_ed25519.pub\nThe key fingerprint is:\nSHA256:w3W/lGyrFJqyVbvQ+tY37sxaCw8fpIqnL4cNlVEpEjw csan@ubuntu\nThe key's randomart image is:\n+--[ED25519 256]--+\n|        ... ...  |\n|         E o .   |\n|          + =    |\n|       . . + o . |\n|        S . o *. |\n|         o = =oo |\n|        . O +=oo |\n|         *.BooX.+|\n|        .oB=+.+X.|\n+----[SHA256]-----+\n```\n\n<blockquote class=\"info\">\n<p>パスフレーズ無しで作成します。</p>\n<p>-tで指定するed25519は暗号化方式になります。強固で速いということで指定しています。opensshが対応していないバージョンの場合、rsa等別の方式を指定してください。</p>\n</blockquote>\n\n<br />\n\n## 公開鍵登録\n公開鍵を確認します。\n\n```sh\n$ cat /home/csan/.ssh/id_ed25519.pub\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID9vBTivpnj/oZ1rizCWvAMHxeDyx2bxRs7KWvqIw3XR csan@ubuntu\n```\n\n<br />\n\nCさんでWeb画面にログインして、右上のアバター（ユーザーアイコン画像）部分をクリックして、「Preferences」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image56.png)  \n\n<br />\n\n左側の「SSH 鍵」をクリックして、先ほどの `$ cat /home/csan/.ssh/id_ed25519.pub` の出力内容をキーのところへコピーペーストします。  \nタイトルは、何でも良く、自動入力されたままとし、Expire at は有効期限を設定できますが、今回設定しないため、yyyy/mm/dd のままにし、「キーを追加」をクリックします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image57.png)  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image58.png)  \n\n<br />\n\nCさんにメール送信されます。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image59.png)  \n\n<br />\n\n## gitからSSH接続\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/terminal.png) \n\nSSHで作業用サーバーにログインして、コマンドラインで作業します。  \n\n<br />\n\n現在のブランチを確認します。\n\n```sh\n$ cd /home/csan/project1\n$ git branch\n* develop\n  master\n```\n\n<br />\n\nhello.cを変更します。\n\n```sh\n$ vi hello.c\n```\n\n```c\n#include <stdio.h>\n\nmain()\n{\n  printf(\"Hello World!!!!!\\n\");\n  printf(\"Hey World\\n\");\n}\n```\n⇒printf(\"Hey World\\n\");追加\n\n<br />\n\ngit add で登録します。\n\n```sh\n$ git add .\n```\n\n<br />\n\ngit の設定を変更します。\n\n```sh\n$ git remote set-url origin git@gitlab.itccorporation.jp:group1/project1.git\n```\n\n<blockquote class=\"info\">\n<p>.git/configが</p>\n<pre><code>[remote \"origin\"]\n        url = http://gitlab.itccorporation.jp/group1/project1.git\n↓\n[remote \"origin\"]\n        url = git@gitlab.itccorporation.jp:group1/project1.git</code></pre>\n<p>と変更されます。</p>\n<p>「git@gitlab.itccorporation.jp:group1/project1.git」ですが、以下の 「SSH でクローン」のところで分かります。</p>\n<p>↓</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image60.png)  \n\n<br />\n\ncommitして、pushします。\n\n```sh\n$ git commit -m \"fix : add new line\"\n$ git push -u origin develop\n```\n\nここで何も聞かれずにpushできたら成功です。\n\n<br />\n\nリモートの方もちゃんと新しい行がコミットされています。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/image61.png)  \n\n<br />\n\n成功です！","description":"Ubuntu 20.04.2.0 にインストールした GitLab を使い、 ・初期設定 ・ユーザー登録 ・プロジェクト(リポジトリ)作成 ・ファイル新規作成 ・git clone,commit,push ・マージリクエスト(プルリクエスト) ・SSH接続 までチュートリアル的に一通り行います。 clone→commit→push→マージまで以下の図のような流れで進めます。 各バージョンは、以下の通りです。 # gitlab-rake gitlab:env:info GitLab information Version:        13.11.2 GitLab Shell Version:        13.17.0 # git --version git version 2.25.1 # openssl version OpenSSL 1.1.1f  31 Mar 2020","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"o5r9wibq94ik","createdAt":"2021-07-16T10:02:42.667Z","updatedAt":"2021-07-16T10:02:42.667Z","publishedAt":"2021-07-16T10:02:42.667Z","revisedAt":"2021-07-16T10:02:42.667Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab_tutorial/ITC_Engineering_Blog.png","alt":"GitLab プロジェクト作成→commit→push ssh接続","width":1200,"height":630}],"seo_authors":[]},{"id":"simplesaml-oidc-azure","createdAt":"2023-12-29T08:25:04.687Z","updatedAt":"2024-03-02T10:15:29.607Z","publishedAt":"2023-12-29T08:25:04.687Z","revisedAt":"2024-03-02T10:15:29.607Z","title":"Nginx&SimpleSAMLphpをOIDC対応RP化してAzure AD(Entra ID)認証","category":{"id":"lgiabhpmz","createdAt":"2021-11-25T13:17:57.984Z","updatedAt":"2021-11-25T13:17:57.984Z","publishedAt":"2021-11-25T13:17:57.984Z","revisedAt":"2021-11-25T13:17:57.984Z","topics":"OpenID Connect","logo":"/logos/OpenIDConnect.png","needs_title":false},"topics":[{"id":"lgiabhpmz","createdAt":"2021-11-25T13:17:57.984Z","updatedAt":"2021-11-25T13:17:57.984Z","publishedAt":"2021-11-25T13:17:57.984Z","revisedAt":"2021-11-25T13:17:57.984Z","topics":"OpenID Connect","logo":"/logos/OpenIDConnect.png","needs_title":false},{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"9iy1ks71tv7n","createdAt":"2021-05-31T13:08:18.404Z","updatedAt":"2021-08-31T12:04:47.612Z","publishedAt":"2021-05-31T13:08:18.404Z","revisedAt":"2021-08-31T12:04:47.612Z","topics":"Nginx","logo":"/logos/Nginx.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false}],"content":"# はじめに\n\n前回記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/simplesamlphp-nginx-azuread\" target=\"_blank\">Nginx&SimpleSAMLphp で SAML の SP を構築 Azure AD で認証</a>」にて、SimpleSAMLphp の SAML における SP(Service Provider) を構築して、Azure AD で認証まで行いました。  \n今回は、SimpleSAMLphp で OpenID Connect(OIDC) の Relying Party(RP) を構築して、OpenID Provider(OP) は、Azure AD(Microsoft Entra ID) とし、Azure AD のユーザーアカウントで認証まで行います。  \nWeb アプリケーション作成から Azure AD の設定、SSO 認証実現までの全手順を紹介していきます。\n\n<br />\n\nなお、  \n・Web アプリケーション環境構築  \n・SimpleSAMLphp Web コンソール環境作成  \n・SimpleSAMLphp Web コンソール初期設定  \nまでは前回記事と被りますので、実施内容だけ書いて、説明無しでいきます。  \nこの記事の本題は、SimpleSAMLphp の Web コンソール管理者画面を表示したところからスタートです。  \n説明が必要な場合、<a href=\"https://itc-engineering-blog.netlify.app/blogs/simplesamlphp-nginx-azuread\" target=\"_blank\">前回記事</a>を参照してください。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image1.png\" alt=\"SimpleSAMLphp OIDC 関係図\" width=\"801\" height=\"391\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>Azure AD（Azure Active Directory）は、Microsoft Entra ID に名称が変わりましたが、この記事では、Azure AD 表記のままでいきます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p>Ubuntu 22.04.3 LTS</p>\n<p>Nginx 1.18.0</p>\n<p>PHP 8.3.0</p>\n<p>SimpleSAMLphp 2.1.1</p>\n</blockquote>\n\n<br />\n\n# Web アプリケーション環境構築\n\n<blockquote class=\"info\">\n<p>今回、Web アプリケーションサーバーの URI は、</p>\n<p><code>https://webapps-php.example.com/info.php</code> とします。</p>\n</blockquote>\n\n```shellsession\n# apt update\n# add-apt-repository ppa:ondrej/php -y\n# apt update\n# apt -y install php8.3 php8.3-gd php8.3-mbstring php8.3-common php8.3-curl\n# php -v\nPHP 8.3.0 (cli) (built: Nov 24 2023 08:50:08) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.0, Copyright (c) Zend Technologies\n    with Zend OPcache v8.3.0, Copyright (c), by Zend Technologies\n# apt list --installed | grep apache2\n\nWARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n\napache2-bin/jammy-updates,jammy-security,now 2.4.52-1ubuntu4.7 amd64 [インストール済み、自動]\napache2-data/jammy-updates,jammy-updates,jammy-security,jammy-security,now 2.4.52-1ubuntu4.7 all [インストール済み、自動]\napache2-utils/jammy-updates,jammy-security,now 2.4.52-1ubuntu4.7 amd64 [インストール済み、自動]\napache2/jammy-updates,jammy-security,now 2.4.52-1ubuntu4.7 amd64 [インストール済み、自動]\nlibapache2-mod-php8.3/jammy,now 8.3.0-1+ubuntu22.04.1+deb.sury.org+1 amd64 [インストール済み、自動]\n# apt -y remove apache2-*\n# apt install -y php-fpm\n# apt install nginx -y\n# nginx -v\nnginx version: nginx/1.18.0 (Ubuntu)\n# vi /etc/nginx/fastcgi_params\n```\n\n```ini:/etc/nginx/fastcgi_params\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\n↓\nfastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\n```\n\n```shellsession\n# mkdir -p /opt/webapps/php\n# chown -R www-data: /opt/webapps\n# mkdir -p /var/log/webapps/php\n# vi /opt/webapps/php/info.php\n```\n\n```php:/opt/webapps/php/info.php\n<?php\nphpinfo();\n```\n\n```shellsession\n# openssl genrsa -out ca.key 2048\n# openssl req -new -key ca.key -out ca.csr\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:webapps-php.example.com\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\nAn optional company name []:\n# echo \"subjectAltName=DNS:*.example.com,IP:192.168.12.200\" > san.txt\n# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt -extfile san.txt\nSignature ok\nsubject=C = JP, ST = Aichi, L = Toyota, O = Default Company Ltd, CN = webapp.example.com\nGetting Private key\n# mkdir -p /etc/pki/tls/certs\n# mkdir /etc/pki/tls/private\n# mv ca.crt /etc/pki/tls/certs/webapps-php.crt\n# mv ca.key /etc/pki/tls/private/webapps-php.key\n# mv ca.csr /etc/pki/tls/private/webapps-php.csr\n# vi /etc/nginx/conf.d/webapps-info.conf\n```\n\n```nginx:/etc/nginx/conf.d/webapps-info.conf\nserver  # サーバーブロックの開始\n{\n  listen 443 ssl; # サーバーが待ち受けるポート番号\n  ssl_certificate /etc/pki/tls/certs/webapps-php.crt; # TLS証明書\n  ssl_certificate_key /etc/pki/tls/private/webapps-php.key; # TLS秘密鍵\n  server_name webapps-php.example.com;  # サーバーの名前\n  access_log /var/log/webapps/php/access.log;  # アクセスログのパス\n  error_log /var/log/webapps/php/error.log;  # エラーログのパス\n\n  root /opt/webapps/php;  # サーバーのルートディレクトリ\n\n  location /  # ルートディレクトリに対する設定\n  {\n    index index.html index.htm index.php;  # デフォルトで使用するインデックスファイル\n  }\n\n  location ~ [^/]\\.php(/|$)  # .phpで終わるリクエストに対する設定\n  {\n    fastcgi_split_path_info ^(.+?\\.php)(/.*)$;  # パス情報を分割\n    if (!-f $document_root$fastcgi_script_name)  # スクリプトファイルが存在しない場合\n    {\n      return 404;  # 404エラーを返す\n    }\n\n    client_max_body_size 100m;  # クライアントからの最大ボディサイズ\n\n    # Mitigate https://httpoxy.org/ vulnerabilities\n    fastcgi_param HTTP_PROXY \"\";  # HTTP_PROXYを空に設定してhttpoxy脆弱性を緩和\n\n    # fastcgi_pass 127.0.0.1:9000;\n    fastcgi_pass unix:/run/php/php8.3-fpm.sock;  # FastCGIサーバーへのパス\n    fastcgi_index index.php;  # デフォルトのFastCGIスクリプト\n\n    # include the fastcgi_param setting\n    include fastcgi_params;  # FastCGIパラメータの設定を含む\n\n    # SCRIPT_FILENAME parameter is used for PHP FPM determining\n    #  the script name. If it is not set in fastcgi_params file,\n    # i.e. /etc/nginx/fastcgi_params or in the parent contexts,\n    # please comment off following line:\n    # fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;  # スクリプト名を決定するためのパラメータ\n  }\n}\n```\n\n<br />\n\n```shellsession\n# vi /etc/php/8.3/fpm/php.ini\n```\n\n```ini\ndate.timezone = Asia/Tokyo\n# 新規設定\n\ndisplay_errors = On\ndisplay_startup_errors = On\n# 確認（最初から設定されている。）\n```\n\n```shellsession\n# vi /etc/hosts\n192.168.12.200 webapps-php.example.com\n```\n\n<blockquote class=\"info\">\n<p>今回、Web アプリケーションサーバーの IP アドレスは、192.168.12.200 とします。</p>\n</blockquote>\n\n```shellsession\n# systemctl restart nginx\n# systemctl restart php8.3-fpm\n```\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" alt=\"phpinfo()の画面\" width=\"1249\" height=\"373\" loading=\"lazy\"></a>\n\n<br />\n\n# SimpleSAMLphp Web コンソール環境作成\n\n```shellsession\n# apt install -y php8.3-dom php8.3-xml\n# tar xzf simplesamlphp-2.1.1-full.tar.gz\n# mv simplesamlphp-2.1.1 /var/simplesamlphp\n```\n\n<blockquote class=\"info\">\n<p>今回、SimpleSAMLphp Web コンソールの URL は、</p>\n<p><code>https://ssp2.example.com/simplesaml/</code> とします。</p>\n</blockquote>\n\n```shellsession\n# openssl genrsa -out ca.key 2048\n# openssl req -new -key ca.key -out ca.csr\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:ssp2.example.com\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\nAn optional company name []:\n# echo \"subjectAltName=DNS:*.example.com,IP:192.168.12.200\" > san.txt\n# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt -extfile san.txt\nSignature ok\nsubject=C = JP, ST = Aichi, L = Toyota, O = Default Company Ltd, CN = webapp.example.com\nGetting Private key\n# mv ca.crt /etc/pki/tls/certs/ssp2.example.com.crt\n# mv ca.key /etc/pki/tls/private/ssp2.example.com.key\n# mv ca.csr /etc/pki/tls/private/ssp2.example.com.csr\n# vi /etc/nginx/conf.d/simplesaml.conf\n```\n\n```nginx:/etc/nginx/conf.d/simplesaml.conf\nserver {\n    listen 443 ssl;\n    server_name ssp2.example.com;\n\n    ssl_certificate        /etc/pki/tls/certs/ssp2.example.com.crt;\n    ssl_certificate_key    /etc/pki/tls/private/ssp2.example.com.key;\n    ssl_protocols          TLSv1.3 TLSv1.2;\n    ssl_ciphers            EECDH+AESGCM:EDH+AESGCM;\n\n    location ^~ /simplesaml {\n        index index.php;\n        alias /var/simplesamlphp/public;\n\n        location ~^(?<prefix>/simplesaml)(?<phpfile>.+?\\.php)(?<pathinfo>/.*)?$ {\n            include          fastcgi_params;\n            # fastcgi_pass     $fastcgi_pass;\n            fastcgi_pass     unix:/run/php/php8.3-fpm.sock;\n            fastcgi_param SCRIPT_FILENAME $document_root$phpfile;\n\n            # Must be prepended with the baseurlpath\n            fastcgi_param SCRIPT_NAME /simplesaml$phpfile;\n\n            fastcgi_param PATH_INFO $pathinfo if_not_empty;\n        }\n    }\n}\n```\n\n```shellsession\n# chown -R www-data: /var/simplesamlphp\n# vi /etc/hosts\n192.168.12.200\tssp2.example.com\n# systemctl restart nginx\n# systemctl restart php8.3-fpm\n# cd /var/simplesamlphp\n# cp config/config.php.dist config/config.php\n# cp config/authsources.php.dist config/authsources.php\n# cp metadata/saml20-idp-hosted.php.dist metadata/saml20-idp-hosted.php\n# cp metadata/saml20-idp-remote.php.dist metadata/saml20-idp-remote.php\n# cp metadata/saml20-sp-remote.php.dist metadata/saml20-sp-remote.php\n```\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image4.png\" alt=\"SimpleSAMLphpのコンソール画面\" width=\"1393\" height=\"629\" loading=\"lazy\"></a>\n\n<br />\n\n# SimpleSAMLphp Web コンソール初期設定\n\n```shellsession\n# openssl rand -base64 32\nRCN2jfGolAsfOb1J4UXQrbrwevYIyz/O/o9sJWRzxTc=\n# vi /var/simplesamlphp/config/config.php\n```\n\n```php:/var/simplesamlphp/config/config.php\n//'secretsalt' => 'defaultsecretsalt',\n// ↓ 変更\n'secretsalt' => 'RCN2jfGolAsfOb1J4UXQrbrwevYIyz/O/o9sJWRzxTc=',\n\n//'auth.adminpassword' => '123',\n// ↓ 変更\n'auth.adminpassword' => 'admin',\n\n//'timezone' => null,\n// ↓ 変更\n'timezone' => 'Asia/Tokyo',\n\n// 'technicalcontact_name' => 'Administrator',\n// 'technicalcontact_email' => 'na@example.org',\n// ↓ 変更\n'technicalcontact_name' => 'Administrator',\n'technicalcontact_email' => 'admin@ssp2.example.com',\n```\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image5.png\" alt=\"管理者ログイン\" width=\"1104\" height=\"719\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image6.png\" alt=\"管理者ログイン成功後\" width=\"1253\" height=\"1303\" loading=\"lazy\"></a>\n\n<br />\n\n# simplesamlphp-module-authoauth2 インストール\n\nPHP のパッケージ管理システム `composer` で SimpleSAMLphp 用 OpenID Connect <span style=\"color: red;\"><strong>Relying Party(RP) 対応拡張モジュール simplesamlphp-module-authoauth2</strong></span> をインストールします。\n\n<blockquote class=\"info\">\n<p>ソースコードは、</p>\n<p><code>https://github.com/simplesamlphp/simplesamlphp-module-oidc</code></p>\n<p>にあります。</p>\n<p>README に 「SSP」, 「SSP2」 とありますが、それぞれ、SimpleSAMLphp v1.x.x, SimpleSAMLphp v2.x.x のことです。（最初何のことだか分かりませんでした。）</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>OP（OpenID Provider）を構築するときに使用するのは、以下です。</strong></span></p>\n<p><span style=\"color: red;\"><strong><code>https://github.com/simplesamlphp/simplesamlphp-module-oidc</code></strong></span></p>\n<p><span style=\"color: red;\"><strong>今回は、Relying Party(RP) を構築するため、使いません。説明もしません。</strong></span></p>\n</blockquote>\n\n```shellsession\n# cd /var/simplesamlphp\n# apt update -y\n# apt install composer -y\n```\n\n<br />\n\n次に行う composer で依存関係エラーになるため、先に足りないモジュールインストールします。\n\n```shellsession\n# apt install php8.3-sqlite3 -y\n# apt install php8.3-ldap -y\n# apt install php8.3-intl -y\n```\n\n<blockquote class=\"info\">\n<p>依存関係エラーの内容は以下です。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Problem 1</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> - Root composer.json requires PHP extension ext-pdo_sqlite _ but it is missing from your system. Install or enable PHP's pdo_sqlite extension.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Problem 2</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> - simplesamlphp/simplesamlphp-module-ldap is locked to version v2.2.1 and an update of this package was not requested.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> - simplesamlphp/simplesamlphp-module-ldap v2.2.1 requires ext-ldap _ -> it is missing from your system. Install or enable PHP's ldap extension.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">PHP Fatal error: Uncaught Error: Class \"Normalizer\" not found in /usr/share/php/Symfony/Component/String/AbstractUnicodeString.php:31</span></p>\n</blockquote>\n\n<br />\n\ncomposer でインストールします。\n\n```shellsession\n# composer require cirrusidentity/simplesamlphp-module-authoauth2\n...\n  - Installing symfony/translation (v6.0.19): Extracting archive\nGenerating autoload files\n66 packages you are using are looking for funding.\nUse the `composer fund` command to find out more!\n> php bin/translations translations:update:binary\n```\n\n<br />\n\nsimplesamlphp-module-authoauth2 インストール成功です！\n\n<br />\n\n# Azure AD - アプリの登録\n\nOP（Azure AD）側の設定を行います。  \nAzure ポータルから、Microsoft Entra ID に移動して、**アプリの登録** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image2.png\" alt=\"Microsoft Entra ID\" width=\"1187\" height=\"289\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image3.png\" alt=\"アプリの登録\" width=\"1186\" height=\"656\" loading=\"lazy\"></a>\n\n<br />\n\n**＋新規作成** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image4.png\" alt=\"アプリの登録 ＋新規作成\" width=\"1186\" height=\"455\" loading=\"lazy\"></a>\n\n<br />\n\nアプリ情報を入力し、**登録** をクリックします。  \n**名前**： `SimpleSAMLphpOIDC`（任意です。）  \n**サポートされているアカウントの種類**：`この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)`  \n**リダイレクト URI (省略可能)**：`Web` `https://ssp2.example.com/simplesaml/module.php/authoauth2/linkback.php`\n\n<blockquote class=\"info\">\n<p>リダイレクト URI については、<a href=\"https://github.com/cirrusidentity/simplesamlphp-module-authoauth2\" target=\"_blank\">simplesamlphp-module-authoauth2 の README</a> に</p>\n<p><code>https://hostname/SSP_PATH/module.php/authoauth2/linkback.php</code></p>\n<p>と指示があります。</p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image5.png\" alt=\"アプリ情報を入力\" width=\"1127\" height=\"818\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** をクリックして、  \n**アプリケーション (クライアント) ID** から clientId（後で行う SimpleSAMLphp 側設定項目）を確認しておきます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image6.png\" alt=\"概要 アプリケーション (クライアント) ID\" width=\"1126\" height=\"737\" loading=\"lazy\"></a>\n\n<br />\n\n**証明書とシークレット** をクリックし、**＋新しいクライアント シークレット** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image7.png\" alt=\"証明書とシークレット ＋新しいクライアント シークレット\" width=\"1125\" height=\"549\" loading=\"lazy\"></a>\n\n<br />\n\n**説明**、**有効期限** を任意の値に設定し、**追加** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image8.png\" alt=\"説明 有効期限\" width=\"1126\" height=\"552\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>ここで出てくる **値** の文字列が clientSecret（後で行う SimpleSAMLphp 側設定項目）の文字列になります。（シークレット ID の方ではありません。）</strong></span>  \n<span style=\"color: red;\"><strong>二度と表示されないため、ここで、メモっておきます。</strong></span>\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image9.png\" alt=\"値=clientSecret（後で行う SimpleSAMLphp 側設定項目）\" width=\"1125\" height=\"612\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** に移動して、**エンドポイント** をクリックし、  \n**OpenID Connect メタデータ ドキュメント** から discoveryUrl（後で行う SimpleSAMLphp 側設定項目） を確認します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image10.png\" alt=\"概要 エンドポイント\" width=\"1126\" height=\"737\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image11.png\" alt=\"discoveryUrl（後で行う SimpleSAMLphp 側設定項目） を確認\" width=\"1126\" height=\"723\" loading=\"lazy\"></a>\n\n<br />\n\n# SimpleSAMLphp 設定\n\nauthsources.php に Relying Party(RP) の設定を追加します。\n\n```shellsession\n# cd /var/simplesamlphp\n# vi config/authsources.php\n```\n\n```php:/var/simplesamlphp/config/authsources.php\n    'microsoftOIDCSource' => [\n        'authoauth2:OpenIDConnect',\n        'issuer' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0',\n        // When using the 'common' discovery endpoint it allows any Azure user to authenticate, however\n        // the token issuer is tenant specific and will not match what is in the common discovery document.\n        'validateIssuer' => false,  // issuer is just used to confirm correct discovery endpoint loaded\n        'discoveryUrl' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0/.well-known/openid-configuration',\n        'clientId' => '3******6-0**9-4**b-85bd-f**********8',\n        'clientSecret' => 'sv************************************Z_',\n    ],\n```\n\n<blockquote class=\"info\">\n<p>設定キーの <code>'microsoftOIDCSource'</code> は任意です。</p>\n<p><code>'discoveryUrl'</code>、<code>'clientId'</code>、<code>'clientSecret'</code> は Azure の <strong>アプリの登録</strong> で確認した値です。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>ソースコードの中に入っていた docker/config/authsources.php の設定を真似しています。</p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image12.png\" alt=\"ソースコードの中に入っていた docker/config/authsources.php の設定\" width=\"1268\" height=\"447\" loading=\"lazy\"></a></p>\n</blockquote>\n\n<br />\n\nなお、issuer の値は、以下の出力で確認できます。\n\n```shellsession\n# apt install curl -y\n# apt install jq -y\n# curl https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0/.well-known/openid-configuration | jq\n```\n\n```json\n{\n  \"token_endpoint\": \"https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/oauth2/v2.0/token\",\n  \"token_endpoint_auth_methods_supported\": [\n    \"client_secret_post\",\n    \"private_key_jwt\",\n    \"client_secret_basic\"\n  ],\n  \"jwks_uri\": \"https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/discovery/v2.0/keys\",\n  \"response_modes_supported\": [\n    \"query\",\n    \"fragment\",\n    \"form_post\"\n  ],\n  \"subject_types_supported\": [\n    \"pairwise\"\n  ],\n  \"id_token_signing_alg_values_supported\": [\n    \"RS256\"\n  ],\n  \"response_types_supported\": [\n    \"code\",\n    \"id_token\",\n    \"code id_token\",\n    \"id_token token\"\n  ],\n  \"scopes_supported\": [\n    \"openid\",\n    \"profile\",\n    \"email\",\n    \"offline_access\"\n  ],\n  \"issuer\": \"https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0\",\n  \"request_uri_parameter_supported\": false,\n...\n```\n\n<br />\n\nauthoauth2 モジュールを有効化します。\n\n<br />\n\nauthoauth2 モジュールとは、今回、composer でインストールした cirrusidentity/simplesamlphp-module-authoauth2 のモジュール名です。  \nこれを認識させる設定を追加します。\n\n<blockquote class=\"warn\">\n<p>有効にしないと Test のときに以下のエラーになります。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">SimpleSAML\\Error\\Error: UNHANDLEDEXCEPTION</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/Error/ExceptionHandler.php:32 (SimpleSAML\\Error\\ExceptionHandler::customExceptionHandler)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/error-handler/ErrorHandler.php:541 (Symfony\\Component\\ErrorHandler\\ErrorHandler::handleException)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">0 [builtin] (N/A)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: Exception: The module 'authoauth2' is not enabled.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">8 src/SimpleSAML/Module.php:449 (SimpleSAML\\Module::resolveClass)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">7 src/SimpleSAML/Auth/Source.php:313 (SimpleSAML\\Auth\\Source::parseAuthSource)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">6 src/SimpleSAML/Auth/Source.php:356 (SimpleSAML\\Auth\\Source::getById)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">5 src/SimpleSAML/Auth/Simple.php:62 (SimpleSAML\\Auth\\Simple::getAuthSource)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">4 src/SimpleSAML/Auth/Simple.php:151 (SimpleSAML\\Auth\\Simple::login)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">3 [builtin] (call_user_func_array)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/HTTP/RunnableResponse.php:68 (SimpleSAML\\HTTP\\RunnableResponse::sendContent)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/http-foundation/Response.php:373 (Symfony\\Component\\HttpFoundation\\Response::send)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">0 public/module.php:24 (N/A)</span></p>\n</blockquote>\n\n```shellsession\n# vi config/config.php\n```\n\n```php:/var/simplesamlphp/config/config.php\n    'module.enable' => [\n        'exampleauth' => false,\n        'core' => true,\n        'admin' => true,\n        'saml' => true,//末尾に , 追加必要なのに注意\n        'authoauth2' => true//追加\n    ],\n```\n\n<br />\n\nついでに、ログ出力がデフォルトで syslog になっているため、ファイルに出力するようにします。（実施は任意です。）  \nまた、ログレベルを DEBUG に引き上げておきます。（実施は任意です。）\n\n```php:/var/simplesamlphp/config/config.php\n    //'loggingdir' => '/var/log/',\n    'loggingdir' => '/var/log/simplesaml',\n...\n    //'logging.level' => SimpleSAML\\Logger::NOTICE,\n    'logging.level' => SimpleSAML\\Logger::DEBUG,\n    //'logging.handler' => 'syslog',\n    'logging.handler' => 'file',\n```\n\n```shellsession\n# mkdir /var/log/simplesaml\n# chown www-data: /var/log/simplesaml\n```\n\n<br />\n\n# Test\n\n## Test1\n\n`https://ssp2.example.com/simplesaml/admin` にて、  \n**Test** タブをクリックして、  \n**microsoftOIDCSource** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image13.png\" alt=\"simplesaml/admin画面 Testタブクリック\" width=\"995\" height=\"532\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image14.png\" alt=\"microsoftOIDCSource をクリック\" width=\"998\" height=\"345\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image15.png\" alt=\"Error: UNHANDLEDEXCEPTION\" width=\"934\" height=\"796\" loading=\"lazy\"></a>\n\n<br />\n\n以下のエラーになりました。\n\n<br />\n\n<span style=\"color: #e70500;background-color: #ffebe7;\">SimpleSAML\\Error\\Error: UNHANDLEDEXCEPTION</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/Error/ExceptionHandler.php:32 (SimpleSAML\\Error\\ExceptionHandler::customExceptionHandler)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/error-handler/ErrorHandler.php:541 (Symfony\\Component\\ErrorHandler\\ErrorHandler::handleException)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">0 [builtin] (N/A)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: GuzzleHttp\\Exception\\ConnectException: cURL error 28: Resolving timed out after 3000 milliseconds (see `https://curl.haxx.se/libcurl/c/libcurl-errors.html`) for `https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0/.well-known/openid-configuration`</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">26 vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php:210 (GuzzleHttp\\Handler\\CurlFactory::createRejection)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">（略）</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">4 src/SimpleSAML/Auth/Simple.php:165 (SimpleSAML\\Auth\\Simple::login)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">3 [builtin] (call_user_func_array)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/HTTP/RunnableResponse.php:68 (SimpleSAML\\HTTP\\RunnableResponse::sendContent)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/http-foundation/Response.php:373 (Symfony\\Component\\HttpFoundation\\Response::send)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">0 public/module.php:24 (N/A)</span>\n\n<br />\n\n今回の場合、  \n<span style=\"color: red;\"><strong>SimpleSAMLphp からプロキシサーバーを使って、インターネット（`https://login.microsoftonline.com/...`）に出る必要があるため、エラーになりました。</strong></span>  \n<span style=\"color: red;\"><strong>SimpleSAMLphp にプロキシサーバーの設定が必要です。</strong></span>\n\n<br />\n\nNG：  \n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image16.png\" alt=\"プロキシサーバーを使う場合のNGパターン 図\" width=\"871\" height=\"306\" loading=\"lazy\"></a>\n\n<br />\n\nOK：  \n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image17.png\" alt=\"プロキシサーバーを使う場合のOKパターン 図\" width=\"871\" height=\"306\" loading=\"lazy\"></a>\n\n<br />\n\n## プロキシサーバー設定\n\n<blockquote class=\"warn\">\n<p>SimpleSAMLphp からプロキシサーバーを使わずに直接インターネットアクセスできる場合は、この設定は必要ありません。</p>\n</blockquote>\n\nSimpleSAMLphp → プロキシサーバー → インターネット（`https://login.microsoftonline.com/...`）の通信が可能になるようにプロキシサーバーの設定をします。  \n今回プロキシサーバーは、`http://192.168.0.158:3128` とします。\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>/etc/environment などサーバー全体の環境変数で指定しても効きません。</strong></span></p>\n</blockquote>\n\n```shellsession\n# cd /var/simplesamlphp\n# vi config/authsources.php\n```\n\n```php:/var/simplesamlphp/config/authsources.php\n    'microsoftOIDCSource' => [\n        'authoauth2:OpenIDConnect',\n        'issuer' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0',\n        // When using the 'common' discovery endpoint it allows any Azure user to authenticate, however\n        // the token issuer is tenant specific and will not match what is in the common discovery document.\n        'validateIssuer' => false,  // issuer is just used to confirm correct discovery endpoint loaded\n        'discoveryUrl' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0/.well-known/openid-configuration',\n        'clientId' => '3******6-0**9-4**b-85bd-f**********8',\n        'clientSecret' => 'sv************************************Z_',\n        'proxy' => 'http://192.168.0.158:3128',//←追加\n    ],\n```\n\n<br />\n\nなお、上記設定以外の方法では、php-fpm の環境変数としてセットすると、プロキシサーバーが有効になります。\n\n<blockquote class=\"info\">\n<p>この対応の場合、authsources.php の <code>'proxy'</code> 設定は不要です。ソースコードを見ると、<code>$_SERVER[\"HTTP_PROXY\"]</code> の存在をチェックしていました。</p>\n</blockquote>\n\n```shellsession\n# vi /etc/nginx/fastcgi_params\nfastcgi_param\tHTTP_PROXY\thttp://192.168.0.158:3128;\nfastcgi_param\tHTTPS_PROXY\thttp://192.168.0.158:3128;\nfastcgi_param\tNO_PROXY\t.example.com;\n# systemctl restart nginx\n```\n\n<br />\n\n## Test2\n\n再び  \n**Test** タブをクリックして、  \n**microsoftOIDCSource** をクリックします。\n\n<br />\n\nプロキシサーバー問題はなくなりましたが、まだエラーになりました。（今度のエラーは、成功したり、失敗したりです。）  \n\n<br />\n\n<span style=\"color: #e70500;background-color: #ffebe7;\">SimpleSAML\\Error\\Error: UNHANDLEDEXCEPTION</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/Error/ExceptionHandler.php:32 (SimpleSAML\\Error\\ExceptionHandler::customExceptionHandler)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/error-handler/ErrorHandler.php:541 (Symfony\\Component\\ErrorHandler\\ErrorHandler::handleException)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">0 [builtin] (N/A)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: GuzzleHttp\\Exception\\ConnectException: cURL error 28: Operation timed out after 3001 milliseconds with 0 bytes received (see `https://curl.haxx.se/libcurl/c/libcurl-errors.html`) for `https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0/.well-known/openid-configuration`</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">26 vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php:210 (GuzzleHttp\\Handler\\CurlFactory::createRejection)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">（略）</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">4 src/SimpleSAML/Auth/Simple.php:165 (SimpleSAML\\Auth\\Simple::login)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">3 [builtin] (call_user_func_array)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/HTTP/RunnableResponse.php:68 (SimpleSAML\\HTTP\\RunnableResponse::sendContent)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/http-foundation/Response.php:373 (Symfony\\Component\\HttpFoundation\\Response::send)</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">0 public/module.php:24 (N/A)</span>\n\n<br />\n\n<span style=\"color: red;\"><strong>デフォルトの 3 秒を超えて応答が無い場合、エラーになるようでした。</strong></span>\n<span style=\"color: red;\"><strong>タイムアウトの秒数を 10 秒に変更します。</strong></span>\n\n```shellsession\n# cd /var/simplesamlphp\n# vi config/authsources.php\n```\n\n```php:/var/simplesamlphp/config/authsources.php\n    'microsoftOIDCSource' => [\n        'authoauth2:OpenIDConnect',\n        'issuer' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0',\n        // When using the 'common' discovery endpoint it allows any Azure user to authenticate, however\n        // the token issuer is tenant specific and will not match what is in the common discovery document.\n        'validateIssuer' => false,  // issuer is just used to confirm correct discovery endpoint loaded\n        'discoveryUrl' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/v2.0/.well-known/openid-configuration',\n        'clientId' => '3******6-0**9-4**b-85bd-f**********8',\n        'clientSecret' => 'sv************************************Z_',\n        'proxy' => 'http://192.168.0.158:3128',\n        'timeout' => 10,//←追加\n    ],\n```\n\n<br />\n\n## Test3\n\n**Test** タブをクリックして、  \n**microsoftOIDCSource** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" alt=\"Azure ADサインイン画面\" width=\"878\" height=\"613\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image18.png\" alt=\"Azure AD(Entra ID)のConsent画面（同意/承諾画面）\" width=\"1115\" height=\"733\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image19.png\" alt=\"SimpleSAMLphpのTestログイン後画面\" width=\"882\" height=\"913\" loading=\"lazy\"></a>\n\n<br />\n\n成功しました！ヨシ！\n\n<br />\n\n# Web アプリ（info.php）SSO 対応\n\nAzure AD - アプリの登録 - SimpleSAMLphpOIDC - 認証  \nに  \nリダイレクト URI  \n`https://webapps-php.example.com/simplesaml/module.php/authoauth2/linkback.php`  \nを追加します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/image20.png\" alt=\"Azure AD - アプリの登録 - SimpleSAMLphpOIDC - 認証 - リダイレクト URI\" width=\"1259\" height=\"1297\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>Web アプリを SSO 対応に改修します。</strong></span>\n\n```shellsession\n# vi /opt/webapps/php/info.php\n```\n\n```php:/opt/webapps/php/info.php\n<?php\nrequire_once('/var/simplesamlphp/lib/_autoload.php');\nuse SimpleSAML\\Auth\\Simple;\n$as = new Simple('microsoftOIDCSource');\n$as->requireAuth();\nphpinfo();\n```\n\n```shellsession\n# vi /etc/nginx/conf.d/webapps-info.conf\n```\n\n```nginx:/etc/nginx/conf.d/webapps-info.conf\n# location / の上に追記\n  location ^~ /simplesaml {\n    index index.php;\n    alias /var/simplesamlphp/public;\n\n    location ~^(?<prefix>/simplesaml)(?<phpfile>.+?\\.php)(?<pathinfo>/.*)?$ {\n      include      fastcgi_params;\n      # fastcgi_pass   $fastcgi_pass;\n      fastcgi_pass   unix:/run/php/php8.3-fpm.sock;\n      fastcgi_param SCRIPT_FILENAME $document_root$phpfile;\n\n      # Must be prepended with the baseurlpath\n      fastcgi_param SCRIPT_NAME /simplesaml$phpfile;\n\n      fastcgi_param PATH_INFO $pathinfo if_not_empty;\n    }\n  }\n  location /\n```\n\n```shellsession\n# systemctl restart nginx\n```\n\n<br />\n\n`https://webapps-php.example.com/info.php` にアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" alt=\"Azure ADサインイン画面\" width=\"878\" height=\"613\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>Test でログインしたままブラウザを再起動していない場合、再認証はかかりません。</p>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" alt=\"phpinfo()の画面\" width=\"1249\" height=\"373\" loading=\"lazy\"></a>\n\n<br />\n\nヨシっ！\n\n<br />\n","description":"SimpleSAMLphpでOpenID Connect(OIDC)のRelying Party(RP)を構築して、OpenID Provider(OP)は、Azure AD(Microsoft Entra ID)とし、Azure AD のユーザーアカウントで認証まで行いました。その全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"yj89dylqd4m","createdAt":"2023-12-29T08:23:09.631Z","updatedAt":"2023-12-29T08:23:09.631Z","publishedAt":"2023-12-29T08:23:09.631Z","revisedAt":"2023-12-29T08:23:09.631Z","url":"https://itc-engineering-blog.imgix.net/simplesaml-oidc-azure/ITC_Engineering_Blog.png","alt":"Nginx&SimpleSAMLphpをOIDC対応RP化してAzure AD(Entra ID)認証","width":1200,"height":630}],"seo_authors":[]},{"id":"spring-boot-kotlin-docker","createdAt":"2022-06-12T08:56:05.704Z","updatedAt":"2023-12-16T12:39:36.115Z","publishedAt":"2022-06-12T08:56:05.704Z","revisedAt":"2023-12-16T12:39:36.115Z","title":"Kotlin ＋ Spring Boot ＋ Dockerビルド(Gradle, Fat JAR)まで全手順","category":{"id":"jgza8_u_t2xl","createdAt":"2022-06-12T08:52:15.121Z","updatedAt":"2022-06-12T08:52:15.121Z","publishedAt":"2022-06-12T08:52:15.121Z","revisedAt":"2022-06-12T08:52:15.121Z","topics":"Spring Boot","logo":"/logos/SpringBoot.png","needs_title":true},"topics":[{"id":"jgza8_u_t2xl","createdAt":"2022-06-12T08:52:15.121Z","updatedAt":"2022-06-12T08:52:15.121Z","publishedAt":"2022-06-12T08:52:15.121Z","revisedAt":"2022-06-12T08:52:15.121Z","topics":"Spring Boot","logo":"/logos/SpringBoot.png","needs_title":true},{"id":"6jp855sldd","createdAt":"2022-04-29T11:49:21.447Z","updatedAt":"2022-04-29T11:49:21.447Z","publishedAt":"2022-04-29T11:49:21.447Z","revisedAt":"2022-04-29T11:49:21.447Z","topics":"Kotlin","logo":"/logos/Kotlin.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\n\nKotlin ＋ SpringBoot で REST API 実装  \n↓  \nGradle / FatJar のビルド  \n↓  \nDocker ビルド  \nの流れを最初から最後までの全手順になります。\n\n<br />\n\n<blockquote class=\"warn\">\n<p>どちらかと言うと、Dockerビルドが主題で、REST APIの実装は、主題ではありません。そのため、かなり簡素な実装で進めます。</p>\n</blockquote>\n\n<br />\n\nこの記事の Docker ビルドについては、公式ドキュメント  \n<a href=\"https://spring.io/guides/gs/spring-boot-docker/\" target=\"_blank\">Spring Boot with Docker</a>  \nの  \nExample 3  \nKotlin 版という感じです。\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS</code></p>\n<p>　<code>IntelliJ IDEA 2022.1.2(Community Edition)</code></p>\n<p>　<code>openjdk version \"17.0.3\"</code></p>\n<p>　<code>org.springframework.boot 2.6.8</code></p>\n<p>　<code>kotlin(\"jvm\") version \"1.6.21\"</code></p>\n<p>　<code>kotlin(\"plugin.spring\") version \"1.6.21\"</code></p>\n<p>　<code>gradle-7.4.1</code></p>\n<p>　<code>Docker version 20.10.17, build 100c701</code></p>\n<p>　　<code>openjdk:17-jdk-alpine</code></p>\n</blockquote>\n\n<br />\n\n# プロジェクト作成\n\nSpring initializr にて、プロジェクトのテンプレートをダウンロードします。\n\n<br />\n\n有料の IntelliJ IDEA Ultimate には、 Spring initializr が備わっていますが、今回は、無料の IntelliJ IDEA Community を使う前提です。\n\n<blockquote class=\"info\">\n<p>【余談】</p>\n<p>initializr が initializer じゃないのが気になりましたが、</p>\n<p>initializr.com にインスパイアされたという情報がありました。</p>\n<p>その initializr.com は、なぜ initializr なのかは謎ですが...。</p>\n</blockquote>\n\n<br />\n\nProject: `Gradle Project`  \nLanguage: `Kotlin`  \nSpring Boot: `2.6.8`  \nPacking: `Jar`  \nJava: `17`  \nDependencies: `Spring Web`  \nを選択します。\n\n<br />\n\nプロジェクトの名前は、任意ですが、最初から表示されている `demo` で進めていきます。\n\n<br />\n\n選択したら、GENERATE をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image1.png\" alt=\"Spring initializrでGENERATE\" width=\"1454\" height=\"868\" loading=\"lazy\"></a>\n\n<br />\n\n# API 実装\n\nIntelliJ IDEA Community をインストールします。  \nインストール手順は、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/kotless-localstack-s3\" target=\"_blank\">Kotless と LocalStack で疑似サーバーレス - AWS S3 ダウンロード</a>」にありますので、省略して、Ubuntu に IntelliJ IDEA Community Linux をインストールしたものとして、進めていきます。\n\n<br />\n\nSpring initializr から入手した、`demo.zip` を展開します。\n\n<br />\n\n今回は、`/home/admin/demo` に展開したものとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image2.png\" alt=\"/home/admin/demoにdemo.zipを展開\" width=\"883\" height=\"593\" loading=\"lazy\"></a>\n\n<br />\n\nIntelliJ IDEA Community で読み込みます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image3.png\" alt=\"IntelliJ IDEA Community で読み込み\" width=\"792\" height=\"642\" loading=\"lazy\"></a>\n\n<br />\n\n最初は、いろいろダウンロードが始まるので、終わるまで待ちます。（作業していても良いです。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image4.png\" alt=\"IntelliJ いろいろダウンロード中\" width=\"1184\" height=\"729\" loading=\"lazy\"></a>\n\n<br />\n\n`src/main/kotlin/com/example/demo/DemoApplication.kt` を編集します。\n\n<br />\n\n最初は、以下の状態でした。\n\n```kotlin:DemoApplication.kt\npackage com.example.demo\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\n\n@SpringBootApplication\nclass DemoApplication\n\nfun main(args: Array<String>) {\n\trunApplication<DemoApplication>(*args)\n}\n```\n\n<br />\n\n編集後、以下のようにします。  \nGET /api 　 → 　`Hello world!` と返す。  \nPOST /api 　 → 　 POST パラメーターの `value` の値をそのまま返す。  \nという単純な実装です。\n\n```kotlin:DemoApplication.kt\npackage com.example.demo\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\nimport org.springframework.web.bind.annotation.*\n\n@SpringBootApplication\nclass DemoApplication\n\nfun main(args: Array<String>) {\n\trunApplication<DemoApplication>(*args)\n}\n\n@RestController\n@RequestMapping(\"/api\")\nclass IndexController {\n\t@GetMapping\n\tfun get(): String? = \"Hello world!\"\n\t@PostMapping\n\tfun post(@RequestParam(\"value\") value: String?): String? = value\n}\n```\n\n<br />\n\nテストプログラム  \n`src/test/kotlin/com/example/demo/DemoApplicationTests.kt`  \nを編集します。  \n最初は、以下の状態でした。\n\n```kotlin:DemoApplicationTests.kt\npackage com.example.demo\n\nimport org.junit.jupiter.api.Test\nimport org.springframework.boot.test.context.SpringBootTest\n\n@SpringBootTest\nclass DemoApplicationTests {\n\n\t@Test\n\tfun contextLoads() {\n\t}\n\n}\n```\n\n<br />\n\n編集後、以下のようにします。  \nGET /api 　 → 　`Hello world!` と返ってくるかどうか確認。  \n`value=hello` で POST /api 　 → 　`hello` と返ってくるかどうか確認。  \nという単純な実装です。\n\n```kotlin:DemoApplicationTests.kt\npackage com.example.demo\n\nimport org.junit.jupiter.api.Test\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc\nimport org.springframework.boot.test.context.SpringBootTest\nimport org.springframework.test.web.servlet.MockMvc\nimport org.springframework.test.web.servlet.get\nimport org.springframework.test.web.servlet.post\n\n@SpringBootTest\n@AutoConfigureMockMvc\nclass DemoApplicationTests {\n\n\t@Autowired\n\tprivate lateinit var mockMvc: MockMvc\n\n\t@Test\n\tfun get() {\n\t\tmockMvc.get(\"/api\").andExpect {\n\t\t\tstatus { isOk() }\n\t\t\tcontent {\n\t\t\t\tcontentTypeCompatibleWith(\"text/plain\")\n\t\t\t\tstring(\"Hello world!\")\n\t\t\t}\n\t\t}\n\t}\n\n\t@Test\n\tfun post() {\n\t\tmockMvc.post(\"/api\") {\n\t\t\tparam(\"value\", \"hello\")\n\t\t}.andExpect {\n\t\t\tstatus { isOk() }\n\t\t\tcontent {\n\t\t\t\tstring(\"hello\")\n\t\t\t}\n\t\t}.andDo {\n\t\t\tprint()\n\t\t\thandle {\n\t\t\t\tprintln(it.response.characterEncoding)\n\t\t\t}\n\t\t}\n\t}\n\n}\n```\n\n<br />\n\nテストを実行します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image5.png\" alt=\"IntelliJ でテストを実行\" width=\"1212\" height=\"806\" loading=\"lazy\"></a>\n\n<br />\n\nあるいは、端末から、\n\n```shellsession\n$ cd /home/admin/demo\n$ ./gradlew test\n```\n\nで実行します。\n\n<br />\n\n結果、問題無しでした。\n\n<br />\n\nアプリを実行します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/image6.png\" alt=\"IntelliJ でアプリを実行\" width=\"1208\" height=\"806\" loading=\"lazy\"></a>\n\n<br />\n\nあるいは、端末から、\n\n```shellsession\n$ cd /home/admin/demo\n$ ./gradlew bootRun\n```\n\nで実行します。\n\n<br />\n\n端末から API を呼び出してみます。\n\n```shellsession\n$ curl http://localhost:8080/api\nHello world!\n```\n\n```shellsession\n$ curl -d \"value=hello\" http://localhost:8080/api\nhello\n```\n\nＯＫです！\n\n<br />\n\n# Kotlin ビルド\n\nビルドして、Fat JAR を生成します。  \n実行可能 jar ファイル（Fat JAR）の作成を担当するタスク bootJar タスク でビルドします。  \nbootJar タスク は、SpringBoot プラグインのタスクです。\n\n```shellsession\n# cd /home/admin/demo\n# ./gradlew bootJar\n```\n\n<blockquote class=\"info\">\n<p>【 Fat JAR 】</p>\n<p>Fat JAR（または uber-jar とも呼ばれる）アーカイブは通常、すべての依存ライブラリを１つにまとめた単一のJARファイルで、Javaを使ってスタンドアロンのアプリケーションとして起動することができます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 bootJarタスク 】</p>\n<p><code>./gradlew build</code> あるいは、<code>./gradlew assemble</code> でも bootJar タスクが実行されますが、他のタスクに興味は無いため、<code>./gradlew bootJar</code> としています。</p>\n</blockquote>\n\n```shellsession\n# find build/libs\nbuild/libs\nbuild/libs/demo-0.0.1-SNAPSHOT.jar\n```\n\nビルドできました！\n\n<br />\n\n# Docker インストール\n\ndocker コマンドを使ってビルドするため、docker をインストールします。\n\n```shellsession\n# apt update\n# apt install -y apt-transport-https ca-certificates software-properties-common\n# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n# add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\"\n# apt update\n# apt install -y docker-ce\n# docker -v\nDocker version 20.10.17, build 100c701\n```\n\n<br />\n\n# Dockerfile 作成\n\nDockerfile を作成します。\n\n<br />\n\n<blockquote class=\"alert\">\n<p>注意：下記「公式ドキュメント」 は、リンク切れになりました。</p>\n</blockquote>\n\n<br />\n\n内容は、公式ドキュメント  \n<a href=\"https://spring.io/guides/gs/spring-boot-docker/\" target=\"_blank\">Spring Boot with Docker</a>  \nExample 3  \nに書いてある通りで、アプリ名の部分だけ以下のように変えました。  \n`hello.Application` → `com.example.demo.DemoApplicationKt`\n\n<br />\n\n`MANIFEST.MF` に載っている  \n`Start-Class: com.example.demo.DemoApplicationKt`  \nです。（`MANIFEST.MF`は、この後の jar 展開のところで出てきます。）\n\n```shellsession\n# cd /home/admin/demo\n# vi Dockerfile\n```\n\n```dockerfile\nFROM openjdk:17-jdk-alpine\nRUN addgroup -S spring && adduser -S spring -G spring\nUSER spring:spring\nARG DEPENDENCY=target/dependency\nCOPY ${DEPENDENCY}/BOOT-INF/lib /app/lib\nCOPY ${DEPENDENCY}/META-INF /app/META-INF\nCOPY ${DEPENDENCY}/BOOT-INF/classes /app\nENTRYPOINT [\"java\",\"-cp\",\"app:app/lib/*\",\"com.example.demo.DemoApplicationKt\"]\n```\n\n<br />\n\n# jar 展開\n\nDockerfile の中で、BOOT-INF と META-INF をコピーしています。  \nこの BOOT-INF と META-INF は、jar の中にあるため、展開しておきます。\n\n```shellsession\n# mkdir build/dependency\n# cd build/dependency\n# jar -xf ../libs/*.jar\n```\n\n```shellsession\n# apt -y install tree\n# cd ../../\n# tree -L 3 build/dependency\nbuild/dependency\n├── BOOT-INF\n│   ├── classes\n│   │   ├── application.properties\n│   │   ├── com\n│   │   ├── META-INF\n│   │   ├── static\n│   │   └── templates\n│   ├── classpath.idx\n│   ├── layers.idx\n│   └── lib\n│       ├── annotations-13.0.jar\n│       ├(略)\n│       └── tomcat-embed-websocket-9.0.63.jar\n├── META-INF\n│   └── MANIFEST.MF\n└── org\n    └── springframework\n        └── boot\n\n11 directories, 39 files\n# cat build/dependency/META-INF/MANIFEST.MF\nManifest-Version: 1.0\nMain-Class: org.springframework.boot.loader.JarLauncher\nStart-Class: com.example.demo.DemoApplicationKt\nSpring-Boot-Version: 2.6.8\nSpring-Boot-Classes: BOOT-INF/classes/\nSpring-Boot-Lib: BOOT-INF/lib/\nSpring-Boot-Classpath-Index: BOOT-INF/classpath.idx\nSpring-Boot-Layers-Index: BOOT-INF/layers.idx\n```\n\n準備完了です！\n\n<br />\n\n# Docker ビルド\n\nDocker ビルドし、コンテナを起動します。  \nイメージのタグは、公式ドキュメントと同じ `springio/gs-spring-boot-docker` にしています。\n`--build-arg DEPENDENCY=build/dependency ` により、Dockerfile 内の `${DEPENDENCY}` が `build/dependency` になるようにしています。\n\n```shellsession\n# docker build --build-arg DEPENDENCY=build/dependency -t springio/gs-spring-boot-docker .\n・・・\nStep 8/8 : ENTRYPOINT [\"java\",\"-cp\",\"app:app/lib/*\",\"com.example.demo.DemoApplicationKt\"]\n ---> Running in 5a37a2c857c1\nRemoving intermediate container 5a37a2c857c1\n ---> fbf91ab1c9ed\nSuccessfully built fbf91ab1c9ed\nSuccessfully tagged springio/gs-spring-boot-docker:latest\n# docker run -p 8080:8080 springio/gs-spring-boot-docker\n\n  .   ____          _            __ _ _\n /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n  '  |____| .__|_| |_|_| |_\\__, | / / / /\n =========|_|==============|___/=/_/_/_/\n :: Spring Boot ::                (v2.6.8)\n\n2022-06-11 10:25:52.896  INFO 1 --- [           main] com.example.demo.DemoApplicationKt       : Starting DemoApplicationKt using Java 17-ea on 2183b7ba3a37 with PID 1 (/app started by spring in /)\n・・・\n```\n\nできました！\n\n<br />\n\n# 動作確認\n\n```shellsession\n# docker images\nREPOSITORY                       TAG             IMAGE ID       CREATED         SIZE\nspringio/gs-spring-boot-docker   latest          fbf91ab1c9ed   7 minutes ago   348MB\nopenjdk                          17-jdk-alpine   264c9bdce361   11 months ago   326MB\n# docker ps\nCONTAINER ID   IMAGE                            COMMAND                  CREATED         STATUS         PORTS                                       NAMES\n2183b7ba3a37   springio/gs-spring-boot-docker   \"java -cp app:app/li…\"   3 minutes ago   Up 3 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   vigorous_lewin\n```\n\nコンテナ起動ヨシ！\n\n<br />\n\n端末から API を呼び出してみます。\n\n```shellsession\n$ curl http://localhost:8080/api\nHello world!\n```\n\n```shellsession\n$ curl -d \"value=hello\" http://localhost:8080/api\nhello\n```\n\nヨシ！！\n\n<br />\n\n# bootBuildImage\n\nここまで書いておいて、なんですが、  \nKotlin ビルド ～ Docker ビルドまで １行で済む別のビルド方法があります。\n\n```shellsession\n# ./gradlew bootBuildImage --imageName=springio/gs-spring-boot-docker\n```\n\nとすると、自動的に Docker ビルドされます。  \nDockerfile 不要です。（docker のインストールは必要です。）\n\n<br />\n\n```shellsession\n# docker images\nREPOSITORY                       TAG        IMAGE ID       CREATED        SIZE\npaketobuildpacks/run             base-cnb   d212cf6a84ff   2 days ago     88.6MB\nspringio/gs-spring-boot-docker   latest     4108c09ba48e   42 years ago   279MB\npaketobuildpacks/builder         base       beb5e13e1cee   42 years ago   980MB\n\n# docker run -it -d -p 8080:8080 springio/gs-spring-boot-docker\n# curl http://localhost:8080/api\nHello world!\n# curl -d \"value=hello\" http://localhost:8080/api\nhello\n# docker ps\nCONTAINER ID   IMAGE                            COMMAND              CREATED          STATUS          PORTS                                       NAMES\nf2433f94adc9   springio/gs-spring-boot-docker   \"/cnb/process/web\"   16 seconds ago   Up 15 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   nice_tereshkova\n# docker exec -it nice_tereshkova head -n2 /etc/*-release\n==> /etc/lsb-release <==\nDISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=18.04\n\n==> /etc/os-release <==\nNAME=\"Ubuntu\"\nVERSION=\"18.04.6 LTS (Bionic Beaver)\"\n```\n\nCREATED が `42 years ago` になっているのが気になりましたが、動作は正常でした。\n\n<br />\n\nUbuntu 18.04.6 LTS で起動しているようでした。\n","description":"Kotlin ＋ SpringBootプロジェクト作成から REST API 実装、Gradle / FatJar のビルド、Docker ビルドまで最初から最後までの全手順記事です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"y05asddygu8f","createdAt":"2022-06-12T08:53:34.798Z","updatedAt":"2022-06-12T08:53:34.798Z","publishedAt":"2022-06-12T08:53:34.798Z","revisedAt":"2022-06-12T08:53:34.798Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/spring-boot-kotlin-docker/ITC_Engineering_Blog.png","alt":"Kotlin ＋ Spring Boot ＋ Dockerビルド(Gradle, Fat JAR)まで全手順","width":1200,"height":630}],"seo_authors":[]},{"id":"simplesamlphp-nginx-azuread","createdAt":"2023-12-16T12:54:40.846Z","updatedAt":"2024-03-02T10:13:44.016Z","publishedAt":"2023-12-16T12:54:40.846Z","revisedAt":"2024-03-02T10:13:44.016Z","title":"Nginx&SimpleSAMLphpでSAMLのSPを構築 Azure ADで認証","category":{"id":"levakp7at","createdAt":"2023-11-28T08:07:06.571Z","updatedAt":"2023-11-28T08:07:06.571Z","publishedAt":"2023-11-28T08:07:06.571Z","revisedAt":"2023-11-28T08:07:06.571Z","topics":"SAML","logo":"/logos/SAML.png","needs_title":false},"topics":[{"id":"levakp7at","createdAt":"2023-11-28T08:07:06.571Z","updatedAt":"2023-11-28T08:07:06.571Z","publishedAt":"2023-11-28T08:07:06.571Z","revisedAt":"2023-11-28T08:07:06.571Z","topics":"SAML","logo":"/logos/SAML.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false},{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"9iy1ks71tv7n","createdAt":"2021-05-31T13:08:18.404Z","updatedAt":"2021-08-31T12:04:47.612Z","publishedAt":"2021-05-31T13:08:18.404Z","revisedAt":"2021-08-31T12:04:47.612Z","topics":"Nginx","logo":"/logos/Nginx.png","needs_title":false}],"content":"# はじめに\n\n<blockquote class=\"alert\">\n<p>この記事に「SSO とは」「SAML とは」「SimpleSAMLphp とは」等々、用語の説明はありません。</p>\n</blockquote>\n\nUbuntu 22.04.3 LTS に Nginx 1.18.0 & PHP & SimpleSAMLphp 2.1.1 をインストールして、SAML による SSO（シングルサインオン／Single Sign On）環境を作成しました。  \nSimpleSAMLphp 2.1.1 は、Service Provider (SP) または、Identity Provider (IdP) として機能しますが、今回は、Service Provider (SP) として使います。  \nIdP は、Azure AD（Azure Active Directory／Microsoft Entra ID）を利用します。  \n\n<br />\n\n認証対象の Web アプリケーション環境構築から SimpleSAMLphp デプロイ、Azure AD で Web アプリケーション認証まで全手順を紹介します。\n\n<blockquote class=\"warn\">\n<p>Azure AD（Azure Active Directory）は、Microsoft Entra ID に名称が変わりましたが、この記事では、Azure AD 表記のままでいきます。</p>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image1.png\" alt=\"Nginx&SimpleSAMLphpでSAMLのSPを構築 Azure ADで認証 構成図\" width=\"801\" height=\"711\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"alert\">\n<p>この記事は、2023 年 12 月現在の公式ドキュメント <a href=\"https://simplesamlphp.org/docs/stable/simplesamlphp-install.html\" target=\"_blank\">SimpleSAMLphp Installation and Configuration</a> の手順を元に、書かれていない内容まで補完したものです。</p>\n<p>時期違いで、手順が異なるかもしれません。ご了承ください。</p>\n<p>また、<span style=\"color :red;\"><strong>本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。</strong></span></p>\n</blockquote>\n\n<br />\n\n# Web アプリケーション環境構築\n\nNginx と PHP で認証対象の Web アプリケーション環境を作成します。  \nアプリケーションと言っても、PHP の関数 `phpinfo()` を実行して、内部パラメータ等を表示するだけのアプリケーションです。  \n\n<br />\n\n今回は、  \nNginx 1.18.0  \nPHP 8.3.0  \nをインストールします。  \n\n<blockquote class=\"info\">\n<p>今回、Web アプリケーションサーバーの URI は、</p>\n<p><code>https://webapps-php.example.com/info.php</code> とします。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>SimpleSAMLphp 2.x.x は、<code>PHP version >= 8.0.0</code> となっていますので、この記事執筆での最新 <code>PHP 8.3.0</code> をインストールしています。</p>\n</blockquote>\n\n```shellsession\n# apt update\n# add-apt-repository ppa:ondrej/php -y\n# apt update\n# apt -y install php8.3 php8.3-gd php8.3-mbstring php8.3-common php8.3-curl\n# php -v\nPHP 8.3.0 (cli) (built: Nov 24 2023 08:50:08) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.0, Copyright (c) Zend Technologies\n    with Zend OPcache v8.3.0, Copyright (c), by Zend Technologies\n# apt list --installed | grep apache2\n\nWARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n\napache2-bin/jammy-updates,jammy-security,now 2.4.52-1ubuntu4.7 amd64 [インストール済み、自動]\napache2-data/jammy-updates,jammy-updates,jammy-security,jammy-security,now 2.4.52-1ubuntu4.7 all [インストール済み、自動]\napache2-utils/jammy-updates,jammy-security,now 2.4.52-1ubuntu4.7 amd64 [インストール済み、自動]\napache2/jammy-updates,jammy-security,now 2.4.52-1ubuntu4.7 amd64 [インストール済み、自動]\nlibapache2-mod-php8.3/jammy,now 8.3.0-1+ubuntu22.04.1+deb.sury.org+1 amd64 [インストール済み、自動]\n# apt -y remove apache2-*\n# apt install -y php-fpm\n# vi /etc/php/8.3/fpm/pool.d/www.conf\nlisten = /run/php/php8.3-fpm.sock\n```\n\n<blockquote class=\"info\">\n<p><code>listen = /run/php/php8.3-fpm.sock</code>は、最初から設定されています。確認のみです。</p>\n</blockquote>\n\n<br />\n\nNginx をインストールします。\n\n```shellsession\n# apt install nginx -y\n# nginx -v\nnginx version: nginx/1.18.0 (Ubuntu)\n# vi /etc/nginx/fastcgi_params\n```\n\n```ini:/etc/nginx/fastcgi_params\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\n↓\nfastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\n```\n\n<blockquote class=\"warn\">\n<p><code>fastcgi_param SCRIPT_FILENAME</code> は必要です。</p>\n<p>無い場合、スクリプトの位置を特定できなくて、エラーになります。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">FastCGI sent in stderr: \"Primary script unknown\" while reading response header from upstream, client: 192.168.11.5, server: webapps-php.example.com, request: \"GET /info.php HTTP/1.1\",</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">upstream: \"fastcgi://unix:/run/php/php8.3-fpm.sock:\",</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">host: \"webapps-php.example.com\"</span></p>\n</blockquote>\n\n<br />\n\nPHP の Web アプリケーションを作成します。\n\n```shellsession\n# mkdir -p /opt/webapps/php\n# chown -R www-data: /opt/webapps\n# mkdir -p /var/log/webapps/php\n# vi /opt/webapps/php/info.php\n```\n\n```php:/opt/webapps/php/info.php\n<?php\nphpinfo();\n```\n\n<br />\n\n自己署名証明書を作成して、/etc/pki/tls 配下に配置します。\n\n```shellsession\n# openssl genrsa -out ca.key 2048\n# openssl req -new -key ca.key -out ca.csr\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:webapps-php.example.com\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\nAn optional company name []:\n# echo \"subjectAltName=DNS:*.example.com,IP:192.168.12.200\" > san.txt\n# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt -extfile san.txt\nSignature ok\nsubject=C = JP, ST = Aichi, L = Toyota, O = Default Company Ltd, CN = webapps-php.example.com\nGetting Private key\n# mkdir -p /etc/pki/tls/certs\n# mkdir /etc/pki/tls/private\n# mv ca.crt /etc/pki/tls/certs/webapps-php.crt\n# mv ca.key /etc/pki/tls/private/webapps-php.key\n# mv ca.csr /etc/pki/tls/private/webapps-php.csr\n```\n\n<br />\n\nNginx の設定を行います。\n\n```shellsession\n# vi /etc/nginx/conf.d/webapps-info.conf\n```\n\n```nginx:/etc/nginx/conf.d/webapps-info.conf\nserver  # サーバーブロックの開始\n{\n  listen 443 ssl; # サーバーが待ち受けるポート番号\n  ssl_certificate /etc/pki/tls/certs/webapps-php.crt; # TLS証明書\n  ssl_certificate_key /etc/pki/tls/private/webapps-php.key; # TLS秘密鍵\n  server_name webapps-php.example.com;  # サーバーの名前\n  access_log /var/log/webapps/php/access.log;  # アクセスログのパス\n  error_log /var/log/webapps/php/error.log;  # エラーログのパス\n\n  root /opt/webapps/php;  # サーバーのルートディレクトリ\n\n  location /  # ルートディレクトリに対する設定\n  {\n    index index.html index.htm index.php;  # デフォルトで使用するインデックスファイル\n  }\n\n  location ~ [^/]\\.php(/|$)  # .phpで終わるリクエストに対する設定\n  {\n    fastcgi_split_path_info ^(.+?\\.php)(/.*)$;  # パス情報を分割\n    if (!-f $document_root$fastcgi_script_name)  # スクリプトファイルが存在しない場合\n    {\n      return 404;  # 404エラーを返す\n    }\n\n    client_max_body_size 100m;  # クライアントからの最大ボディサイズ\n\n    # Mitigate https://httpoxy.org/ vulnerabilities\n    fastcgi_param HTTP_PROXY \"\";  # HTTP_PROXYを空に設定してhttpoxy脆弱性を緩和\n\n    # fastcgi_pass 127.0.0.1:9000;\n    fastcgi_pass unix:/run/php/php8.3-fpm.sock;  # FastCGIサーバーへのパス\n    fastcgi_index index.php;  # デフォルトのFastCGIスクリプト\n\n    # include the fastcgi_param setting\n    include fastcgi_params;  # FastCGIパラメータの設定を含む\n\n    # SCRIPT_FILENAME parameter is used for PHP FPM determining\n    #  the script name. If it is not set in fastcgi_params file,\n    # i.e. /etc/nginx/fastcgi_params or in the parent contexts,\n    # please comment off following line:\n    # fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;  # スクリプト名を決定するためのパラメータ\n  }\n}\n```\n\n<br />\n\n```shellsession\n# vi /etc/php/8.3/fpm/php.ini\n```\n\n```ini\ndate.timezone = Asia/Tokyo\n# 新規設定\n\ndisplay_errors = On\ndisplay_startup_errors = On\n# 確認（最初から設定されている。）\n```\n\n```shellsession\n# vi /etc/hosts\n192.168.12.200 webapps-php.example.com\n```\n\n<blockquote class=\"info\">\n<p>今回、Web アプリケーションサーバーの IP アドレスは、192.168.12.200 とします。</p>\n</blockquote>\n\n```shellsession\n# systemctl restart nginx\n# systemctl restart php8.3-fpm\n```\n\n<br />\n\nこの時点で、\n`https://webapps-php.example.com/info.php`  \nにアクセスすると、`phpinfo()` の画面が確認できます。  \nただし、当たり前ですが、まだ SAML に関して何もしていませんので、認証はかかりません。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" alt=\"phpinfo()の画面\" width=\"1249\" height=\"373\" loading=\"lazy\"></a>\n\n<br />\n\nとりあえず、ヨシ！\n\n<br />\n\n# SimpleSAMLphp Web コンソール環境作成\n\nphp8.3-dom、php8.3-xml を最低限必要とするため、インストールします。\n\n<blockquote class=\"info\">\n<p><a href=\"https://simplesamlphp.org/docs/stable/simplesamlphp-install.html\" target=\"_blank\">SimpleSAMLphp Documentation</a>（英語）では、以下の記述になっているため、この時点で、足りない分をインストールしています。  </p>\n<p><code>次の PHP 拡張機能のサポート:</code>  </p>\n<p><code>常に必須 : date , dom , fileinfo , filter , hash , json , libxml , mbstring , openssl , pcre , session , simplexml , sodium , SPL and zlib</code></p>\n<p>なお、<span style=\"color: red;\"><strong>php8.3-xml は、<code>simplexml</code> のこと</strong></span>です。</p>\n</blockquote>\n\n```shellsession\n# apt install -y php8.3-dom php8.3-xml\n```\n\n<br />\n\n`https://github.com/simplesamlphp/simplesamlphp/releases/tag/v2.1.1`  \nから  \nsimplesamlphp-2.1.1-full.tar.gz  \nをダウンロード、展開し、/var/simplesamlphp に配置します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image3.png\" alt=\"simplesamlphp-2.1.1-full.tar.gzダウンロード\" width=\"1302\" height=\"948\" loading=\"lazy\"></a>\n\n<br />\n\n```shellsession\n# tar xzf simplesamlphp-2.1.1-full.tar.gz\n# mv simplesamlphp-2.1.1 /var/simplesamlphp\n```\n\n<blockquote class=\"info\">\n<p><code>-full</code> と <code>-full</code> 無しの違いは、<code>-full</code> の方がいろいろモジュールが入っていてオンにすれば使えるよというもののようです。（例：LDAP 連携）</p>\n<p>今回、実は、<code>-full</code> である必要はありません。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>/var/simplesamlphp は任意ですが、ドキュメントの記述に合わせると、/var/simplesamlphp です。</p>\n</blockquote>\n\n<br />\n\nSimpleSAMLphp Web コンソール用の証明書、秘密鍵を作成します。\n\n<br />\n\n今回、SimpleSAMLphp Web コンソールの URL は、  \n`https://ssp2.example.com/simplesaml/` とします。\n\n<blockquote class=\"info\">\n<p>URL は任意です。<code>/simplesaml</code> というパスもドキュメントの記述に合わせただけで、この限りではありません。</p>\n<p><code>https://ssp2.example.com/</code> は、404 Not Found になります。</p>\n<p>ssp2 は、SimpleSAMLphp 2.x.x を省略して ssp2 にしました。海外のサイトで SimpleSAMLphp 1.x.x のことを SSP, SimpleSAMLphp 2.x.x のことを SSP2 と呼んでいる場合がありました。</p>\n</blockquote>\n\n```shellsession\n# openssl genrsa -out ca.key 2048\n# openssl req -new -key ca.key -out ca.csr\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:ssp2.example.com\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\nAn optional company name []:\n# echo \"subjectAltName=DNS:*.example.com,IP:192.168.12.200\" > san.txt\n# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt -extfile san.txt\nSignature ok\nsubject=C = JP, ST = Aichi, L = Toyota, O = Default Company Ltd, CN = ssp2.example.com\nGetting Private key\n# mv ca.crt /etc/pki/tls/certs/ssp2.example.com.crt\n# mv ca.key /etc/pki/tls/private/ssp2.example.com.key\n# mv ca.csr /etc/pki/tls/private/ssp2.example.com.csr\n```\n\n```shellsession\n# vi /etc/nginx/conf.d/simplesaml.conf\n```\n\n```nginx:/etc/nginx/conf.d/simplesaml.conf\nserver {\n    listen 443 ssl;\n    server_name ssp2.example.com;\n\n    ssl_certificate        /etc/pki/tls/certs/ssp2.example.com.crt;\n    ssl_certificate_key    /etc/pki/tls/private/ssp2.example.com.key;\n    ssl_protocols          TLSv1.3 TLSv1.2;\n    ssl_ciphers            EECDH+AESGCM:EDH+AESGCM;\n\n    location ^~ /simplesaml {\n        index index.php;\n        alias /var/simplesamlphp/public;\n\n        location ~^(?<prefix>/simplesaml)(?<phpfile>.+?\\.php)(?<pathinfo>/.*)?$ {\n            include          fastcgi_params;\n            # fastcgi_pass     $fastcgi_pass;\n            fastcgi_pass     unix:/run/php/php8.3-fpm.sock;\n            fastcgi_param SCRIPT_FILENAME $document_root$phpfile;\n\n            # Must be prepended with the baseurlpath\n            fastcgi_param SCRIPT_NAME /simplesaml$phpfile;\n\n            fastcgi_param PATH_INFO $pathinfo if_not_empty;\n        }\n    }\n}\n```\n\n<blockquote class=\"warn\">\n<p>ドキュメントに書かれていませんでしたが、</p>\n<p><span style=\"color :red;\"><strong><code>location ^~ /simplesaml {</code></strong></span></p>\n<p><span style=\"color :red;\"><strong><code> index index.php;</code></strong></span></p>\n<p><span style=\"color :red;\"><strong>の</strong></span></p>\n<p><span style=\"color :red;\"><strong><code> index index.php;</code></strong></span></p>\n<p><span style=\"color :red;\"><strong>は重要です。これが無い場合、/simplesaml/admin/（ファイル名無し）にアクセスで、/var/simplesamlphp/admin/index.php が読み込まれず、404 Not found になります。</code></strong></span></p>\n</blockquote>\n\n<br />\n\n```shellsession\n# chown -R www-data: /var/simplesamlphp\n# vi /etc/hosts\n192.168.12.200\tssp2.example.com\n# systemctl restart nginx\n# systemctl restart php8.3-fpm\n```\n\n<br />\n\n`.php.dist` を `.php` にして有効にします。  \n`.php.dist` → `.php` は設定ファイル .conf 的な位置付けで、デフォルトの設定が入っています。  \nとりあえず、ドキュメント記載の手順だけ実施します。\n\n```shellsession\n# cd /var/simplesamlphp\n# cp config/config.php.dist config/config.php\n# cp config/authsources.php.dist config/authsources.php\n# cp metadata/saml20-idp-hosted.php.dist metadata/saml20-idp-hosted.php\n# cp metadata/saml20-idp-remote.php.dist metadata/saml20-idp-remote.php\n# cp metadata/saml20-sp-remote.php.dist metadata/saml20-sp-remote.php\n```\n\n<blockquote class=\"alert\">\n<p><strong style=\"color: red;\"><strong>いろいろ設定がありますが、今回は、必要最低限の設定しかしません。以降、設定の書き換えについても同様です。</strong></span></p>\n</blockquote>\n\n<br />\n\n`https://ssp2.example.com/simplesaml/` へアクセスして、SimpleSAMLphp のコンソール画面が表示されれば、成功です。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image4.png\" alt=\"SimpleSAMLphpのコンソール画面\" width=\"1393\" height=\"629\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>この画面は、設定画面ではないため、何もできません。</p>\n<p>この後、設定画面に入るための初期設定を行います。</p>\n</blockquote>\n\n<br />\n\n# SimpleSAMLphp Web コンソール初期設定\n\n管理者パスワードと`secretsalt`とタイムゾーンを設定します。\n\n```shellsession\n# apt install composer -y\n# composer install\n# openssl rand -base64 32\nRCN2jfGolAsfOb1J4UXQrbrwevYIyz/O/o9sJWRzxTc=\n```\n\n```shellsession\n# vi /var/simplesamlphp/config/config.php\n```\n\n```php:/var/simplesamlphp/config/config.php\n//'secretsalt' => 'defaultsecretsalt',\n// ↓ 変更\n'secretsalt' => 'RCN2jfGolAsfOb1J4UXQrbrwevYIyz/O/o9sJWRzxTc=',\n\n//'auth.adminpassword' => '123',\n// ↓ 変更\n'auth.adminpassword' => 'admin',\n\n//'timezone' => null,\n// ↓ 変更\n'timezone' => 'Asia/Tokyo',\n\n// 'technicalcontact_name' => 'Administrator',\n// 'technicalcontact_email' => 'na@example.org',\n// ↓ 変更\n'technicalcontact_name' => 'Administrator',\n'technicalcontact_email' => 'admin@ssp2.example.com',\n```\n\n<br />\n\n`https://ssp2.example.com/simplesaml/admin` にアクセスして動作確認します。  \nとりあえず、設定したパスワード `admin` で管理者でログインできることを確認します。\n\n<blockquote class=\"info\">\n<p><strong style=\"color: red;\"><strong>管理者名は、<code>admin</code> です。</storng></span></p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image5.png\" alt=\"管理者ログイン\" width=\"1104\" height=\"719\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image6.png\" alt=\"管理者ログイン成功後\" width=\"1253\" height=\"1303\" loading=\"lazy\"></a>\n\n<br />\n\n管理者ログイン ヨシ！\n\n<br />\n\n# SimpleSAMLphp SP 設定\n\n設定ファイル config/authsources.php に SP の設定をします。  \nSP（SAML 認証を使うアプリ）は複数設定できますが、今回は一つですし、一番最初になるので、 `default-sp` のところに設定します。\n\n```shellsession\n# cd /var/simplesamlphp\n# vi config/authsources.php\n```\n\n```php:/var/simplesamlphp/config/authsources.php\n//    'default-sp' => [\n//        'saml:SP',\n//        'entityID' => 'https://myapp.example.org/',\n// ↓ 変更\n    'default-sp' => [\n        'saml:SP',\n        'entityID' => 'https://webapps-php.example.com',\n```\n\n<blockquote class=\"info\">\n<p><code>saml:SP</code> のところは、文字通り、SAML の SP の設定であることを意味するため、このままにします。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>entityID ＝アプリの URL ではありません。IdP と連携するための識別子を任意に決めればよいですが、通常は、アプリケーションの URL のため、<code>https://webapps-php.example.com</code> とします。</p>\n</blockquote>\n\n<br />\n\nSP の証明書を作成し、設定します。\n\n```shellsession\n# cd /var/simplesamlphp\n# cd cert\n# openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.pem\nCountry Name (2 letter code) [AU]:JP\nState or Province Name (full name) [Some-State]:Aichi\nLocality Name (eg, city) []:Toyota\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:ssp2.example.com\nEmail Address []:\n```\n\n```shellsession\n# cd ../\n# vi config/authsources.php\n```\n\n```php:/var/simplesamlphp/config/authsources.php\n//        'entityID' => 'https://webapps-php.example.com',\n// ↓ 変更\n        'entityID' => 'https://webapps-php.example.com',\n        'privatekey' => 'saml.pem', // 追記\n        'certificate' => 'saml.crt',// 追記\n```\n\n<blockquote class=\"info\">\n<p>SP から IdP へ送信される SAML リクエストなどに付けられる署名の検証に使われます。</p>\n<p>証明書が無いまま進めると、以下のエラーになりました。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">未処理例外が投げられました。</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">SimpleSAML\\Error\\Error: UNHANDLEDEXCEPTION</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">2 src/SimpleSAML/Error/ExceptionHandler.php:32 (SimpleSAML\\Error\\ExceptionHandler::customExceptionHandler)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">1 vendor/symfony/error-handler/ErrorHandler.php:541 (Symfony\\Component\\ErrorHandler\\ErrorHandler::handleException)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">0 [builtin] (N/A)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: Exception: Unable to validate Signature</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Backtrace:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">10 vendor/simplesamlphp/saml2/src/SAML2/Utils.php:196 (SAML2\\Utils::validateSignature)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">9 vendor/simplesamlphp/saml2/src/SAML2/Assertion.php:674 (SAML2\\Assertion::validate)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">8 modules/saml/src/Message.php:168 (SimpleSAML\\Module\\saml\\Message::checkSign)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">7 modules/saml/src/Message.php:646 (SimpleSAML\\Module\\saml\\Message::processAssertion)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">6 modules/saml/src/Message.php:613 (SimpleSAML\\Module\\saml\\Message::processResponse)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">5 modules/saml/src/Controller/ServiceProvider.php:310 (SimpleSAML\\Module\\saml\\Controller\\ServiceProvider::assertionConsumerService)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">4 vendor/symfony/http-kernel/HttpKernel.php:163 (Symfony\\Component\\HttpKernel\\HttpKernel::handleRaw)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">3 vendor/symfony/http-kernel/HttpKernel.php:75 (Symfony\\Component\\HttpKernel\\HttpKernel::handle)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">2 vendor/symfony/http-kernel/Kernel.php:202 (Symfony\\Component\\HttpKernel\\Kernel::handle)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">1 src/SimpleSAML/Module.php:234 (SimpleSAML\\Module::process)</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">0 public/module.php:17 (N/A)</span></p>\n</blockquote>\n\n<br />\n\n# SP のメタデータ(XML)取得\n\nSP のメタデータ（XML ファイル）を取得します。取得したメタデータは、次のステップで、IdP（Azure AD）に登録します。\n\n<br />\n\n`https://ssp2.example.com/simplesaml/admin/` にアクセスして、  \n**連携** タブをクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image7.png\" alt=\"連携タブクリック\" width=\"1246\" height=\"224\" loading=\"lazy\"></a>\n\n<br />\n\nYou can get the metadata XML on a dedicated URL:  \nのところのダウンロードアイコンをクリックして、ダウンロードします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image8.png\" alt=\"ダウンロードアイコンをクリック\" width=\"1240\" height=\"584\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image9.png\" alt=\"default-sp.xmlダウンロード\" width=\"1206\" height=\"189\" loading=\"lazy\"></a>\n\n<br />\n\n```xml:メタデータ（例）\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" entityID=\"https://webapps-php.example.com\">\n  <md:SPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n    <md:KeyDescriptor use=\"signing\">\n      <ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n        <ds:X509Data>\n          <ds:X509Certificate>MIIEtzCCAx+gAwIBAgIUNOok9kycnt+3+p45fjn8omQot7UwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBUFpY2hpMQ8wDQYDVQQHDAZUb3lvdGExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPaWRwLmV4YW1wbGUuY29tMB4XDTIzMTIwNjA1NTQwNFoXDTMzMTIwNTA1NTQwNFowazELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBUFpY2hpMQ8wDQYDVQQHDAZUb3lvdGExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPaWRwLmV4YW1wbGUuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0XyeOdgaI/OmCiXa2cIrCOr6oB5GXH6X1DctxVZewIvf6zbjJzbl+G9EEUCrm8uCww0Va8JLpyTG7b3ZgjRrpfEmIgj1Jm6Zxpnsv+LuQozC08vcASENn4vBEmm7B/jTxXaqZm8A1/N8VbIvGuUtVrxTkii02ez6FxSlJAWxDgB4WYg5rVAu2P+rkPwvr70EvcSh5BinDuX5m04oAPrtTZ/hBt3AAGYAqK8+VVAF8G7qqApjz690ntNCwAZLoDZ1lsO3tEaOXFmDqZwf9GGqy+X0UHazH200/xpGSiQm029qAuIQe7VB+l08RQj/3sHAPYlBMu8q5bQmK1b/xiYXF+/42l8aeyUQChBofDgfqMUHYhP4ozmajMaCnI9nbeKbCahFnq3pvEKyjhbu6ziCV5QixBcn+c5biYgLW++gCQcB6/4gXFEMxHDOtm4lg0UN4r34npcVoIKknRQN0Ht1rl+jGZxB3L9JRi1CCa8APXsQwUFVvxhT/zWqGvDEqWF/AgMBAAGjUzBRMB0GA1UdDgQWBBSSebFYBsuEZcVdeVDXcGXx7+WQrjAfBgNVHSMEGDAWgBSSebFYBsuEZcVdeVDXcGXx7+WQrjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBgQA6FmMPNKm381GJfs/HFI584N7FA1R2M4RqXmKgSkSJlPsDky2H81V7wllKI8JA4ULnOpV7zoIjgrlaHW5lg970UkxBe2vx1Zmb295mT5bFcCVXadNNgmSO7MRJqy4DzzQAxr51xxyCYLwpEoPjq3JQTjfXj+wE/LBKO25hIkAVPRBNK2rXYFV+eRG83005UxDx2k9qhMnHXx08DuePS+WShNLfEFIg/xToiOvbkgyPsyyxI2xqgxA4VNZ0ICj7azi5wmEyAsPdOM74S8tY5IQP8wIG3JhOgowTptU3JoiPCuqWikZR8O2UELCFHnLBU/9eEuZ3XxICUqe3Vf1Tringp7zl3dbkytllPVFGvNwdCp0Jf5cY2Y2x75w/o1L/6HIFelItpxAlio58gf4Q0GgaDSANUVFcQ7sHjnq1LsSTZe76ombYfKpyOEy3dzMjb8AjcbOAxbE5zKubZIjd1rYoSyHy010v0d1jcyJKo5Du0pzKbmXL0KRrWznlsTRJYYc=</ds:X509Certificate>\n        </ds:X509Data>\n      </ds:KeyInfo>\n    </md:KeyDescriptor>\n    <md:KeyDescriptor use=\"encryption\">\n      <ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n        <ds:X509Data>\n          <ds:X509Certificate>MIIEtzCCAx+gAwIBAgIUNOok9kycnt+3+p45fjn8omQot7UwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBUFpY2hpMQ8wDQYDVQQHDAZUb3lvdGExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPaWRwLmV4YW1wbGUuY29tMB4XDTIzMTIwNjA1NTQwNFoXDTMzMTIwNTA1NTQwNFowazELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBUFpY2hpMQ8wDQYDVQQHDAZUb3lvdGExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPaWRwLmV4YW1wbGUuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0XyeOdgaI/OmCiXa2cIrCOr6oB5GXH6X1DctxVZewIvf6zbjJzbl+G9EEUCrm8uCww0Va8JLpyTG7b3ZgjRrpfEmIgj1Jm6Zxpnsv+LuQozC08vcASENn4vBEmm7B/jTxXaqZm8A1/N8VbIvGuUtVrxTkii02ez6FxSlJAWxDgB4WYg5rVAu2P+rkPwvr70EvcSh5BinDuX5m04oAPrtTZ/hBt3AAGYAqK8+VVAF8G7qqApjz690ntNCwAZLoDZ1lsO3tEaOXFmDqZwf9GGqy+X0UHazH200/xpGSiQm029qAuIQe7VB+l08RQj/3sHAPYlBMu8q5bQmK1b/xiYXF+/42l8aeyUQChBofDgfqMUHYhP4ozmajMaCnI9nbeKbCahFnq3pvEKyjhbu6ziCV5QixBcn+c5biYgLW++gCQcB6/4gXFEMxHDOtm4lg0UN4r34npcVoIKknRQN0Ht1rl+jGZxB3L9JRi1CCa8APXsQwUFVvxhT/zWqGvDEqWF/AgMBAAGjUzBRMB0GA1UdDgQWBBSSebFYBsuEZcVdeVDXcGXx7+WQrjAfBgNVHSMEGDAWgBSSebFYBsuEZcVdeVDXcGXx7+WQrjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBgQA6FmMPNKm381GJfs/HFI584N7FA1R2M4RqXmKgSkSJlPsDky2H81V7wllKI8JA4ULnOpV7zoIjgrlaHW5lg970UkxBe2vx1Zmb295mT5bFcCVXadNNgmSO7MRJqy4DzzQAxr51xxyCYLwpEoPjq3JQTjfXj+wE/LBKO25hIkAVPRBNK2rXYFV+eRG83005UxDx2k9qhMnHXx08DuePS+WShNLfEFIg/xToiOvbkgyPsyyxI2xqgxA4VNZ0ICj7azi5wmEyAsPdOM74S8tY5IQP8wIG3JhOgowTptU3JoiPCuqWikZR8O2UELCFHnLBU/9eEuZ3XxICUqe3Vf1Tringp7zl3dbkytllPVFGvNwdCp0Jf5cY2Y2x75w/o1L/6HIFelItpxAlio58gf4Q0GgaDSANUVFcQ7sHjnq1LsSTZe76ombYfKpyOEy3dzMjb8AjcbOAxbE5zKubZIjd1rYoSyHy010v0d1jcyJKo5Du0pzKbmXL0KRrWznlsTRJYYc=</ds:X509Certificate>\n        </ds:X509Data>\n      </ds:KeyInfo>\n    </md:KeyDescriptor>\n    <md:SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://ssp2.example.com/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp\"/>\n    <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://ssp2.example.com/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp\" index=\"0\"/>\n    <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact\" Location=\"https://ssp2.example.com/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp\" index=\"1\"/>\n  </md:SPSSODescriptor>\n  <md:ContactPerson contactType=\"technical\">\n    <md:GivenName>Administrator</md:GivenName>\n    <md:EmailAddress>mailto:admin@ssp2.example.com</md:EmailAddress>\n  </md:ContactPerson>\n</md:EntityDescriptor>\n```\n\n<br />\n\n# Azure AD エンタープライズアプリケーション作成\n\nIdP（Azure AD）側の設定を行います。  \nAzure ポータルから、**Microsoft Entra ID** に移動して、**エンタープライズ アプリケーション** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image27.png\" alt=\"Microsoft Entra ID\" width=\"1255\" height=\"213\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image28.png\" alt=\"エンタープライズ アプリケーション\" width=\"1254\" height=\"616\" loading=\"lazy\"></a>\n\n<br />\n\n**＋新しいアプリケーション** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image29.png\" alt=\"＋新しいアプリケーション\" width=\"1256\" height=\"376\" loading=\"lazy\"></a>\n\n<br />\n\n**＋独自のアプリケーション作成** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image30.png\" alt=\"＋独自のアプリケーション作成\" width=\"1252\" height=\"331\" loading=\"lazy\"></a>\n\n<br />\n\nお使いのアプリの名前は何ですか?  \nのところを `SimpleSAMLphpIdP` とします。  \n`ギャラリーに見つからないその他のアプリケーションを統合します (ギャラリー以外)` にチェックが入った状態とし、**作成** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image10.png\" alt=\"アプリの名前SimpleSAMLphpIdP\" width=\"1203\" height=\"612\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>アプリの名前は任意です。</p>\n</blockquote>\n\n<br />\n\n**シングル サインオンの設定** のところの **作業の開始** をクリックします。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image11.png\" alt=\"シングル サインオンの設定\" width=\"1205\" height=\"613\" loading=\"lazy\"></a>\n\n<br />\n\n**SAML** をクリックします。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image12.png\" alt=\"SAML\" width=\"1203\" height=\"699\" loading=\"lazy\"></a>\n\n<br />\n\n**メタデータ ファイルをアップロードする** をクリックして、SP のメタデータ（XML）をアップロードし、**保存** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image13.png\" alt=\"メタデータ ファイルをアップロードする\" width=\"1204\" height=\"614\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image14.png\" alt=\"メタデータ ファイル参照→追加\" width=\"1205\" height=\"391\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image15.png\" alt=\"基本的な SAML 構成\" width=\"1516\" height=\"938\" loading=\"lazy\"></a>\n\n<br />\n\n**ユーザーとグループ** をクリックし、**＋ユーザーまたはグループの追加** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image16.png\" alt=\"＋ユーザーまたはグループの追加\" width=\"1201\" height=\"450\" loading=\"lazy\"></a>\n\n<br />\n\n割り当ての追加 画面に切り替わり、**選択されていません** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image17.png\" alt=\"選択されていません\" width=\"1202\" height=\"345\" loading=\"lazy\"></a>\n\n<br />\n\nSAML 認証を使うユーザー／グループを選択し、**選択** をクリックします。  \n今回は、`すべてのユーザー` にします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image18.png\" alt=\"すべてのユーザー\" width=\"1203\" height=\"634\" loading=\"lazy\"></a>\n\n<br />\n\n**割り当て** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image19.png\" alt=\"割り当て\" width=\"1204\" height=\"433\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image20.png\" alt=\"割り当て完了後\" width=\"1207\" height=\"423\" loading=\"lazy\"></a>\n\n<br />\n\n# IdP のメタデータ(XML)取得\n\nIdP（Azure AD）のメタデータ（XML ファイル）を取得します。取得したメタデータは、次のステップで、SP（SimpleSAMLphp）に登録します。\n\n<br />\n\n**シングル サインオン** をクリックして、  \nフェデレーション メタデータ XML のところの **ダウンロード** をクリックします。\nこれにより、SimpleSAMLphpIdP.xml を入手します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image21.png\" alt=\"フェデレーション メタデータXMLダウンロード\" width=\"1205\" height=\"1020\" loading=\"lazy\"></a>\n\n<br />\n\n# SP に IdP のメタデータ(XML)登録\n\n`https://ssp2.example.com/simplesaml/module.php/admin/federation/metadata-converter`  \nにアクセスして、SimpleSAMLphpIdP.xml を読み込み、パースします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image22.png\" alt=\"SimpleSAMLphpIdP.xml読み込み\" width=\"1190\" height=\"843\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image23.png\" alt=\"SimpleSAMLphpIdP.xmlパース\" width=\"1192\" height=\"235\" loading=\"lazy\"></a>\n\n<br />\n\nパースした結果、以下のように `$metadata['...` が得られますので、コピーします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image24.png\" alt=\"パースした結果をコピー\" width=\"1192\" height=\"835\" loading=\"lazy\"></a>\n\n<br />\n\nコピーした PHP 形式のメタデータを saml20-idp-remote.php に設定します。\n\n```shellsession\n# cd /var/simplesamlphp\n# vi metadata/saml20-idp-remote.php\n```\n\n```php:/var/simplesamlphp/metadata/saml20-idp-remote.php\n// メタデータ例\n$metadata['https://sts.windows.net/0******2-f**7-4**c-9**4-1**********b/'] = [\n    'entityid' => 'https://sts.windows.net/0******2-f**7-4**c-9**4-1**********b/',\n    'contacts' => [],\n    'metadata-set' => 'saml20-idp-remote',\n    'SingleSignOnService' => [\n        [\n            'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',\n            'Location' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/saml2',\n        ],\n        [\n            'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',\n            'Location' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/saml2',\n        ],\n    ],\n    'SingleLogoutService' => [\n        [\n            'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',\n            'Location' => 'https://login.microsoftonline.com/0******2-f**7-4**c-9**4-1**********b/saml2',\n        ],\n    ],\n    'ArtifactResolutionService' => [],\n    'NameIDFormats' => [],\n    'keys' => [\n        [\n            'encryption' => false,\n            'signing' => true,\n            'type' => 'X509Certificate',\n            'X509Certificate' => 'MIIC8DC...(long string)...YzKYfFIvL',\n        ],\n    ],\n];\n```\n\n<br />\n\nIdP（Azure AD）を設定します。  \nこのとき、`$metadata` の `entityid` の URL を設定します。\n\n```shellsession\n# vi config/authsources.php\n```\n\n```php:/var/simplesamlphp/config/authsources.php\n//    'default-sp' => [\n//        'saml:SP',\n//...\n//        'idp' => null,\n// ↓ 変更\n    'default-sp' => [\n        'saml:SP',\n//...\n        'idp' => 'https://sts.windows.net/0******2-f**7-4**c-9**4-1**********b/',\n```\n\n<br />\n\n# Test\n\n`https://ssp2.example.com/simplesaml/admin` にて、  \n**Test** タブをクリックして、  \n**default-sp** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image25.png\" alt=\"Testタブクリック\" width=\"1246\" height=\"224\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image26.png\" alt=\"default-spをクリック\" width=\"1086\" height=\"334\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" alt=\"Azure ADサインイン画面\" width=\"878\" height=\"613\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image27.png\" alt=\"Test成功画面\" width=\"1371\" height=\"1285\" loading=\"lazy\"></a>\n\n<br />\n\nOK!\n\n<br />\n\nAzure AD のアカウントでサインインできれば、ひとまず問題なしです。\n\n<br />\n\n# Web アプリ（info.php）SSO 対応\n\nAzure AD - エンタープライズアプリケーション - SimpleSAMLphpIdP  \nに  \n応答 URL (Assertion Consumer Service URL)  \n`https://webapps-php.example.com/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp`  \nを追加します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image28.png\" alt=\"シングル サインオン→編集\" width=\"1198\" height=\"560\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image29.png\" alt=\"応答 URL (Assertion Consumer Service URL)追加\" width=\"1199\" height=\"651\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>Web アプリを SSO 対応に改修します。</strong></span>\n\n```shellsession\n# vi /opt/webapps/php/info.php\n```\n\n```php:/opt/webapps/php/info.php\n<?php\nrequire_once('/var/simplesamlphp/lib/_autoload.php');\nuse SimpleSAML\\Auth\\Simple;\n$as = new Simple('default-sp');\n$as->requireAuth();\nphpinfo();\n```\n\n```shellsession\n# vi /etc/nginx/conf.d/webapps-info.conf\n```\n\n```nginx:/etc/nginx/conf.d/webapps-info.conf\n# location / の上に追記\n  location ^~ /simplesaml {\n    index index.php;\n    alias /var/simplesamlphp/public;\n\n    location ~^(?<prefix>/simplesaml)(?<phpfile>.+?\\.php)(?<pathinfo>/.*)?$ {\n      include      fastcgi_params;\n      # fastcgi_pass   $fastcgi_pass;\n      fastcgi_pass   unix:/run/php/php8.3-fpm.sock;\n      fastcgi_param SCRIPT_FILENAME $document_root$phpfile;\n\n      # Must be prepended with the baseurlpath\n      fastcgi_param SCRIPT_NAME /simplesaml$phpfile;\n\n      fastcgi_param PATH_INFO $pathinfo if_not_empty;\n    }\n  }\n  location /\n```\n\n```shellsession\n# systemctl restart nginx\n```\n\n<br />\n\n`https://webapps-php.example.com/info.php` にアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/keycloak-saml-azuread/image49.png\" alt=\"Azure ADサインイン画面\" width=\"878\" height=\"613\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>Test でログインしたままブラウザを再起動していない場合、再認証はかかりません。</p>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/image2.png\" alt=\"phpinfo()の画面\" width=\"1249\" height=\"373\" loading=\"lazy\"></a>\n\n<br />\n\nヨシっ！\n\n<br />\n","description":"Ubuntu22にNginx&PHP&SimpleSAMLphp 2をインストールして、SAMLによるSSO環境を作成しました。IdP は、Azure AD（Azure Active Directory／Microsoft Entra ID）を利用します。その全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"yb9-86lzi","createdAt":"2023-12-16T12:52:58.118Z","updatedAt":"2023-12-16T12:52:58.118Z","publishedAt":"2023-12-16T12:52:58.118Z","revisedAt":"2023-12-16T12:52:58.118Z","url":"https://itc-engineering-blog.imgix.net/simplesamlphp-nginx-azuread/ITC_Engineering_Blog.png","alt":"Nginx&SimpleSAMLphpでSAMLのSPを構築 Azure ADで認証","width":1200,"height":630}],"seo_authors":[]},{"id":"azure-devops-staticwebapps","createdAt":"2022-09-23T10:31:53.828Z","updatedAt":"2022-09-23T11:55:03.404Z","publishedAt":"2022-09-23T10:31:53.828Z","revisedAt":"2022-09-23T11:55:03.404Z","title":"Azure DevOpsからReact PythonアプリをAzure Static Web Appsにデプロイ","category":{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},"topics":[{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"xego85dtzyu","createdAt":"2021-06-03T13:50:33.576Z","updatedAt":"2021-08-31T12:04:26.367Z","publishedAt":"2021-06-03T13:50:33.576Z","revisedAt":"2021-08-31T12:04:26.367Z","topics":"React","logo":"/logos/React.png","needs_title":false},{"id":"91zw54wj7d","createdAt":"2021-06-05T07:05:37.594Z","updatedAt":"2021-08-31T12:03:57.429Z","publishedAt":"2021-06-05T07:05:37.594Z","revisedAt":"2021-08-31T12:03:57.429Z","topics":"Python","logo":"/logos/python.png","needs_title":false},{"id":"xnhxgx1v0","createdAt":"2022-04-08T10:53:39.471Z","updatedAt":"2022-04-08T10:53:39.471Z","publishedAt":"2022-04-08T10:53:39.471Z","revisedAt":"2022-04-08T10:53:39.471Z","topics":"TypeScript","logo":"/logos/TypeScript.png","needs_title":true}],"content":"# はじめに\nAzure DevOps に React Web アプリのリポジトリを作成し、Azure Static Web Apps にデプロイしました。\n\n<br />\n\nGitHub → Azure Static Web Apps にデプロイの場合、Azure ポータルで、Static Web Apps 作成時、\nGitHub アカウントでサインイン をクリック → GitHub サインイン画面が現れて、サインイン  \nにてできますが、Azure DevOps の場合、リポジトリを選択するところまでできるのですが、その後なぜかエラーになってできませんでした。（2022/09 現在）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image1.png\" alt=\"Azure Static Web Apps にデプロイ エラー\" width=\"995\" height=\"155\" loading=\"lazy\"></a>\n\n<span style=\"background-color: #fef0f1;\">エラー内容：Azure DevOps でこのユーザーの個人用アクセストークンを作成できませんでした。'Azure DevOps' ではなく、'Other' デプロイ ソースを使用してお客様のアプリをデプロイしてください。アプリが作成されたら、それを開き、指示に従ってトークンを取得し、アプリをデプロイします。</span>\n\n<br />\n\n<blockquote class=\"warn\">\n<p>この記事は、全般的に 2022/09 現在の状況を元に説明しています。</p>\n<p>タイミングが悪かっただけで普通にできるかもしれません。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>Azure DevOps の <strong>Pipelines</strong> を使ってデプロイします。<span style=\"color: red;\"><strong>Pipelines は、無料アカウントの場合、エラーになり、<a href=\"https://aka.ms/azpipelines-parallelism-request\" target=\"_blank\">https://aka.ms/azpipelines-parallelism-request</a> より申請が必要です。</strong></span></p>\n<p>名前、e-mail、会社名（or 会社HPのURL）、Private/Public どちらで Pipelines を使う？ を回答するだけですが、<span style=\"color: red;\"><strong>有効になるまでに約２日</strong></span>かかった記憶があります。</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/pipelines1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/pipelines1.png\" alt=\"Pipelines エラー\" width=\"1075\" height=\"318\" loading=\"lazy\"></a>\n<p><span style=\"background-color: #fef0f1;\">エラー内容：No hosted parallelism has been purchased or granted. To request a free parallelism grant, please fill out the following from https://aka.ms/azpipelines-parallelism-request</span></p>\n<p>【https://aka.ms/azpipelines-parallelism-request】</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/pipelines2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/pipelines2.png\" alt=\"Azure DevOps Parallelism Request\" width=\"855\" height=\"886\" loading=\"lazy\"></a>\n</blockquote>\n\n<br />\n\nGitHub を使って何も問題無い場合、GitHub を使えば良いですが、Azure DevOps 縛りの場合、この記事のような手順で可能でしたので、紹介していきたいと思います。\n\n<br />\n\n# 実装\n\n本記事で使用するソースコードサンプルは、GitHub にアップしました。  \n<a href=\"https://github.com/itc-lab/azure-devops-custom-auth-example\" target=\"_blank\">`https://github.com/itc-lab/azure-devops-custom-auth-example`</a>\n\n<br />\n\n**フロントエンド**： `React TypeScript`  \n**バックエンド API**： `Python (Azure Static Function用)`\n\n<br />\n\nフロントエンドの実装については、  \n別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/eslint-prettier\" target=\"_blank\">React TypeScript ESLint Prettier VSCode のプロジェクト作成</a>」  \nで `create-react-app` を使って作成したプロジェクトをベースにしています。（記事の通り、ESLint、Prettier に対応しています。）  \n肉付けした実装は、  \n「<a href=\"https://learn.microsoft.com/ja-jp/azure/developer/javascript/how-to/with-web-app/static-web-app-with-swa-cli/add-authentication\" target=\"_blank\">7. Web アプリに簡単な認証を追加する</a>」（<a href=\"https://learn.microsoft.com/ja-jp/azure/developer/javascript/how-to/with-web-app/static-web-app-with-swa-cli/add-authentication\" target=\"_blank\">`https://learn.microsoft.com/ja-jp/azure/developer/javascript/how-to/with-web-app/static-web-app-with-swa-cli/add-authentication`</a>）  \nほぼそのままです。今回、認証については関係無いので、画面が開くところまでやります。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image2.png\" alt=\"画面が開くところ\" width=\"901\" height=\"606\" loading=\"lazy\"></a>\n\n<br />\n\nPython の Azure Static Function 用 API 作成　については、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/azure-swa\" target=\"_blank\">【swa】Azure Static Web Apps をローカル環境でデバッグ</a>」で説明しています。  \nこれを `/api/hello` とし、`{ \"message\": \"メッセージ内容\" }` の JSON を返すようにしました。  \n今回、認証については関係無いので、デプロイに成功し、画面が表示されたらＯＫとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/zu1.png\" alt=\"PythonのAPI 図\" width=\"742\" height=\"596\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n# リポジトリ作成\n\n`https://dev.azure.com/` へアクセスします。  \n`＋New Project` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image3.png\" alt=\"＋New Project\" width=\"1150\" height=\"694\" loading=\"lazy\"></a>\n\n<br />\n\nプロジェクト名＝ sample-project としますので、Project name に `sample-project` を入力して、`Create` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image4.png\" alt=\"Project Create\" width=\"1156\" height=\"698\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image5.png\" alt=\"Create直後\" width=\"1114\" height=\"760\" loading=\"lazy\"></a>\n\n<br />\n\n`Repos` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image6.png\" alt=\"Repos をクリック\" width=\"1200\" height=\"821\" loading=\"lazy\"></a>\n\n<br />\n\n`Generate Git Credentials` をクリックして、Password を控えます。\n\n<blockquote class=\"alert\">\n<p>Password は二度と表示されません。</p>\n</blockquote>\n\nまた、リポジトリの URL もコピーして控えます。（こちらは、何度でも表示されます。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image7.png\" alt=\"Generate Git Credentials\" width=\"1200\" height=\"824\" loading=\"lazy\"></a>\n\n<br />\n\n以下のように、git コマンドを使って、ソースコードを push します。\n\n<blockquote class=\"info\">\n<p>ここでは、以下の条件とします。</p>\n<p><strong>リポジトリURL</strong>：<code>https://nanashinogonbe@dev.azure.com/nanashinogonbe/sample-project/_git/sample-project</code></p>\n<p><strong>プロジェクト名</strong>：<code>sample-project</code></p>\n<p><strong>ユーザー名(organization)</strong>：<code>nanashinogonbe</code></p>\n<p><strong>パスワード</strong>：<code>m26ku******************************************2arrq</code></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>ソースコードがリポジトリに登録されれば良いため、必ずしもこの方法である必要はありません。</p>\n</blockquote>\n\n```shellsession\n$ git clone https://nanashinogonbe@dev.azure.com/nanashinogonbe/sample-project/_git/sample-project\nCloning into 'sample-project'...\nPassword for 'https://nanashinogonbe@dev.azure.com':m26ku******************************************2arrq\nwarning: You appear to have cloned an empty repository.\n$ cd sample-project\n$ unzip ../ソースコード.zip\n$ git config --global user.email \"nanashinogonbe@example.com\"\n$ git config --global user.name \"Gonbe Nanashino\"\n$ git add .\n$ git commit -m \"first commit\"\n$ git push\nPassword for 'https://nanashinogonbe@dev.azure.com':m26ku******************************************2arrq\nEnumerating objects: 46, done.\nCounting objects: 100% (46/46), done.\nDelta compression using up to 2 threads\nCompressing objects: 100% (42/42), done.\nWriting objects: 100% (46/46), 163.34 KiB | 6.05 MiB/s, done.\nTotal 46 (delta 1), reused 0 (delta 0)\nremote: Analyzing objects... (46/46) (11 ms)\nremote: Storing packfile... done (70 ms)\nremote: Storing index... done (43 ms)\nTo https://dev.azure.com/nanashinogonbe/sample-project/_git/sample-project\n * [new branch]      master -> master\n```\n\n`https://dev.azure.com/`  \n↓  \n`sample-project`  \n↓  \n`Repos` - `Files`  \nを確認すると、  \n上がっているのを確認できます。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image8.png\" alt=\"Repos - Files\" width=\"1200\" height=\"741\" loading=\"lazy\"></a>\n\n<br />\n\n# Azure Static Web Apps のアプリ作成\n\nここまで単純に DevOps のリポジトリにソースコードを push しただけで、Azure Static Web Apps はまだ関係有りません。\n\n<br />\n\nデプロイ先の Azure Static Web Apps のアプリを作成します。\n\n<br />\n\nAzure ポータル（`https://portal.azure.com/`）  \n↓  \n`静的 Web アプリ`  \nを選択します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image9.png\" alt=\"Azure ポータル 静的 Web アプリ を選択\" width=\"1173\" height=\"460\" loading=\"lazy\"></a>\n\n<br />\n\n`＋作成` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image10.png\" alt=\"＋作成をクリック\" width=\"1111\" height=\"295\" loading=\"lazy\"></a>\n\n<br />\n\n**サブスクリプション**：`既存サブスクリプション`（任意）  \n**リソース グループ**：`既存リソース グループ`（任意）  \n**名前**：`sample-project`（任意）  \n**プランの種類**：`Free: 趣味または個人的なプロジェクト用`  \n**Azure Functions API とステージング環境のリージョン**：`Central US`（任意）  \n**デプロイの詳細 ソース**：`Azure DevOps`  \nを入力します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image11.png\" alt=\"デプロイの詳細 ソース Azure DevOps\" width=\"1097\" height=\"827\" loading=\"lazy\"></a>\n\n<br />\n\n**デプロイの詳細 ソース**：`Azure DevOps`  \nにより、ソースコードリポジトリ等の選択欄が出てきますので、  \n**組織**：`nanashinogonbe`  \n**プロジェクト**：`sample-project`  \n**リポジトリ**：`sample-project`  \n**分岐**：`master`  \n**ビルドのプリセット**：`Custom`  \n**アプリの場所**：`/`  \n**API の場所**：`api`  \n**出力先**：`build`  \nを入力します。  \n`確認および作成` をクリックします。（タグは登録しないものとします。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image12.png\" alt=\"ソース Azure DevOps 確認および作成をクリック\" width=\"1097\" height=\"701\" loading=\"lazy\"></a>\n\n<br />\n\n`作成` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image13.png\" alt=\"ソース Azure DevOps 作成をクリック\" width=\"1034\" height=\"655\" loading=\"lazy\"></a>\n\n<br />\n\nと、ここで、冒頭のエラーになります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image14.png\" alt=\"ソース Azure DevOps エラー\" width=\"1016\" height=\"705\" loading=\"lazy\"></a>\n\n<span style=\"background-color: #fef0f1;\">エラー内容：Azure DevOps でこのユーザーの個人用アクセストークンを作成できませんでした。'Azure DevOps' ではなく、'Other' デプロイ ソースを使用してお客様のアプリをデプロイしてください。アプリが作成されたら、それを開き、指示に従ってトークンを取得し、アプリをデプロイします。</span>\n\n<br />\n\nひとまず、`< 前へ` ボタンで戻って、エラー内容に素直に従って、`Other` を選択して、やり直します。  \n**デプロイの詳細 ソース**：`その他`  \nを選択して、`確認および作成` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image15.png\" alt=\"その他 確認および作成をクリック\" width=\"956\" height=\"992\" loading=\"lazy\"></a>\n\n<br />\n\n`作成` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image16.png\" alt=\"その他 作成をクリック\" width=\"820\" height=\"536\" loading=\"lazy\"></a>\n\n<br />\n\nしばらくすると、\"デプロイが完了しました\" となりますので、`リソースに移動` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image17.png\" alt=\"デプロイが完了しました\" width=\"1200\" height=\"409\" loading=\"lazy\"></a>\n\n<br />\n\nここまでで、URL にアクセスすると、何も無い Web アプリが作成されています。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image18.png\" alt=\"Azure Static Web Apps の URL\" width=\"1200\" height=\"497\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image19.png\" alt=\"何も無い Web アプリ\" width=\"872\" height=\"445\" loading=\"lazy\"></a>\n\n<br />\n\n# ビルド＆デプロイ\n\n`デプロイ トークンの管理` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image20.png\" alt=\"デプロイ トークンの管理\" width=\"1200\" height=\"497\" loading=\"lazy\"></a>\n\n<br />\n\nデプロイ トークンを控えます。（何回でも表示できます。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image21.png\" alt=\"デプロイ トークンをコピー\" width=\"1200\" height=\"738\" loading=\"lazy\"></a>\n\n<br />\n\n**DevOps に移動します。**\n\n<br />\n\n`https://dev.azure.com/`  \n↓  \n`sample-project`  \n↓  \n`Pipilines` をクリックします。  \n↓  \n`Create Pipelines` をクリックします。\n\n<blockquote class=\"info\">\n<p>パイプラインとは、ざっくり言うと、\"リポジトリのソースコードに対する処理\" のようなものです。DevOps では、そのルール、処理内容を <code>azure-pipelines.yml</code> に記述します。今回は、Azure Static Web Apps にデプロイする内容になっています。内容についての詳細は、別セクション <a href=\"#azure-pipelines-yaml\">azure-pipelines.yml 説明</a> に記載しています。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><code>azure-pipelines.yml</code> がリポジトリに含まれていない場合、手順が異なります。</p>\n<p>その手順については、次のセクションの <a href=\"#no-azure-pipelines\">azure-pipelines.yml が無い場合</a> に記載しています。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image22.png\" alt=\"Create Pipelinesをクリック\" width=\"1121\" height=\"685\" loading=\"lazy\"></a>\n\n<br />\n\n`Azure Repos Git` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image23.png\" alt=\"Azure Repos Gitをクリック\" width=\"1019\" height=\"687\" loading=\"lazy\"></a>\n\n<br />\n\n`sample-project` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image24.png\" alt=\"sample-projectをクリック\" width=\"1022\" height=\"676\" loading=\"lazy\"></a>\n\n<br />\n\n`Variables` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image25.png\" alt=\"Variablesをクリック\" width=\"1127\" height=\"673\" loading=\"lazy\"></a>\n\n<br />\n\n`New variable` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image26.png\" alt=\"New variableをクリック\" width=\"1128\" height=\"677\" loading=\"lazy\"></a>\n\n<br />\n\n**Name**：`deployment_token`  \n**Value**：`813fb************************************************************-********-****-****-****-****************30275`（<span style=\"color: red;\"><strong>デプロイ トークン</strong></span>）  \nを入力し、  \n`Keep this value secret` にチェックを入れて、`OK` をクリックします。  \n<span style=\"color: red;\"><strong>これにより、`azure-pipelines.yml` の $(deployment_token) が Value の値に置き換わり、先ほど作成した Azure Static Web Apps のアプリと紐づけられます。</strong></span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image27.png\" alt=\"deployment_token\" width=\"1124\" height=\"674\" loading=\"lazy\"></a>\n\n<br />\n\n`Save` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image28.png\" alt=\"Saveをクリック\" width=\"1124\" height=\"676\" loading=\"lazy\"></a>\n\n<br />\n\n`Run` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image29.png\" alt=\"Runをクリック\" width=\"1127\" height=\"673\" loading=\"lazy\"></a>\n\n<br />\n\nJobs の Status が Queud → Running になって、ビルド＆デプロイ（CI/CD パイプライン）が始まります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image30.png\" alt=\"Jobs の Status が Queud\" width=\"1124\" height=\"673\" loading=\"lazy\"></a>\n\n<br />\n\nSuccess になったら、デプロイ完了です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image31.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image31.png\" alt=\"Jobs の Status が Success\" width=\"1121\" height=\"743\" loading=\"lazy\"></a>\n\n<br />\n\n作成した Azure Static Web Apps のアプリの URL を確認します。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image2.png\" alt=\"Azure Static Web Apps のアプリの URL を確認\" width=\"901\" height=\"606\" loading=\"lazy\"></a>\n\n<br />\n\n成功しました！  \nヨシッ！\n\n<a class=\"anchor\" id=\"no-azure-pipelines\"></a>\n\n# azure-pipelines.yml が無い場合\n\n<a href=\"https://github.com/itc-lab/azure-devops-custom-auth-example\" target=\"_blank\">サンプルのソースコード</a>には、  \n`azure-pipelines.yml`  \nが含まれていて、含まれていない場合、`Create Pipelines` のところの手順が異なります。\n\n含まれていない場合の手順を書きます。\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>先に結論を言いますと、自前の <code>azure-pipelines.yml</code> が結局必要という話です。</strong></span></p>\n</blockquote>\n\n<br />\n\n`Create Pipelines` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image22.png\" alt=\"azure-pipelines.yml が無い場合　Create Pipelinesをクリック\" width=\"1121\" height=\"685\" loading=\"lazy\"></a>\n\n<br />\n\n`Azure Repos Git` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image23.png\" alt=\"azure-pipelines.yml が無い場合　Azure Repos Gitをクリック\" width=\"1019\" height=\"687\" loading=\"lazy\"></a>\n\n<br />\n\n`sample-project` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image24.png\" alt=\"azure-pipelines.yml が無い場合　sample-projectをクリック\" width=\"1022\" height=\"676\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>ここから異なります。azure-pipelines.yml が push されていない場合、YAML テンプレート選択になります。</strong></span>\n\n<br />\n\nドンピシャのものが無いため、とりあえず、それっぽい `Node.js Express Web App to Linux on Azure` を選択します。\n\n<blockquote class=\"alert\">\n<p>2022/09 現在の状況を元に説明しています。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image32.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image32.png\" alt=\"Node.js Express Web App to Linux on Azure\" width=\"1138\" height=\"901\" loading=\"lazy\"></a>\n\n<br />\n\nAzure のサブスクリプションを選択します。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image33.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image33.png\" alt=\"Azure のサブスクリプションを選択\" width=\"1159\" height=\"820\" loading=\"lazy\"></a>\n\n<br />\n\nAzure サインイン画面がポップアップしてきますので、サインインします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image34.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image34.png\" alt=\"Azure サインイン\" width=\"929\" height=\"592\" loading=\"lazy\"></a>\n\n<br />\n\n`Web App name` のところに選択肢が無く、これではないようでした。\n空白のまま、`Validate and configure` を選択します。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image35.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image35.png\" alt=\"Validate and configure\" width=\"1159\" height=\"816\" loading=\"lazy\"></a>\n\n<br />\n\n全然違う YAML が出てきました！\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image36.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/image36.png\" alt=\"YAML生成\" width=\"1200\" height=\"868\" loading=\"lazy\"></a>\n\n<br />\n\n全部消して、サンプルの `azure-pipelines.yml` のような内容に書き換えればＯＫです。  \n（その内容で `azure-pipelines.yml` が commit されます。）  \n後は同じ手順です。\n\n<br />\n\n<a class=\"anchor\" id=\"azure-pipelines-yaml\"></a>\n\n# azure-pipelines.yml 説明\n\n今回使用する `azure-pipelines.yml` の意味は、以下になります。（中にコメントで記載しています。）\n\n```yaml:azure-pipelines.yml\ntrigger:# トリガ（発動条件）を設定\n  - master\n  # masterブランチにpushされたときに発動\n\npool:# プール（ビルド環境）の設定\n  vmImage: ubuntu-latest\n  # Microsoft がホストするエージェント\n  # エージェントとは、VM(Virtual Machine)のこと\n  # ubuntu-latest、macOS-latest、windows-latest、ubuntu-[バージョン]...が設定可\n  # マイクロソフトの説明↓\n  # > Windows および Linux イメージを実行する Microsoft がホストするエージェントは、2 コア CPU、7 GB の RAM、\n  # > および 14 GB の SSD ディスク領域を備えた Azure 汎用仮想マシンにプロビジョニングされます。\n\nsteps: #Pipelinesで実行する手順の設定\n  - checkout: self\n    # このazure-pipelines.ymlが配置されているリポジトリをチェックアウト\n    submodules: true\n    # git submodule（サブモジュール）を含めるかどうか（今回は意味無い）\n  - task: AzureStaticWebApp@0# あらかじめ用意されている処理を実行\n    # AzureStaticWebApp@0 は、Azure Static Web App タスク\n    # Azure Static Web Appにデプロイするため、AzureStaticWebApp@0 は、固定\n    # このタスクを使用して、Azure Static Web アプリをビルドしてデプロイ\n    inputs:# タスクに対する引数（設定）\n      app_location: \"/\" # アプリの場所\n      # 作業ディレクトリに対するアプリケーション ソース コードのディレクトリの場所\n      # ディレクトリを移動せずにnpm buildしたいため、ここで良い\n      api_location: \"api\" # API の場所\n      # 作業ディレクトリを基準としたAzure Functionsソース コードのディレクトリの場所\n      # apiディレクトリ以下にPythonのapiソースコードがあるため、\"api\"\n      output_location: \"build\" # Output location (出力場所)\n      # ビルド後のコンパイル済みアプリケーション コードのディレクトリの場所\n      # npm build(react-scripts build)で./buildに成果物ができるため、\"build\"\n      azure_static_web_apps_api_token: $(deployment_token) # api トークンのAzure Static Web Apps\n      # デプロイ用の API トークン。 環境変数として渡された場合は必須ではありません\n      # $(deployment_token)は、DevOpsで設定する環境変数（後で説明）\n```\n\nもちろん、他にもいろいろ設定できますが、今回は最低限だけになります。\n\n<br />\n\nYAML の内容の詳しい説明は、以下にあります。  \n<a href=\"https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema\" target=\"_blank\">`https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema`</a>  \n<a href=\"https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/utility/azure-static-web-app\" target=\"_blank\">`https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/utility/azure-static-web-app`</a>\n\n<br />\n","description":"Azure DevOps に React Web アプリのリポジトリを作成し、Azure Static Web Apps にデプロイしました。ソースコードサンプルを使って、デプロイ完了までの手順をまとめました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"i5gg683cc","createdAt":"2022-09-23T10:29:20.126Z","updatedAt":"2022-09-23T10:29:20.126Z","publishedAt":"2022-09-23T10:29:20.126Z","revisedAt":"2022-09-23T10:29:20.126Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-devops-staticwebapps/ITC_Engineering_Blog.png","alt":"Azure DevOpsからReact PythonアプリをAzure Static Web Appsにデプロイ","width":1200,"height":630}],"seo_authors":[]},{"id":"fgm-dkbu10","createdAt":"2021-05-09T08:41:26.685Z","updatedAt":"2023-11-11T11:07:03.569Z","publishedAt":"2021-05-09T08:41:26.685Z","revisedAt":"2023-11-11T11:07:03.569Z","title":"Ubuntu 20.04.2.0にGitLabをインストール","category":{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},"topics":[{"id":"xexrrtp93gce","createdAt":"2021-05-09T08:36:14.468Z","updatedAt":"2021-08-31T12:05:21.841Z","publishedAt":"2021-05-09T08:36:14.468Z","revisedAt":"2021-08-31T12:05:21.841Z","topics":"GitLab","logo":"/logos/GitLab.png","needs_title":false},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\n  \nUbuntu 20.04.2.0 に GitLab をインストールしてみました。  \n<br />\nGitLabはGitHubとほぼ同じ機能を有するOSSのソースコード管理ツールです。  \nオンプレミスで使う分には完全無料です。  \n同じ目的の場合、GitHubが有名ですが、閉じた環境で自力運用したくて、GitLabを選びました。  \n（GitHub Enterpriseの場合、オンプレミス運用できるようですが、有料になります。）\n<br />\nインストール環境：Ubuntu 20.04.2.0 (VMware上、インターネット接続あり)  \n<br />\n\n# インストール準備\n  \n<blockquote class=\"warn\">\n<p>root権限で作業していますので、全てsudoは省略しています。</p>\n</blockquote>\n\n<br />\n\nパッケージを最新化します。  \n```shellsession\n# apt update && apt upgrade\nDo you want to continue? [Y/n]: Y\n```\n※以降基本的にYのため、-yを付けます。  \n　-y は、? [y/N]: のようなときに自動的に y とするオプションです。  \n\n<blockquote class=\"info\">\n<p>apt update は、パッケージ一覧の更新</p>\n<p>apt upgrade は、更新された一覧を元に、実際にパッケージを更新</p>\n<p>という動作になります。</p>\n</blockquote>\n\n<br />\n\ncurlをインストールします。  \n```shellsession\n# apt install -y curl\n```\n\n<br />\n\n# GitLab インストール\n  \nGitLabをインストールします。  \n```shellsession\n# curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | bash\n# apt install -y gitlab-ce\n```\n\n<br />\n\nホスト名を設定します。\n```shellsession\n# vi /etc/gitlab/gitlab.rb\n```\n```shellsession\nexternal_url \"http://gitlab.itccorporation.jp\"\n```\n\n<br />\n\n設定を反映します。\n```shellsession\n# gitlab-ctl reconfigure\n```\n\n<br />\n\n# ブラウザアクセス\n  \n`https://` にもできますが、今回は、閉じた環境で運用するため、`http://` のまま利用します。\n<br />\n\n`http://gitlab.itccorporation.jp` へブラウザでアクセス  \n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image1.png\" alt=\"ブラウザでアクセス\" width=\"1200\" height=\"821\" loading=\"lazy\"></a>\n\n<br />\n\nＯＫです！\n\n<br />\n\n<blockquote class=\"info\">\n<p>最初の画面は、root のパスワードの設定です。rootは、GitLab管理者になります。</p>\n</blockquote>\n\n<br />\n\n<a href=\"https://about.gitlab.com/install/#ubuntu\" target=\"_blank\">公式サイトのインストール手順</a>では、postfixのインストールが書かれていましたが、smtpサーバーに直接繋いでメール送信できれば良いため、postfixのインストールは行っていません。  \ngmailでのメール送信設定を試してみました。  \n↓\n\n# メール送信設定(gmail)\n\n## メール設定\n\ngmailの設定を行います。  \n```shellsession\n# vi /etc/gitlab/gitlab.rb\n```\n\n```shellsession\ngitlab_rails['smtp_enable'] = true\ngitlab_rails['smtp_address'] = \"smtp.gmail.com\"\ngitlab_rails['smtp_port'] = 587\ngitlab_rails['smtp_user_name'] = \"my.email@gmail.com\"\ngitlab_rails['smtp_password'] = \"my-gmail-password\"\ngitlab_rails['smtp_domain'] = \"smtp.gmail.com\"\ngitlab_rails['smtp_authentication'] = \"login\"\ngitlab_rails['smtp_enable_starttls_auto'] = true\ngitlab_rails['smtp_tls'] = false\ngitlab_rails['smtp_openssl_verify_mode'] = 'peer'\n```\n<blockquote class=\"info\">\n<p><code>gitlab_rails['smtp_openssl_verify_mode'] = 'peer'</code>は、</p>\n<p>SMTPサーバのSSL証明書の有効性を検証して、接続先がなりすまされていないことを確認します。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>my.email@gmail.com は、自分のメールアドレス。my-gmail-password は、Googleアカウントのパスワードになります。</p>\n</blockquote>\n\n<br />\n\n設定を反映します。\n```shellsession\n# gitlab-ctl reconfigure\n```\n\n<br />\n\ngitlab-rails consoleで確認してみます。  \nsmtpが有効かどうか確認します。\n```shellsession\n# gitlab-rails console\n```\n\n```shellsession\n> ActionMailer::Base.delivery_method\n=> :smtp\n```\n  \n⇒ＯＫです。\n\n<br />\n\nメール送信設定がきちんと設定されているか確認します。\n```shellsession\n> ActionMailer::Base.smtp_settings\n=> {:authentication=>:login, :address=>\"smtp.gmail.com\", :port=>587, :user_name=>\"my.email@gmail.com\", :password=>\"my-gmail-password\", :domain=>\"smtp.gmail.com\", :enable_starttls_auto=>true, :tls=>false, :openssl_verify_mode=>\"peer\", :ca_file=>\"/opt/gitlab/embedded/ssl/certs/cacert.pem\"}\n```\n  \n⇒ＯＫです。\n\n<br />\n\n```shellsession\n> Notify.test_email('my.email@gmail.com', 'Hello World', 'This is a test message').deliver_now\n(略)\nNet::SMTPAuthenticationError (535-5.7.8 Username and Password not accepted. Learn more at)\n```\n  \n⇒メール送信テストでエラーになりました。\n\n<br />\n\n## ２段階認証無し\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p>GMailの仕様変更により、2022年6月からこの方法は使えなくなりました。</p>\n<p>下記「<a href=\"#nidan\">２段階認証有り</a>」セクションへ飛んでください。</p>\n</blockquote>\n\n<span style=\"color: red; \"><strong>smtp.gmail.comを利用する場合、Googleアカウントの設定が必要のようです。</strong></span>  \n「安全性の低いアプリのアクセス」をオンにしたらうまくいきました。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image2.png\" alt=\"(旧設定)セキュリティ\" width=\"1200\" height=\"173\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image3.png\" alt=\"(旧設定)アクセスを有効にする (非推奨)\" width=\"1200\" height=\"363\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image4.png\" alt=\"(旧設定)「安全性の低いアプリのアクセス」をオン\" width=\"1200\" height=\"554\" loading=\"lazy\"></a>\n\n<br />\n\n```shellsession\n# gitlab-rails console\n```\n\n```shellsession\n> Notify.test_email('my.email@gmail.com', 'Hello World', 'This is a test message').deliver_now\n```\nエラーは起きません。\n\n<br />\n  \nメーラーで確認をすると、メールが来ました。\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image5.png\" alt=\"２段階認証無し テストメール\" width=\"1079\" height=\"719\" loading=\"lazy\"></a>\n\n<br />\n\n⇒Ｇｏｏｄ！  \n\n<br />\n\n<a class=\"anchor\" id=\"nidan\"></a>\n\n## ２段階認証有り\n\n<span style=\"color: red; \"><strong>２段階認証をOnにしている場合、別の対応が必要のようです。</strong></span>  \nアプリ パスワードを利用したらうまくいきました。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image6.png\" alt=\"アプリパスワード\" width=\"1200\" height=\"407\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image7.png\" alt=\"生成されたアプリパスワード\" width=\"1200\" height=\"868\" loading=\"lazy\"></a>\n\n<br />\n\n```shellsession\n# vi /etc/gitlab/gitlab.rb\n```\n\n```shellsession\ngitlab_rails['smtp_enable'] = true\ngitlab_rails['smtp_address'] = \"smtp.gmail.com\"\ngitlab_rails['smtp_port'] = 587\ngitlab_rails['smtp_user_name'] = \"my.email@gmail.com\"\ngitlab_rails['smtp_password'] = \"my-gmail-password\"\ngitlab_rails['smtp_domain'] = \"smtp.gmail.com\"\ngitlab_rails['smtp_authentication'] = \"login\"\ngitlab_rails['smtp_enable_starttls_auto'] = true\ngitlab_rails['smtp_tls'] = false\ngitlab_rails['smtp_openssl_verify_mode'] = 'peer'\n```\n<blockquote class=\"info\">\n<p><strong>my-gmail-password をアプリ パスワードにします。</strong></p>\n</blockquote>\n\n<br />\n\n```shellsession\n# gitlab-ctl reconfigure\n```\n\n<br />\n\n```shellsession\n# gitlab-rails console\n```\n\n```shellsession\n> Notify.test_email('my.email@gmail.com', 'Hello World', 'This is a test message').deliver_now\n```\n  \n<br />\n\nメーラーで確認をすると、メールが来ました。\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image5.png\" alt=\"２段階認証有り テストメール\" width=\"1079\" height=\"719\" loading=\"lazy\"></a>\n\n<br />\n\n⇒Ｇｏｏｄ！  \n\n<br />\n\n# コンソールでのユーザー登録\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p>ブログ投稿当時（2021年05月09日）と状況が異なっていましたので、内容を刷新しました。</p>\n</blockquote>\n\ngitlab-rails console でユーザー登録ができます。\n\n<br />\n\nまずは、root（管理者）を登録します。\n```shellsession\n# gitlab-rails console\n```\n\n```shellsession\n> user = User.where(id: 1).first\n=> #<User id:1 @root>\n> user.password = 'パスワード'\n> user.password_confirmation = 'パスワード'\n> user.email\n=> \"admin@example.com\"\n> user.email = 'rootのメールアドレス'\n> user.email_confirmation = 'rootのメールアドレス'\n> user.save!\n=> true\n```\n\n<br />\n\nrootのメールアドレス宛に「Confirmation instructions」メールが送信されてきますので、**Confirm your email address** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image8.png\" alt=\"root宛てConfirm your email addressメール\" width=\"796\" height=\"502\" loading=\"lazy\"></a>\n\n<blockquote class=\"alert\">\n<p>同時にパスワード変更通知「Password Changed」メールとメールアドレス変更通知「Email Changed」メールが送られてきます。</p>\n<p>変更前のメールアドレス <code>admin@example.com</code> に送られてエラーになりますが、避けられないようです。</p>\n<p>以降、同様にメールアドレスを変更すると、メールアドレス変更前のメールアドレスに通知されるようです。</p>\n<p>【エラー】</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">ドメイン example.com が見つからなかったため、メールは admin@example.com に配信されませんでした。入力ミスや不要なスペースがないことを確認してから、もう一度送信してみてください。</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">DNS Error: DNS type 'mx' lookup of example.com responded with code NCERROR The domain example.com doesn't receive email according to the administrator: returned Null MX. Learn more  at https://www.rfc.editor.org/info/rfc7505</span></p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image9.png\" alt=\"Email Changedメールエラー\" width=\"1191\" height=\"1066\" loading=\"lazy\"></a>\n\n<br />\n\n先ほど入力した root のメールアドレスとパスワードでログインできたら、root の登録は完了です。\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image10.png\" alt=\"root のメールアドレスとパスワードでログイン\" width=\"997\" height=\"531\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image11.png\" alt=\"rootログイン後\" width=\"1202\" height=\"818\" loading=\"lazy\"></a>\n\n<br />\n\n続いて、ユーザーを登録します。\n\n<br />\n\nまず、以下のように Email confirmation settings を `hard` に変更しておきます。\n\n```shellsession\n> ApplicationSetting.last.update(email_confirmation_setting: 'hard')\n```\n\n`soft`：3日以内に「Confirmation instructions」メールのリンクのクリックが必要です。3日以内なら、「Confirmation instructions」メールのリンクのクリック無しでログインできます。  \n`hard`：「Confirmation instructions」メールのリンクのクリックが必須です。  \n\n<br />\n\nちなみに、root で GUI でも変更できます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image12.png\" alt=\"GUIでhardに変更\" width=\"1202\" height=\"816\" loading=\"lazy\"></a>\n\n<br />\n\nユーザーを新規作成します。\n\n<blockquote class=\"warn\">\n<p>パスワードにユーザー名を含めたり、単純すぎたりしたらNGのようです。</p>\n<p>メールアドレスは、rootと同じにできません。メアドが一つでテストする場合、xxxxx+test1@gmail.com のようにプラス記号を使うと良いです。</p>\n</blockquote>\n\n```shellsession\n>u = User.new(\n>username: 'testuser',\n>email: 'my.email@gmail.com',\n>name: 'Test User',\n>password: 'test-user-pass',\n>password_confirmation: 'test-user-pass')\n=> #<User id: @testuser>\n>u.save!\n=> true\n```\n\n<br />\n\n登録したユーザーに「Confirmation instructions」メールが来ますので、リンクをクリックすると、登録完了になります。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image13.png\" alt=\"ユーザー宛て「Confirmation instructions」メール\" width=\"776\" height=\"503\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image14.png\" alt=\"ユーザーのメールアドレスとパスワードでログイン\" width=\"1003\" height=\"562\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/gitlab-install/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/gitlab-install/image15.png\" alt=\"ユーザーログイン後\" width=\"1202\" height=\"825\" loading=\"lazy\"></a>\n\n<br />\n\nできました！\n","description":"Ubuntu 20.04.2.0 に GitLab をインストールしてみました。 GitLabはGitHubとほぼ同じ機能を有するOSSのソースコード管理ツールです。 オンプレミスで使う分には完全無料です。 同じ目的の場合、GitHubが有名ですが、閉じた環境で自力運用したくて、GitLabを選びました。 （GitHub Enterpriseの場合、オンプレミス運用できるようですが、有料になります。） インストール環境：Ubuntu 20.04.2.0 (VMware上、インターネット接続あり)","reflect_updatedAt":true,"reflect_revisedAt":true,"seo_images":[{"id":"x5ad6bml5","createdAt":"2021-07-16T10:02:27.966Z","updatedAt":"2023-11-11T11:05:39.476Z","publishedAt":"2021-07-16T10:02:27.966Z","revisedAt":"2023-11-11T11:05:39.476Z","url":"https://itc-engineering-blog.imgix.net/gitlab-install/ITC_Engineering_Blog.png","alt":"Ubuntu 20.04.2.0にGitLabをインストール","width":1200,"height":630}],"seo_authors":[]},{"id":"acr-aca-bicep","createdAt":"2022-11-15T14:11:15.774Z","updatedAt":"2022-12-06T13:39:44.587Z","publishedAt":"2022-11-15T14:11:15.774Z","revisedAt":"2022-12-06T13:39:44.587Z","title":"Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ","category":{"id":"r2upy60kf","createdAt":"2022-12-06T10:13:03.471Z","updatedAt":"2022-12-06T10:13:03.471Z","publishedAt":"2022-12-06T10:13:03.471Z","revisedAt":"2022-12-06T10:13:03.471Z","topics":"Bicep","logo":"/logos/Bicep.png","needs_title":true},"topics":[{"id":"r2upy60kf","createdAt":"2022-12-06T10:13:03.471Z","updatedAt":"2022-12-06T10:13:03.471Z","publishedAt":"2022-12-06T10:13:03.471Z","revisedAt":"2022-12-06T10:13:03.471Z","topics":"Bicep","logo":"/logos/Bicep.png","needs_title":true},{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"hyb2dlkbyj-y","createdAt":"2021-06-03T13:49:36.431Z","updatedAt":"2021-06-03T13:49:36.431Z","publishedAt":"2021-06-03T13:49:36.431Z","revisedAt":"2021-06-03T13:49:36.431Z","topics":"GitHub","logo":"/logos/GitHub.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false},{"id":"xego85dtzyu","createdAt":"2021-06-03T13:50:33.576Z","updatedAt":"2021-08-31T12:04:26.367Z","publishedAt":"2021-06-03T13:50:33.576Z","revisedAt":"2021-08-31T12:04:26.367Z","topics":"React","logo":"/logos/React.png","needs_title":false},{"id":"xnhxgx1v0","createdAt":"2022-04-08T10:53:39.471Z","updatedAt":"2022-04-08T10:53:39.471Z","publishedAt":"2022-04-08T10:53:39.471Z","revisedAt":"2022-04-08T10:53:39.471Z","topics":"TypeScript","logo":"/logos/TypeScript.png","needs_title":true}],"content":"# はじめに\n\nAzure Container Apps へ bicep、Azure Container Registry、GitHub Actions を使って React Web アプリをデプロイしてみました。一連の手順を紹介していきたいと思います。\n\n<br />\n\n<blockquote class=\"info\">\n<p>先日アップした記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/azure-container-bicep\" target=\"_blank\">Azure Container Appsへbicep,GitHub Container Registry,GitHub Actionsを使ってデプロイ</a>」の Azure Container Registry 版です。その他は、まったく同一で、コンテナレジストリを GitHub Container Registry（GitHub Packages）から Azure Container Registry に変更したのみです。</p>\n</blockquote>\n\n<br />\n\n**Azure Container Apps**：フル マネージド サーバーレス コンテナー サービスです。Azure Container Apps を使うと、Kubernetes のオーケストレーションやインフラストラクチャを気にすることなく、コンテナー化されたアプリケーションを実行できます。  \n**bicep**：Bicep は、宣言型の構文を使用して Azure リソースをデプロイするドメイン固有言語 (DSL) です。無人でAzure をいじるときに使うプログラミング言語のようなものです。  \n**GitHub Actions**：GitHub Actions は、GitHub が提供する CI/CD サービスです。 GitHub と高度に統合されており、GitHub に公開されたコードを自動でビルド・テスト・デプロイを行うのが主目的です。  \n**Azure Container Registry**：Azureのコンテナレジストリです。Docker HubのAzure版のようなものです。<span style=\"color: red;\">有料です。</span>  \n**Web アプリ**：create-react-app で作成したものそのままです。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/zu1.png\" alt=\"Azure Container Appsデプロイ全体像の図\" width=\"1161\" height=\"1292\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\nパイプラインや CI/CD については、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-ci-cd\" target=\"_blank\">Kotlin ＋ Spring Boot のアプリを GitLab CI/CD による Docker デプロイまで全手順</a>」に詳しく書きましたので、こちらを参照してください。\n\n<blockquote class=\"info\">\n<p>【 コンテナレジストリ 】</p>\n<p>Azure Container Registry、GitHub Container Registry（ghcr.io）、Docker Registry、etc...の内、今回は、Azure Container Registry を使います。Azure Container Registry、GitHub Container Registry（ghcr.io）以外の場合、どうなるかは検証していません。</p>\n</blockquote>\n\n<br />\n\n基本的には、learn.microsoft.com の「<a href=\"https://learn.microsoft.com/ja-jp/azure/container-apps/dapr-github-actions?tabs=bash\" target=\"_blank\">チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする</a>」を見ながら進めましたが、参考にしただけで、同一ではありません。Dapr 未使用で、Azure Cosmos DB は作成せず、コンテナも一つで、かなり単純化しています。  \nlearn.microsoft.com は、GitHub Container Registry（ghcr.io）を使っていますが、今回は、Azure Container Registry に差し替えています。  \n\n<br />\n\n<blockquote class=\"alert\">\n<p>本記事情報により何らかの問題が生じても、一切責任を負いません。</p>\n</blockquote>\n\n\n<blockquote class=\"warn\">\n<p>コンソールは、Ubuntu 20.04 LTS の bash で作業しています。</p>\n<p>git コマンドや az コマンド、npx などはインストール済みで使えるものとします。</p>\n</blockquote>\n\n<br />\n\n# ソースコード準備\n\n<blockquote class=\"info\">\n<p>作成済みの全体ソースコードは、 <a href=\"https://github.com/itc-lab/azure-container-apps-bicep-acr-example\" target=\"_blank\">https://github.com/itc-lab/azure-container-apps-bicep-acr-example</a> にアップしました。</p>\n</blockquote>\n\ncreate-react-app で作成  \n↓  \nコンテナビルドのための Dockerfile  \n↓  \nGitHub Actions ワークフローファイル `.github/workflows/build-and-deploy.yaml`  \n↓  \nAzure Container Registry デプロイ用に `./deploy/create-acr.bicep`  \n↓  \nAzure Container Apps 他、Azure へのリソースデプロイ用に `./deploy/main.bicep`、`./deploy/environment.bicep`、`./deploy/container-http.bicep`  \nと準備していきます。\n\n<br />\n\n## create-react-app\n\n```shellsession\n$ npx create-react-app aca-app --template typescript\n$ cd aca-app\n$ npm start\n```\n\n自動的に最低限の Web アプリが作成されて、`http://localhost:3000` で以下の画面が表示されます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" alt=\"Webアプリ画面\" width=\"582\" height=\"403\" loading=\"lazy\"></a>\n\n<br />\n\n## Dockerfile\n\n`yarn build` した静的ファイルを nginx で参照する形のコンテナを作成するための、Dockerfile を準備します。\n\n```dockerfile:./Dockerfile\n# Dockerマルチステージビルド\nFROM node:16 as build\n\nWORKDIR /app\n\nCOPY package.json /app/\n\nRUN yarn\n\nCOPY . /app/\n\nRUN yarn build\n\n\nFROM nginx:latest\n\nCOPY --from=build /app/build /usr/share/nginx/html\n\nCOPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf\n```\n\n<br />\n\nnginx.conf を準備します。  \n3000 番ポートを開いています。\n\n```nginx:./nginx.conf\nserver {\n  listen 3000;\n\n  gzip on;\n  gzip_vary on;\n  gzip_min_length 10240;\n  gzip_proxied expired no-cache no-store private auth;\n  gzip_http_version 1.1;\n  gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/xml image/png;\n  gzip_disable \"MSIE [1-6]\\.\";\n\n  location / {\n    root /usr/share/nginx/html;\n    index index.html index.htm;\n    try_files $uri $uri/ /index.html =404;\n  }\n  include /etc/nginx/extra-conf.d/*.conf;\n}\n```\n\n<br />\n\n## build-and-deploy.yaml\n\nGitHub Actions パイプラインのワークフローファイル `.github/workflows/build-and-deploy.yaml` を作成します。\n\n<blockquote class=\"info\">\n<p>意味は、中のコメントを見てください。</p>\n</blockquote>\n\n```yaml:./.github/workflows/build-and-deploy.yaml\nname: Build and Deploy\non:\n  push:\n    # mainブランチにpushまたは、v*.*.* タグがreleaseされたときに発動\n    branches: [main]\n    tags: [\"v*.*.*\"]\n    # このファイルの変更だけの場合、パイプラインを実行しない。\n    # パイプラインとは、一連の自動化処理のこと。GitHubでは、ワークフローと言っている。\n    # ここに書いてある実行するかどうかの条件は、ワークフロー トリガーと言う。\n    paths-ignore:\n      - \"README.md\"\n      - \".vscode/**\"\n      - \"assets/**\"\n      - \"build-and-run.md\"\n  # 手動実行（GUIからRun workflowボタン）有効\n  workflow_dispatch:\n\njobs:\n  set-env:\n    name: Set Environment Variables\n    runs-on: ubuntu-latest\n    outputs:\n      # mainステップの実行結果を保存→他のjobで変数として使える。\n      # needs.set-env.outputs.version のように参照\n      version: ${{ steps.main.outputs.version }}\n      created: ${{ steps.main.outputs.created }}\n      repository: ${{ steps.main.outputs.repository }}\n      appname: ${{ steps.main.outputs.appname }}\n    steps:\n      # $GITHUB_SHA：ワークフローをトリガーしたコミット SHA。\n      # $GITHUB_REPOSITORY：リポジトリオーナー/リポジトリ名。例：octocat/Hello-World\n      - id: main\n        # set-outputコマンドを利用することで出力パラメータに値を代入することができたが、\n        # set-outputの使用は非推奨になった。\n        # echo \"::set-output name={name}::{value}\" で{name}に{value}を代入。\n        # ↓\n        # echo \"{name}={value}\" >> $GITHUB_OUTPUT で{name}に{value}を代入。\n        # cut -c1-7 ＝ 先頭７文字\n        run: |\n          echo version=$(echo ${GITHUB_SHA} | cut -c1-7) >> $GITHUB_OUTPUT\n          echo created=$(date -u +'%Y-%m-%dT%H:%M:%SZ') >> $GITHUB_OUTPUT\n          echo repository=$GITHUB_REPOSITORY >> $GITHUB_OUTPUT\n          echo appname=react-frontend >> $GITHUB_OUTPUT\n\n  # コンテナ build & コンテナレジストリへ push のジョブ\n  package-services:\n    # ランナー イメージ（ジョブを実行する仮想マシン）を指定。windows-latest、windows-2022、macos-12とかいろいろある。\n    # ランナーは自分で作ることもできて、自分で作ったランナーをセルフホストランナー（self-hosted runners）という。\n    runs-on: ubuntu-latest\n    # needs＝先行のジョブを指定（指定しないと、並列実行される。）\n    # この場合、needs: set-env が無いと、set-envジョブと同時にpackage-servicesジョブが動く。\n    needs: set-env\n    # secrets.GITHUB_TOKENの権限を設定。このワークフローで必要な権限を設定。\n    permissions:\n      # ソースコードリポジトリは、読み取りのみ\n      contents: read\n      # コンテナレジストリは書き込み可\n      packages: write\n    outputs:\n      # image-tagステップにて、echo image-${{ needs.set-env.outputs.appname }}=*・・・ が実行されていて、\n      # その値をcontainerImage-reactという名称で、他ジョブでも使えるように出力している。（最後に実行するコマンドのパラメータに使っている）\n      containerImage-react: ${{ steps.image-tag.outputs.image-react-frontend }}\n    steps:\n      - name: Checkout repository\n        # actions/checkout@v2アクション\n        # pushされた時にはそのpushされたコードをチェックアウト\n        # プルリク時には、そのプルリクされたコードをチェックアウト\n        uses: actions/checkout@v2\n      - name: Log into registry ${{ secrets.REGISTRY_LOGIN_SERVER }}\n        # プルリク以外だったら実行\n        if: github.event_name != 'pull_request'\n        # Azure Container Registryにdocker imageをpushするためにdocker login。\n        # azure/docker-login@v1アクションを利用。\n        # withでアクションに必要なパラメータを定義。\n        uses: azure/docker-login@v1\n        with:\n          # ログインするAzureコンテナレジストリ。\n          login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}\n          # ログインするユーザー＝事前にpush許可が与えられているサービスプリンシパルのclientId\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          # ログインするユーザーのパスワード＝事前にpush許可が与えられているサービスプリンシパルのclientSecret\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Extract Docker metadata\n        id: meta\n        # docker/metadata-action@v3アクション\n        # 動的なタグ生成を実施→docker build時に指示できる\n        # ビルドされたimageが\n        # acrtestyou.azurecr.io/xxx/aaa:latest、acrtestyou.azurecr.io/xxx/aaa:1.2.3、acrtestyou.azurecr.io/xxx/aaa:1.2、acrtestyou.azurecr.io/xxx/aaa:1、\n        # acrtestyou.azurecr.io/xxx/aaa:sha-ad132f5、acrtestyou.azurecr.io/xxx/aaa:main\n        # といったタグでpushされる。\n        uses: docker/metadata-action@v3\n        with:\n          images: ${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.set-env.outputs.repository }}/${{ needs.set-env.outputs.appname }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=ref,event=branch\n            type=sha\n      - name: Build and push Docker image\n        # docker/build-push-action@v2アクション\n        # コンテナ build & コンテナレジストリへ push\n        uses: docker/build-push-action@v2\n        with:\n          # context: Dockerfileが置いてある場所\n          # 今回は、リポジトリ直下に置いているが、ディレクトリの中なら、'./frontend'、'./backend' とか。\n          context: \".\"\n          # コンテナレジストリにpushするかどうか。プルリク以外は、push。\n          push: ${{ github.event_name != 'pull_request' }}\n          # 上で設定したタグを適用\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n      - name: Output image tag\n        id: image-tag\n        # image-react-frontend = acrtestyou.azurecr.io/<github-username>/aca-app2-repo/react-frontend:sha-<コミットSHA（先頭７文字）>\n        # tr '[:upper:]' '[:lower:]' は、小文字化するLinuxコマンド\n        # 以下のようにsecretsを含んでechoはダメ！\n        # run: |\n        #   echo image-${{ needs.set-env.outputs.appname }}=${{ secrets.REGISTRY_LOGIN_SERVER }}/$GITHUB_REPOSITORY/${{ needs.set-env.outputs.appname }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT\n        # image-react-frontend = <github-username>/aca-app2-repo/react-frontend:sha-<コミットSHA（先頭７文字）>\n        run: |\n          echo image-${{ needs.set-env.outputs.appname }}=$GITHUB_REPOSITORY/${{ needs.set-env.outputs.appname }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT\n\n  deploy:\n    runs-on: ubuntu-latest\n    # package-servicesジョブ終了後に実行\n    # 複数終了待ちの場合：[job1, job2] で job1とjob2 両方が終わっている必要があるという意味になる。\n    needs: package-services\n    steps:\n      - name: Checkout repository\n        # actions/checkout@v2アクション\n        # リポジトリをチェックアウト\n        uses: actions/checkout@v2\n\n      - name: Azure Login\n        # azure/login@v1アクション\n        # Azureにログイン\n        uses: azure/login@v1\n        with:\n          # サービス プリンシパル シークレット（事前にAzureに作成して、シークレットをGitHubのGUIで登録）\n          # Azure CLI で bicep を実行、Azure Container Appsにデプロイする権限を得るために必要。\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n\n      - name: Deploy bicep\n        # azure/CLI@v1アクション\n        # Azure CLI （Azure内のターミナル）で実行\n        uses: azure/CLI@v1\n        with:\n          # Azureリソースグループを東日本リージョン（japaneast）に作成\n          # az deployment group create -g <リソース グループ> -f <bicepファイル> でAzureへのデプロイを開始。\n          # -p は引数指定（main.bicep内で使うパラメータ）\n          # minReplicas：最小レプリカ（Pod）数\n          # reactImage：React App の containerImage\n          # reactPort：React App の containerPort\n          # containerRegistry：コンテナレジストリ。ここでは、Azure Container Registry （今回の場合は、acrtestyou.azurecr.io）\n          # containerRegistryUsername：コンテナレジストリのログインユーザー名（事前にpull許可が与えられているサービスプリンシパルのclientId）\n          # containerRegistryPassword：コンテナレジストリのパスワード（事前にpull許可が与えられているサービスプリンシパルのclientSecret）\n          # リソース作成済み前提で動かすため、以下は除外（作成済みでもinlineScript:の最初に実行して構わない。）\n          # az group create -g ${{ secrets.RESOURCE_GROUP }} -l japaneast\n          inlineScript: |\n            az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep \\\n              -p \\\n                minReplicas=0 \\\n                reactImage='${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.package-services.outputs.containerImage-react }}' \\\n                reactPort=3000 \\\n                containerRegistry=${{ secrets.REGISTRY_LOGIN_SERVER }} \\\n                containerRegistryUsername=${{ secrets.REGISTRY_USERNAME }} \\\n                containerRegistryPassword='${{ secrets.REGISTRY_PASSWORD }}'\n```\n\n<br />\n\n## Bicep\n\nAzure Container Registry デプロイ用に `./deploy/create-acr.bicep`、  \nAzure Container Apps 他、Azure へのリソースデプロイ用に `./deploy/main.bicep`、`./deploy/environment.bicep`、`./deploy/container-http.bicep` を準備します。  \nこれは、GitHub Actions の 最後に `az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep・・・` で実行されています。\n\n```bicep:./deploy/create-acr.bicep\n@minLength(5)\n@maxLength(50)\n@description('Provide a globally unique name of your Azure Container Registry')\nparam acrName string = 'acr${uniqueString(resourceGroup().id)}'\n\n@description('Provide a location for the registry.')\nparam location string = resourceGroup().location\n\n@description('Provide a tier of your Azure Container Registry.')\nparam acrSku string = 'Basic'\n\nresource acrResource 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {\n  name: acrName\n  location: location\n  sku: {\n    name: acrSku\n  }\n  properties: {\n    adminUserEnabled: false\n  }\n}\n\n@description('Output the login server property for later use')\noutput loginServer string = acrResource.properties.loginServer\n```\n\n<br />\n\n```bicep:./deploy/main.bicep\n// param <parameter-name> <parameter-data-type> = <default-value>\n// 注意：=の右側 <default-value> は、-p で与えられなかった場合、適用される。\n// 今回、build-and-deploy.yaml にて以下のように実行されている。\n// -p \\\n// minReplicas=0 \\\n// reactImage='${{ needs.package-services.outputs.containerImage-react }}' \\\n// reactPort=3000 \\\n// containerRegistry=${{ secrets.REGISTRY_LOGIN_SERVER }} \\\n// containerRegistryUsername=${{ secrets.REGISTRY_USERNAME }} \\\n// containerRegistryPassword=${{ secrets.REGISTRY_PASSWORD }}\n\n// function resourceGroup(): resourceGroup\n// 現在のリソース グループのスコープを返す\nparam location string = resourceGroup().location\n// function uniqueString(... : string): string\n// パラメータとして提供された値に基づいてハッシュ文字列を作成。戻り値は 13 文字。\nparam environmentName string = 'env-${uniqueString(resourceGroup().id)}'\n\nparam minReplicas int = 0\n\nparam reactImage string\nparam reactPort int = 3000\n// var で始まる部分はコマンドライン等からは変更できないファイル内で利用する内部変数\nvar reactFrontendAppName = 'react-app'\n\nparam isPrivateRegistry bool = true\n\nparam containerRegistry string\n// ログインするユーザー＝事前にpush許可が与えられているサービスプリンシパルのclientId\n// function secure(): any\n// @secure() 修飾子\n// パラメーターの値はデプロイ履歴に保存されず、ログにも記録されない。\n@secure()\nparam containerRegistryUsername string = ''\n// ログインするユーザーのパスワード＝事前にpush許可が与えられているサービスプリンシパルのclientSecret\n@secure()\nparam containerRegistryPassword string = ''\n// コンテナレジストリシークレットのキー名\n// ただのキー名だけど、\"Password\"という言葉に反応して、secure-secrets-in-params警告が出るため、\n// #disable-next-line で沈黙させる。\n#disable-next-line secure-secrets-in-params\nparam registryPassword string = 'registry-password'\n\n// Container Apps 環境モジュール\n// module <symbolic-name> '<path-to-file>' = {\n//   name: '<linked-deployment-name>'\n//   params: {\n//     <parameter-names-and-values>\n//   }\n// }\n// <path-to-file>は、テンプレートのようなもので、値を渡して何回も使用可能。\n// 注意：今回は、１回しか使っていない。\n// 各パラーメータの説明は、environment.bicep、ontainer-http.bicep 内に記載。\nmodule environment 'environment.bicep' = {\n  // function deployment(): deployment\n  // 現在のデプロイメント操作に関する情報を返す。\n  // ここでは、name: \"main--environment\" になる。\n  name: '${deployment().name}--environment'\n  params: {\n    environmentName: environmentName\n    location: location\n    appInsightsName: '${environmentName}-ai'\n    logAnalyticsWorkspaceName: '${environmentName}-la'\n  }\n}\n\n// React App\nmodule reactFrontend 'container-http.bicep' = {\n  name: '${deployment().name}--${reactFrontendAppName}'\n  dependsOn: [\n    environment\n  ]\n  params: {\n    enableIngress: true\n    isExternalIngress: true\n    location: location\n    environmentName: environmentName\n    containerAppName: reactFrontendAppName\n    containerImage: reactImage\n    containerPort: reactPort\n    minReplicas: minReplicas\n    isPrivateRegistry: isPrivateRegistry\n    containerRegistry: containerRegistry\n    registryPassword: registryPassword\n    containerRegistryUsername: containerRegistryUsername\n    revisionMode: 'Multiple'\n    secrets: [\n      {\n        name: registryPassword\n        value: containerRegistryPassword\n      }\n    ]\n  }\n}\n\noutput reactFqdn string = reactFrontend.outputs.fqdn\n```\n\n<br />\n\n```bicep:./deploy/environment.bicep\nparam environmentName string\nparam logAnalyticsWorkspaceName string\nparam appInsightsName string\nparam location string\n\n// ログ分析ワークスペース 作成\n// resource キーワードを使用してリソースを追加。\n// リソースのシンボリック名を設定。シンボリック名はリソース名と同じものではない。\n// シンボリック名は、Bicep ファイルの他の部分にあるリソースを参照するために使用。\n// resource <symbolic-name> '<full-type-name>@<api-version>' = {\n//   <resource-properties>\n// }\nresource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = {\n  name: logAnalyticsWorkspaceName\n  location: location\n  properties: any({\n    // ワークスペースのデータ保持日数。 -1 は、Unlimited Sku の場合、無制限。\n    // 他のすべての Sku で許可される最大日数は 730 日。\n    retentionInDays: 30\n    // ？どこにもこの項目の説明が無い。\n    features: {\n      searchVersion: 1\n    }\n    // SKU（Stock Keeping Unit）\n    // Basic、Standardなどの課金形態のこと。\n    // 2018 年 4 月の価格モデルを選択したサブスクリプションで Log Analytics ワークスペースを作成または構成する場合、\n    // 有効な Log Analytics 価格レベルは PerGB2018 のみ。\n    sku: {\n      name: 'PerGB2018'\n    }\n  })\n}\n\n// Application Insights 作成\n// Application Insights は Azure Monitor の拡張機能であり、\n// アプリケーション パフォーマンス監視 (\"APM\" とも呼ばれる) 機能を提供。\n// APM ツールは、開発、テスト、運用環境からアプリケーションを監視するのに役立つ。\nresource appInsights 'Microsoft.Insights/components@2020-02-02' = {\n  name: appInsightsName\n  location: location\n  // このコンポーネントが参照するアプリケーションの種類。\n  // 任意の文字列で、値は通常、web、ios、other、store、java、phone のいずれか。\n  kind: 'web'\n  properties: {\n    // 監視アプリケーションの種類\n    Application_Type: 'web'\n    // データが取り込まれるログ分析ワークスペースのリソース ID\n    // （logAnalyticsWorkspace は上で作成しているリソースを参照している）\n    WorkspaceResourceId:logAnalyticsWorkspace.id\n  }\n}\n\n// マネージド環境（Azure Container Apps の環境）作成\nresource environment 'Microsoft.App/managedEnvironments@2022-03-01' = {\n  // 環境名：'env-${uniqueString(resourceGroup().id)}'\n  name: environmentName\n  location: location\n  properties: {\n    // サービスをサービス通信テレメトリにエクスポートするために Dapr によって使用される Azure Monitor インストルメンテーション キー\n    // Application Insights インストルメンテーション キー。\n    // アプリケーションが Azure Application Insights に送信されるすべてのテレメトリの送信先を識別するために使用できる読み取り専用の値。\n    // この値は、新しい各 Application Insights コンポーネントの構築時に提供される。\n    // appInsightsは、上で作成しているApplication Insights。\n    // 今回は、Dapr を導入しないため、意味無いかも。\n    daprAIInstrumentationKey:appInsights.properties.InstrumentationKey\n    appLogsConfiguration: {\n      // ログ記録先。 現在、「log-analytics」のみがサポートされている。\n      destination: 'log-analytics'\n      logAnalyticsConfiguration: {\n        // Log AnalyticsのcustomerIdとsharedKey\n        customerId: logAnalyticsWorkspace.properties.customerId\n        // function list*([apiVersion: string], [functionValues: object]): any\n        // この関数の構文は、リスト操作の名前によって異なる。\n        // 一般的な使用法には、listKeys、listKeyValue、および listSecrets がある。\n        // 各実装は、リスト操作をサポートするリソース タイプの値を返す。\n        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey\n      }\n    }\n  }\n}\n\n// output <name> <data-type> = <value>\n// デプロイが成功すると、出力値はデプロイの結果で自動的に返される。\n// Azure CLI または Azure PowerShell スクリプトでデプロイ履歴から出力値を参照できる。\n// az deployment group show \\\n// -g <resource-group-name> \\\n// -n <deployment-name> \\\n// --query properties.outputs.resourceID.value\noutput location string = location\noutput environmentId string = environment.id\n```\n\n<br />\n\n```bicep:./deploy/container-http.bicep\nparam containerAppName string\nparam location string\nparam environmentName string\nparam containerImage string\nparam containerPort int\nparam isExternalIngress bool\nparam containerRegistry string\n@secure()\nparam containerRegistryUsername string\nparam isPrivateRegistry bool\nparam enableIngress bool = true\n@secure()\nparam registryPassword string\nparam minReplicas int = 0\nparam secrets array = []\nparam env array = []\nparam revisionMode string = 'Single'\n\n// existing\n// デプロイ済みのリソースを参照。→この後、environment.id で使っている。\n// existing キーワードを使って参照した場合、リソースは再デプロイされない。\n// 今回の場合、environment.bicepで作成される マネージド環境（Azure Container Apps の環境）への参照。\nresource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {\n  name: environmentName\n}\n\nvar resources = [\n  {\n    cpu: '0.25'\n    memory: '0.5Gi'\n  }\n  {\n    cpu: '0.5'\n    memory: '1.0Gi'\n  }\n  {\n    cpu: '0.75'\n    memory: '1.5Gi'\n  }\n  {\n    cpu: '1.0'\n    memory: '2.0Gi'\n  }\n  {\n    cpu: '1.25'\n    memory: '2.5Gi'\n  }\n  {\n    cpu: '1.5'\n    memory: '3.0Gi'\n  }\n  {\n    cpu: '1.75'\n    memory: '3.5Gi'\n  }\n  {\n    cpu: '2.0'\n    memory: '4.0Gi'\n  }\n]\n\n// Azure Container Apps 作成\nresource containerApp 'Microsoft.App/containerApps@2022-03-01' = {\n  name: containerAppName\n  location: location\n  properties: {\n    // コンテナー アプリの環境のリソース ID\n    managedEnvironmentId: environment.id\n    configuration: {\n      // activeRevisionsMode: 'Multiple' | 'Single' | string\n      // ActiveRevisionsMode は、コンテナー アプリのアクティブなリビジョンの処理方法を制御。\n      // Multiple: 複数のリビジョンをアクティブにできる。\n      // Single: 一度にアクティブにできるリビジョンは 1 つだけ。\n      // Singleモードでは、リビジョン ウェイトは使用できない。\n      // 値が指定されていない場合、Singleモードがデフォルト。（今回は、Multipleを指定）\n      activeRevisionsMode: revisionMode\n      // コンテナ アプリで使用されるシークレットのコレクション\n      // 今回は、main.bicepから以下が渡されている。docker pull するために必要。\n      // [\n      //   {\n      //     name: registryPassword\n      //     value: containerRegistryPassword\n      //   }\n      // ]\n      secrets: secrets\n      // コンテナー アプリで使用されるコンテナーのプライベート コンテナー レジストリ資格情報のコレクション\n      // 今回は、プライベート（非公開）で、isPrivateRegistry=trueのため、nullではなく、セットされる。\n      registries: isPrivateRegistry ? [\n        {\n          // コンテナレジストリ。ここでは、Azure Container Registry （今回の場合は、acrtestyou.azurecr.io）\n          server: containerRegistry\n          // コンテナレジストリのログインユーザー名（事前にpull許可が与えられているサービスプリンシパルのclientId）\n          username: containerRegistryUsername\n          // コンテナレジストリのパスワード（事前にpull許可が与えられているサービスプリンシパルのclientSecret）\n          passwordSecretRef: registryPassword\n        }\n      ] : null\n      // イングレス設定\n      // Ingress：外部からのアクセス（主にHTTP）を管理するもの\n      // インターネット - イングレス - コンテナ\n      ingress: enableIngress ? {\n        // httpsのみ\n        allowInsecure: false\n        // インターネットに公開（true）\n        external: isExternalIngress\n        // コンテナ側のポート（3000）\n        targetPort: containerPort\n        // ポート転送方式（auto,http,http2,tcp）\n        transport: 'auto'\n        // トラフィック ルールを設定\n        traffic: [\n          {\n            // デプロイされた最新のリビジョン\n            latestRevision: true\n            // リビジョンに割り当てられたトラフィックの重み→100＝トラフィック分割しない。\n            weight: 100\n            // weight: 70 //ブルーグリーン・デプロイメントする場合、ここに割り振っておく。\n          }\n          // {\n          //   revisionName: 'react-app--prh39yx'←直近のリビジョン（自動で指定されるような工夫が必要だとは思うが、とりあえず、即値）\n          //   weight: 30\n          // }\n        ]\n      } : null\n      // Daprの設定\n      dapr: {\n        // Daprサイドカー無効\n        enabled: false\n      }\n    }\n    // コンテナー アプリのバージョン管理されたアプリケーション定義。\n    template: {\n      // コンテナ アプリのコンテナ定義のリスト。\n      containers: [\n        {\n          // コンテナイメージタグ\n          // 例：acrtestyou.azurecr.io/xxxxx/aca-app-repo/react-frontend:sha-734785c\n          image: containerImage\n          // カスタムコンテナ名\n          name: containerAppName\n          // env: EnvironmentVar[]\n          // コンテナ環境変数。\n          // 今回は、特に設定しないが、\n          // env: [\n          //   {\n          //     name: 'HOGEHOGE'\n          //     value: 'foobar'\n          //   }\n          // ]\n          // とした場合、例えば、nodeアプリの場合、アプリ側ソースコードで、process.env.HOGEHOGE のように参照できる。\n          env: env\n          // cpu: '0.25' memory: '0.5Gi'\n          resources: resources[0]\n          // 正常性プローブ\n          // Container Apps では、次のプローブがサポートされている。\n          // Liveness: レプリカの全体的な正常性を報告。\n          // Readiness: レプリカがトラフィックを受け入れる準備ができていることを示す。\n          // Startup: startup probe を使用して、遅いアプリの健全性または準備状態のレポートを遅延。\n          probes: [\n            {\n              // プローブが成功した後に失敗したと見なされる最小連続失敗回数。\n              // デフォルトは 3。最小値は 1。最大値は 10。\n              // failureThreshold: 3\n              // 実行する http 要求を指定\n              httpGet: {\n                // 接続先のホスト名。デフォルトはポッド IP。\n                // host: 'string'\n                // リクエストに設定するカスタム ヘッダー。\n                // httpHeaders: [\n                //   {\n                //     name: 'string'\n                //     value: 'string'\n                //   }\n                // ]\n                // アクセス先のパス、ポート番号\n                path: '/'\n                port: 3000\n                // ホストへの接続に使用するスキーム。 デフォルトは HTTP\n                scheme: 'HTTP'\n              }\n              // liveness プローブが開始されるまでのコンテナーの起動後の秒数。 最小値は 1。最大値は 60。\n              initialDelaySeconds: 60\n              // プローブを実行する頻度 (秒単位)。 デフォルトは 10 秒。 最小値は 1。最大値は 240。\n              periodSeconds: 30\n              // プローブが失敗した後に成功したと見なされるための最小連続成功。デフォルトは 1。LivenessとStartupには 1 である必要がある。 最小値は 1。最大値は 10。\n              // successThreshold: 1\n              // TCPSocket は、TCP ポートに関するアクションを指定。 TCP フックはまだサポートされていない。\n              // tcpSocket: {\n              //   host: 'string'\n              //   port: int\n              // }\n              // プローブの失敗時に Pod が正常に終了する必要があるオプションの期間 (秒単位)。\n              // terminationGracePeriodSeconds: int\n              // プローブがタイムアウトになるまでの秒数。 デフォルトは 1 秒。 最小値は 1。最大値は 240。\n              timeoutSeconds: 10\n              // type: 'Liveness' | 'Readiness' | 'Startup' | string\n              // プローブの種類\n              type: 'Startup'\n            }\n          ]\n        }\n      ]\n      scale: {\n        // minReplicas: int\n        // オプション。 コンテナー レプリカの最小数。0を指定して、使われていないと、無課金の待機状態になる。\n        minReplicas: minReplicas\n        // オプション。 コンテナー レプリカの最大数。 設定されていない場合、デフォルトは 10。\n        maxReplicas: 1\n      }\n    }\n  }\n}\n\n// fqdn = ホスト名\n// 今回、全コンテナ enableIngress=true のため、ホスト名が返る。\noutput fqdn string = enableIngress ? containerApp.properties.configuration.ingress.fqdn : 'Ingress not enabled'\n```\n\n<br />\n\n# コンテナレジストリ作成\n\nコンテナを push できる場所のコンテナレジストリ（Azure Container Registry のインスタンス）を作成します。\n\n<br />\n\nAzure CLI で、サインインします。\n\n```shellsession\n$ az login --use-device-code\nTo sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H*******W to authenticate.\n```\n\n`https://microsoft.com/devicelogin` にブラウザでアクセスして、表示されているコード（`H*******W`）を入力して、サインインします。\n\nAzure CLI 最新版を使っているか確認します。\n\n```shellsession\n$ az upgrade\n```\n\n<br />\n\n東日本リージョン `japaneast`（任意）に  \nリソースグループ `my-containerapp-store`（任意）を作成します。\n\n```shellsession\n$ az group create --name my-containerapp-store --location japaneast\n```\n\n<br />\n\n作成したリソースに Azure Container Registry のインスタンスを作成します。\n\n```shellsession\n$ az deployment group create --resource-group my-containerapp-store --template-file ./deploy/create-acr.bicep --parameters acrName=acrtestyou\n```\n\n<blockquote class=\"warn\">\n<p>ここで、bicepを使っていますが、特に重要な意味はありません。普通にコマンドラインで作成しても良いです。</p>\n<p>コマンドライン：<code>az acr create --resource-group $RES_GROUP --name $ACR_NAME --sku Basic --location japaneast</code></p>\n</blockquote>\n\n`my-containerapp-store` は、先ほど作成したリソースグループです。`acrtestyou` は任意ですが、以下の注意点があります。  \n\n<br />\n\n<span style=\"color: red;\"><strong>注意：</strong></span>  \n・`aclName=acrtestyou` とすると、`acrtestyou.azurecr.io` が作成されます。これは、全世界で重複しない必要があります。重複すると、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">The registry DNS name aaaaa.azurecr.io is already in use. You can check if the name is already claimed using following API:</span>  \n\n<br />\n\n・`aclName=acr-testyou` のように記号は使えません。アルファベットと数字のみです。また、5～50文字である必要があります。名前がまずい場合、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Invalid resource name: 'react-frontend'. Resource names may contain alpha numeric characters only and must be between 5 and 50 characters.. For more information, please refer resource name requirements at</span>  \n\n<br />\n\n# サービスプリンシパル作成\n\n共同作成者のロールを持ち、コンテナー レジストリのリソース グループをスコープとするサービス プリンシパルを作成します。\n\n```shellsession\n$ groupId=$(az group show \\\n  --name my-containerapp-store \\\n  --query id --output tsv)\n$ az ad sp create-for-rbac \\\n  --scope $groupId \\\n  --role Contributor \\\n  --sdk-auth\n```\n\n`my-containerapp-store` は、先ほど作成したリソースグループ名です。\n\n<br />\n\n成功したら、以下のような出力が得られます。\n<span style=\"color: red;\"><strong>この出力は後で使います。</strong></span>\n\n```json\n{\n  \"clientId\": \"d9******-****-****-****-**********e3\",\n  \"clientSecret\": \"V9************************************wS\",\n  \"subscriptionId\": \"ea******-****-****-****-**********80\",\n  \"tenantId\": \"0c******-****-****-****-**********2b\",\n  \"activeDirectoryEndpointUrl\": \"https://login.microsoftonline.com\",\n  \"resourceManagerEndpointUrl\": \"https://management.azure.com/\",\n  \"activeDirectoryGraphResourceId\": \"https://graph.windows.net/\",\n  \"sqlManagementEndpointUrl\": \"https://management.core.windows.net:8443/\",\n  \"galleryEndpointUrl\": \"https://gallery.azure.com/\",\n  \"managementEndpointUrl\": \"https://management.core.windows.net/\"\n}\n```\n\n<br />\n\nAzure サービス プリンシパルの資格情報を更新して、コンテナー レジストリに対するプッシュとプルのアクセスを許可します。  \nこの手順により、GitHub ワークフローでサービス プリンシパルを使用して、コンテナー レジストリに対する認証と、  \nDocker イメージのプッシュおよびプルを実行できます。  \nコンテナー レジストリのリソース ID を取得します。  \n環境変数 `registryId` に一旦、先ほど作成した Azure Container Registry のインスタンス のリソースＩＤを格納します。\n\n```shellsession\n$ registryId=$(az acr show \\\n  --name acrtestyou \\\n  --resource-group my-containerapp-store \\\n  --query id --output tsv)\n$ echo $registryId\n/subscriptions/ea0c9***-****-****-****-*******a0c80/resourceGroups/my-containerapp-store/providers/Microsoft.ContainerRegistry/registries/acrtestyou\n```\n\n`my-containerapp-store` は、先ほど作成したリソースグループ名です。`acrtestyou` は、先ほど作成した Azure Container Registry です。\n\n<br />\n\n`az role assignment create` を使用して、レジストリに対するプッシュおよびプル アクセスを付与する AcrPush ロールを割り当てます。 \n\n```shellsession\n$ az role assignment create \\\n  --assignee d9******-****-****-****-**********e3 \\\n  --scope $registryId \\\n  --role AcrPush\n```\n\n<span style=\"color: red;\"><strong>`--assignee d9******-****-****-****-**********e3` のところは、先ほど JSON 出力にあった clientId です。</strong></span>\n\n<br />\n\n# GitHub 準備\n\n## リポジトリ作成\n\nGitHub にプライベートリポジトリ `aca-app-repo` を作成します。（リポジトリの作り方については省略します。）  \nリポジトリ名は任意です。（サービスプリンシパルのアプリ名に合わせる必要もありません。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image2.png\" alt=\"リポジトリ作成\" width=\"1200\" height=\"451\" loading=\"lazy\"></a>\n\n<br />\n\n## Personal access tokens\n\n・GitHub ワークフロー実行権限（`workflow`）  \n・GitHub Container Registry への push 権限（`write:packages`）  \nを有効にします。\n\n<br />\n\n**Settings** -> 左下の **Developer settings** -> **Personal access tokens** -> **Tokens (classic)**  \nにて、`workflow` と `write:packages` にチェックを入れ、`Update token` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image3.png\" alt=\"Personal access tokens手順　Settings\" width=\"1200\" height=\"557\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image4.png\" alt=\"Personal access tokens手順　Developer settings\" width=\"1200\" height=\"673\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image5.png\" alt=\"Personal access tokens手順　Update token\" width=\"1200\" height=\"868\" loading=\"lazy\"></a>\n\n<br />\n\n## シークレット\n\nシークレット（ワークフロー内で使う秘密の値）を設定します。\n\n<br />\n\nリポジトリ `aca-app-repo` に戻って、\n\n**Settings** -> **Secrets** -> **Actions** -> **New repository secret** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image6.png\" alt=\"New repository secret\" width=\"1156\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\nここに、以下を設定します。  \n**AZURE_CREDENTIALS**：先ほどのサービス プリンシパルの作成の JSON 出力全体  \n**REGISTRY_LOGIN_SERVER**：レジストリのログイン サーバー名（今回は、`acrtestyou.azurecr.io`）  \n**REGISTRY_USERNAME**：サービス プリンシパルの作成 JSON 出力の clientId（今回は、`d9******-****-****-****-**********e3`）  \n**REGISTRY_PASSWORD**：サービス プリンシパルの作成 JSON 出力の clientSecret（今回は、`V9************************************wS`）  \n**RESOURCE_GROUP**：サービス プリンシパルのスコープ指定に使用したリソース グループの名前（今回は、`my-containerapp-store`）  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image7.png\" alt=\"AZURE_CREDENTIALS、RESOURCE_GROUP\" width=\"1163\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\n# commit & push\n\nリポジトリ `aca-app-repo` main ブランチに push します。\n\n```shellsession\n$ git init\n$ git config --local user.name \"AAAAA BBBBB\"\n$ git config --local user.email \"xxxxx@example.com\"\n$ git add .\n$ git commit -m \"first commit\"\n$ git branch -M main\n$ git remote add origin https://github.com/<github user name>/aca-app-repo.git\n$ git push -u origin main\n```\n\n`<github user name>` 部分は、GitHub ユーザー名です。\n\n<br />\n\n# Run workflow\n\n**Actions** -> **Build and Deploy** -> **Run workflow**  \nをクリックして、`Branch: main` のままにして、**Run workflow** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image8.png\" alt=\"Run workflow\" width=\"1152\" height=\"603\" loading=\"lazy\"></a>\n\n<br />\n\n**`Set Environment Vairables` ジョブ**  \n後続のジョブで使用する値を生成しています。  \n↓  \n**`package-services` ジョブ**  \ndocker コンテナをビルドして、コンテナレジストリ（今回の場合、`acrtestyou.azurecr.io`）にビルドしたコンテナを push しています。  \n↓  \n**`deploy`** ジョブ  \nAzure リソースをデプロイしています。  \n（既にデプロイ済みの場合は、Azure Container Apps アプリ`react-app`のリビジョンが一つ増えます。）\n\n<br />\n\nと進んで、全て緑色のチェックが付いたら、デプロイ完了です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image9.png\" alt=\"デプロイ完了\" width=\"1200\" height=\"557\" loading=\"lazy\"></a>\n\n`deploy` ジョブで作成されるリソースは、以下です。  \n・ログ分析ワークスペース  \n・Application Insights  \n・Azure Container Apps の環境  \n・Azure Container Apps のアプリ\n\n<br />\n\nAzure Container Apps アプリ `react-app` のアプリ名は、`./deploy/main.bicep` の  \n`var reactFrontendAppName = 'react-app'`  \nに書かれています。（アプリ名は任意です。）\n\n<br />\n\n# 動作確認１\n\n**Azure Portal**（`https://portal.azure.com/`） -> **コンテナ― アプリ** -> **`react-app`** -> **リビジョン管理**  \nを見ると、リビジョンが一つだけ有ることが分かります。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image10.png\" alt=\"コンテナ― アプリ選択\" width=\"1134\" height=\"323\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image11.png\" alt=\"リビジョン管理　リビジョンが一つだけ有る\" width=\"1156\" height=\"732\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** から **アプリケーション URL** にアクセスすると、アプリが起動しています。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image12.png\" alt=\"概要　アプリケーション URL\" width=\"1143\" height=\"726\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" alt=\"アプリ起動確認\" width=\"582\" height=\"403\" loading=\"lazy\"></a>\n\n<br />\n\n# CI/CD\n\n`App.tsx` のメッセージに \"New\" という言葉を足して、commit & push します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image13.png\" alt=\"Newという言葉追加\" width=\"708\" height=\"323\" loading=\"lazy\"></a>\n\n```shellsession\n$ git add .\n$ git commit -m \"second commit\"\n$ git push\n```\n\n<br />\n\n# 動作確認２\n\n**Azure Portal**（`https://portal.azure.com/`） -> **コンテナ― アプリ** -> **`react-app`** -> **リビジョン管理**\nを見ると、リビジョンが二つ有ることが分かります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image14.png\" alt=\"リビジョン管理　リビジョンが二つ有る\" width=\"1160\" height=\"735\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** から **アプリケーション URL** にアクセスすると、アプリが起動しています。\"New\" という言葉が反映された最新アプリということが分かります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image15.png\" alt=\"Newという言葉が追加されている\" width=\"666\" height=\"530\" loading=\"lazy\"></a>\n\n<br />\n\n一連の作成物は、以下のコマンドでリソースグループごと削除することで、まとめて削除できます。\n\n```shellsession\n$ az group delete \\\n  --resource-group my-containerapp-store\n```\n\n<br />\n\n# トラフィック変更\n\n## 70%,30%\n\nトラフィックを70%,30%の配分にして、保存します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/zu2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/zu2.png\" alt=\"リビジョントラフィック 70%,30%配分 図\" width=\"602\" height=\"311\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image1.png\" alt=\"リビジョントラフィック 70%,30%配分 設定\" width=\"1173\" height=\"425\" loading=\"lazy\"></a>\n\n<br />\n\n↓  \n\n<br />\n\n10回に3回くらい、前（\"New\"無し）の状態に戻ります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image2.png\" alt=\"リビジョントラフィック 70%,30%配分 結果\" width=\"981\" height=\"481\" loading=\"lazy\"></a>\n\n<br />\n\n## 100% 入れ替え\n\n前（\"New\"無し）のリビジョンを 100% にして、保存します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/zu3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/zu3.png\" alt=\"リビジョントラフィック 0%,100%配分 図\" width=\"602\" height=\"311\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image3.png\" alt=\"リビジョントラフィック 0%,100%配分 設定\" width=\"1167\" height=\"418\" loading=\"lazy\"></a>\n\n<br />\n\n↓  \n\n<br />\n\n前（\"New\"無し）の状態のままになります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/image4.png\" alt=\"リビジョントラフィック 0%,100%配分 結果\" width=\"981\" height=\"481\" loading=\"lazy\"></a>\n\n<br />\n\n## スケーリングについて\n今回の場合、Pod のスケーリングに関して、最小レプリカ数 0 です。  \nしたがって、しばらくすると、0 になり、無課金状態に移行します。  \nその代わり、Pod レプリカ数 0 → 1 に復帰する時、１分近く待ちますので、注意が必要です。  \n今回の場合、（Pod レプリカ数 0 の状態で）ブラウザでアクセスすると、読み込み待ちが１分近く続きました。  \n","description":"Azure Container Apps へ bicep、Azure Container Registry、GitHub Actions を使って React Web アプリをデプロイしてみました。一連の手順の紹介です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"yyao1_kpprss","createdAt":"2022-11-15T14:08:44.290Z","updatedAt":"2022-11-15T14:08:44.290Z","publishedAt":"2022-11-15T14:08:44.290Z","revisedAt":"2022-11-15T14:08:44.290Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/acr-aca-bicep/ITC_Engineering_Blog.png","alt":"Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ","width":1200,"height":630}],"seo_authors":[]},{"id":"azure-container-bicep","createdAt":"2022-11-12T10:52:29.602Z","updatedAt":"2024-01-23T12:50:58.870Z","publishedAt":"2022-11-12T10:52:29.602Z","revisedAt":"2024-01-23T12:50:58.870Z","title":"Azure Container Appsへbicep,GitHub Container Registry,GitHub Actionsを使ってデプロイ","category":{"id":"r2upy60kf","createdAt":"2022-12-06T10:13:03.471Z","updatedAt":"2022-12-06T10:13:03.471Z","publishedAt":"2022-12-06T10:13:03.471Z","revisedAt":"2022-12-06T10:13:03.471Z","topics":"Bicep","logo":"/logos/Bicep.png","needs_title":true},"topics":[{"id":"r2upy60kf","createdAt":"2022-12-06T10:13:03.471Z","updatedAt":"2022-12-06T10:13:03.471Z","publishedAt":"2022-12-06T10:13:03.471Z","revisedAt":"2022-12-06T10:13:03.471Z","topics":"Bicep","logo":"/logos/Bicep.png","needs_title":true},{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"hyb2dlkbyj-y","createdAt":"2021-06-03T13:49:36.431Z","updatedAt":"2021-06-03T13:49:36.431Z","publishedAt":"2021-06-03T13:49:36.431Z","revisedAt":"2021-06-03T13:49:36.431Z","topics":"GitHub","logo":"/logos/GitHub.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false},{"id":"xego85dtzyu","createdAt":"2021-06-03T13:50:33.576Z","updatedAt":"2021-08-31T12:04:26.367Z","publishedAt":"2021-06-03T13:50:33.576Z","revisedAt":"2021-08-31T12:04:26.367Z","topics":"React","logo":"/logos/React.png","needs_title":false},{"id":"xnhxgx1v0","createdAt":"2022-04-08T10:53:39.471Z","updatedAt":"2022-04-08T10:53:39.471Z","publishedAt":"2022-04-08T10:53:39.471Z","revisedAt":"2022-04-08T10:53:39.471Z","topics":"TypeScript","logo":"/logos/TypeScript.png","needs_title":true}],"content":"# はじめに\n\nAzure Container Apps へ bicep、GitHub Container Registry、GitHub Actions を使って React Web アプリをデプロイしてみました。一連の手順を紹介していきたいと思います。\n\n<br />\n\n<blockquote class=\"info\">\n<p>2022年11月15日 追記：</p>\n<p>コンテナレジストリを GitHub Container Registry（GitHub Packages）から Azure Container Registry に変更した記事をアップしました。</p>\n<p>「<a href=\"https://itc-engineering-blog.netlify.app/blogs/acr-aca-bicep\" target=\"_blank\">Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ</a>」</p>\n</blockquote>\n\n<br />\n\n**Azure Container Apps**：フル マネージド サーバーレス コンテナー サービスです。Azure Container Apps を使うと、Kubernetes のオーケストレーションやインフラストラクチャを気にすることなく、コンテナー化されたアプリケーションを実行できます。  \n**bicep**：Bicep は、宣言型の構文を使用して Azure リソースをデプロイするドメイン固有言語 (DSL) です。無人でAzure をいじるときに使うプログラミング言語のようなものです。  \n**GitHub Actions**：GitHub Actions は、GitHub が提供する CI/CD サービスです。 GitHub と高度に統合されており、GitHub に公開されたコードを自動でビルド・テスト・デプロイを行うのが主目的です。  \n**GitHub Container Registry**：GitHub Packages を構成する１つで Docker を始めとしたコンテナを扱えるレジストリです。  \n**Web アプリ**：create-react-app で作成したものそのままです。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/zu1.png\" alt=\"Azure Container Appsデプロイ全体像の図\" width=\"1162\" height=\"1292\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\nパイプラインや CI/CD については、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-ci-cd\" target=\"_blank\">Kotlin ＋ Spring Boot のアプリを GitLab CI/CD による Docker デプロイまで全手順</a>」に詳しく書きましたので、こちらを参照してください。\n\n<blockquote class=\"info\">\n<p>【 コンテナレジストリ 】</p>\n<p>Azure Container Registry、GitHub Container Registry（ghcr.io）、Docker Registry、etc...の内、今回は、GitHub Container Registry（ghcr.io） を使います。Azure Container Registry、GitHub Container Registry（ghcr.io）以外の場合、どうなるかは検証していません。</p>\n</blockquote>\n\n<br />\n\n基本的には、learn.microsoft.com の「<a href=\"https://learn.microsoft.com/ja-jp/azure/container-apps/dapr-github-actions?tabs=bash\" target=\"_blank\">チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする</a>」を見ながら進めましたが、参考にしただけで、同一ではありません。Dapr 未使用で、Azure Cosmos DB は作成せず、コンテナも一つで、かなり単純化しています。\n\n<br />\n\n<blockquote class=\"alert\">\n<p>本記事情報により何らかの問題が生じても、一切責任を負いません。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>2022年11月現在、GitHub Container Registry（ghcr.io）を使わない方が良いかもしれません。詳細は、「<a href=\"#caution\">注意点</a>」のセクションを参照してください。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>コンソールは、Ubuntu 20.04 LTS の bash で作業しています。</p>\n<p>git コマンドや az コマンド、npx などはインストール済みで使えるものとします。</p>\n</blockquote>\n\n<br />\n\n# ソースコード準備\n\n<blockquote class=\"info\">\n<p>作成済みの全体ソースコードは、 <a href=\"https://github.com/itc-lab/azure-container-apps-bicep-example\" target=\"_blank\">https://github.com/itc-lab/azure-container-apps-bicep-example</a> にアップしました。</p>\n</blockquote>\n\ncreate-react-app で作成  \n↓  \nコンテナビルドのための Dockerfile  \n↓  \nGitHub Actions ワークフローファイル `.github/workflows/build-and-deploy.yaml`  \n↓  \nAzure Container Apps 他、Azure へのリソースデプロイ用に `./deploy/main.bicep`、`./deploy/environment.bicep`、`./deploy/container-http.bicep`  \nと準備していきます。\n\n<br />\n\n## create-react-app\n\n```shellsession\n$ npx create-react-app aca-app --template typescript\n$ cd aca-app\n$ npm start\n```\n\n自動的に最低限の Web アプリが作成されて、`http://localhost:3000` で以下の画面が表示されます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" alt=\"Webアプリ画面\" width=\"582\" height=\"403\" loading=\"lazy\"></a>\n\n<br />\n\n## Dockerfile\n\n`yarn build` した静的ファイルを nginx で参照する形のコンテナを作成するための、Dockerfile を準備します。\n\n```dockerfile:./Dockerfile\n# Dockerマルチステージビルド\nFROM node:16 as build\n\nWORKDIR /app\n\nCOPY package.json /app/\n\nRUN yarn\n\nCOPY . /app/\n\nRUN yarn build\n\n\nFROM nginx:latest\n\nCOPY --from=build /app/build /usr/share/nginx/html\n\nCOPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf\n```\n\n<br />\n\nnginx.conf を準備します。  \n3000 番ポートを開いています。\n\n```nginx:./nginx.conf\nserver {\n  listen 3000;\n\n  gzip on;\n  gzip_vary on;\n  gzip_min_length 10240;\n  gzip_proxied expired no-cache no-store private auth;\n  gzip_http_version 1.1;\n  gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/xml image/png;\n  gzip_disable \"MSIE [1-6]\\.\";\n\n  location / {\n    root /usr/share/nginx/html;\n    index index.html index.htm;\n    try_files $uri $uri/ /index.html =404;\n  }\n  include /etc/nginx/extra-conf.d/*.conf;\n}\n```\n\n<br />\n\n## build-and-deploy.yaml\n\nGitHub Actions パイプラインのワークフローファイル `.github/workflows/build-and-deploy.yaml` を作成します。\n\n<blockquote class=\"info\">\n<p>意味は、中のコメントを見てください。</p>\n</blockquote>\n\n```yaml:./.github/workflows/build-and-deploy.yaml\nname: Build and Deploy\non:\n  push:\n    # mainブランチにpushまたは、v*.*.* タグがreleaseされたときに発動\n    branches: [main]\n    tags: [\"v*.*.*\"]\n    # このファイルの変更だけの場合、パイプラインを実行しない。\n    # パイプラインとは、一連の自動化処理のこと。GitHubでは、ワークフローと言っている。\n    # ここに書いてある実行するかどうかの条件は、ワークフロー トリガーと言う。\n    paths-ignore:\n      - \"README.md\"\n      - \".vscode/**\"\n      - \"assets/**\"\n      - \"build-and-run.md\"\n  # 手動実行（GUIからRun workflowボタン）有効\n  workflow_dispatch:\n\nenv:\n  # ワークフロー全体で使える環境変数\n  # コンテナレジストリとして、GitHub Container Registry （ghcr.io）を使うため、環境変数に登録\n  REGISTRY: ghcr.io\njobs:\n  set-env:\n    name: Set Environment Variables\n    runs-on: ubuntu-latest\n    outputs:\n      # mainステップの実行結果を保存→他のjobで変数として使える。\n      # needs.set-env.outputs.version のように参照\n      version: ${{ steps.main.outputs.version }}\n      created: ${{ steps.main.outputs.created }}\n      repository: ${{ steps.main.outputs.repository }}\n      appname: ${{ steps.main.outputs.appname }}\n    steps:\n      # $GITHUB_SHA：ワークフローをトリガーしたコミット SHA。\n      # $GITHUB_REPOSITORY：リポジトリオーナー/リポジトリ名。例：octocat/Hello-World\n      - id: main\n        # set-outputコマンドを利用することで出力パラメータに値を代入することができたが、\n        # set-outputの使用は非推奨になった。\n        # echo \"::set-output name={name}::{value}\" で{name}に{value}を代入。\n        # ↓\n        # echo \"{name}={value}\" >> $GITHUB_OUTPUT で{name}に{value}を代入。\n        # cut -c1-7 ＝ 先頭７文字\n        run: |\n          echo version=$(echo ${GITHUB_SHA} | cut -c1-7) >> $GITHUB_OUTPUT\n          echo created=$(date -u +'%Y-%m-%dT%H:%M:%SZ') >> $GITHUB_OUTPUT\n          echo repository=$GITHUB_REPOSITORY >> $GITHUB_OUTPUT\n          echo appname=react-frontend >> $GITHUB_OUTPUT\n\n  # コンテナ build & コンテナレジストリへ push のジョブ\n  package-services:\n    # ランナー イメージ（ジョブを実行する仮想マシン）を指定。windows-latest、windows-2022、macos-12とかいろいろある。\n    # ランナーは自分で作ることもできて、自分で作ったランナーをセルフホストランナー（self-hosted runners）という。\n    runs-on: ubuntu-latest\n    # needs＝先行のジョブを指定（指定しないと、並列実行される。）\n    # この場合、needs: set-env が無いと、set-envジョブと同時にpackage-servicesジョブが動く。\n    needs: set-env\n    # secrets.GITHUB_TOKENの権限を設定。このワークフローで必要な権限を設定。\n    permissions:\n      # ソースコードリポジトリは、読み取りのみ\n      contents: read\n      # コンテナレジストリは書き込み可\n      packages: write\n    outputs:\n      # image-tagステップにて、echo image-${{ needs.set-env.outputs.appname }}=*・・・ が実行されていて、\n      # その値をcontainerImage-reactという名称で、他ジョブでも使えるように出力している。\n      containerImage-react: ${{ steps.image-tag.outputs.image-react-frontend }}\n    steps:\n      - name: Checkout repository\n        # actions/checkout@v2アクション\n        # pushされた時にはそのpushされたコードをチェックアウト\n        # プルリク時には、そのプルリクされたコードをチェックアウト\n        uses: actions/checkout@v2\n      - name: Log into registry ${{ env.REGISTRY }}\n        # プルリク以外だったら実行\n        if: github.event_name != 'pull_request'\n        # GitHub Container Registryにdocker imageをpushするためにdocker login。\n        # docker/login-action@v1アクションを利用。\n        # withでアクションに必要なパラメータを定義。\n        uses: docker/login-action@v1\n        with:\n          # ログインするコンテナレジストリ。未指定の場合はDocker Hub（今回は、ghcr.ioだから指定しないといけない。）\n          registry: ${{ env.REGISTRY }}\n          # ghcr.ioにログインするユーザー。github.actorは、このワークフローを実行しているユーザー。\n          username: ${{ github.actor }}\n          # secrets.GITHUB_TOKEN は、Settings -> Developer settings -> Personal access tokens で設定するトークン\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Extract Docker metadata\n        id: meta\n        # docker/metadata-action@v3アクション\n        # 動的なタグ生成を実施→docker build時に指示できる\n        # ビルドされたimageが\n        # ghcr.io/xxx/aaa:latest、ghcr.io/xxx/aaa:1.2.3、ghcr.io/xxx/aaa:1.2、ghcr.io/xxx/aaa:1、\n        # ghcr.io/xxx/aaa:sha-ad132f5、ghcr.io/xxx/aaa:main\n        # といったタグでpushされる。\n        uses: docker/metadata-action@v3\n        with:\n          images: ${{ env.REGISTRY }}/${{ needs.set-env.outputs.repository }}/${{ needs.set-env.outputs.appname }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=ref,event=branch\n            type=sha\n      - name: Build and push Docker image\n        # docker/build-push-action@v2アクション\n        # コンテナ build & コンテナレジストリへ push\n        uses: docker/build-push-action@v2\n        with:\n          # context: Dockerfileが置いてある場所\n          # 今回は、リポジトリ直下に置いているが、ディレクトリの中なら、'./frontend'、'./backend' とか。\n          context: \".\"\n          # コンテナレジストリにpushするかどうか。プルリク以外は、push。\n          push: ${{ github.event_name != 'pull_request' }}\n          # 上で設定したタグを適用\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n      - name: Output image tag\n        id: image-tag\n        # image-react-frontend = ghcr.io/<github-username>/aca-app/react-frontend:sha-<コミットSHA（先頭７文字）>\n        # tr '[:upper:]' '[:lower:]' は、小文字化するLinuxコマンド\n        run: |\n          echo image-${{ needs.set-env.outputs.appname }}=${{ env.REGISTRY }}/$GITHUB_REPOSITORY/${{ needs.set-env.outputs.appname }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT\n\n  deploy:\n    runs-on: ubuntu-latest\n    # package-servicesジョブ終了後に実行\n    # 複数終了待ちの場合：[job1, job2] で job1とjob2 両方が終わっている必要があるという意味になる。\n    needs: package-services\n    steps:\n      - name: Checkout repository\n        # actions/checkout@v2アクション\n        # リポジトリをチェックアウト\n        uses: actions/checkout@v2\n\n      - name: Azure Login\n        # azure/login@v1アクション\n        # Azureにログイン\n        uses: azure/login@v1\n        with:\n          # サービス プリンシパル シークレット（事前にAzureに作成して、シークレットをGitHubのGUIで登録）\n          # Azure CLI で bicep を実行、Azure Container Appsにデプロイする権限を得るために必要。\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n\n      - name: Deploy bicep\n        # azure/CLI@v1アクション\n        # Azure CLI （Azure内のターミナル）で実行\n        uses: azure/CLI@v1\n        with:\n          # Azureリソースグループを東日本リージョン（japaneast）に作成\n          # az deployment group create -g <リソース グループ> -f <bicepファイル> でAzureへのデプロイを開始。\n          # -p は引数指定（main.bicep内で使うパラメータ）\n          # minReplicas：最小レプリカ（Pod）数\n          # reactImage：React App の containerImage\n          # reactPort：React App の containerPort\n          # containerRegistry：コンテナレジストリ。ここでは、GitHub Container Registry （ghcr.io）\n          # containerRegistryUsername：コンテナレジストリのログインユーザー名（これを実行しているGitHubユーザー）\n          # containerRegistryPassword：コンテナレジストリのパスワード（Personal access tokens）\n          inlineScript: |\n            az group create -g ${{ secrets.RESOURCE_GROUP }} -l japaneast\n            az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep \\\n              -p \\\n                minReplicas=0 \\\n                reactImage='${{ needs.package-services.outputs.containerImage-react }}' \\\n                reactPort=3000 \\\n                containerRegistry=${{ env.REGISTRY }} \\\n                containerRegistryUsername=${{ github.actor }} \\\n                containerRegistryPassword=${{ secrets.GITHUB_TOKEN }}\n```\n\n<br />\n\n## Bicep\n\nAzure Container Apps 他、Azure へのリソースデプロイ用に `./deploy/main.bicep`、`./deploy/environment.bicep`、`./deploy/container-http.bicep` を準備します。  \nこれは、GitHub Actions の 最後に `az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep・・・` で実行されています。\n\n```bicep:./deploy/main.bicep\n// param <parameter-name> <parameter-data-type> = <default-value>\n// 注意：=の右側 <default-value> は、-p で与えられなかった場合、適用される。\n// 今回、build-and-deploy.yaml にて以下のように実行されている。\n// -p \\\n// minReplicas=0 \\\n// reactImage='${{ needs.package-services.outputs.containerImage-react }}' \\\n// reactPort=3000 \\\n// containerRegistry=${{ env.REGISTRY }} \\\n// containerRegistryUsername=${{ github.actor }} \\\n// containerRegistryPassword=${{ secrets.GITHUB_TOKEN }}\n\n// function resourceGroup(): resourceGroup\n// 現在のリソース グループのスコープを返す\nparam location string = resourceGroup().location\n// function uniqueString(... : string): string\n// パラメータとして提供された値に基づいてハッシュ文字列を作成。戻り値は 13 文字。\nparam environmentName string = 'env-${uniqueString(resourceGroup().id)}'\n\nparam minReplicas int = 0\n\nparam reactImage string\nparam reactPort int = 3000\n// var で始まる部分はコマンドライン等からは変更できないファイル内で利用する内部変数\nvar reactFrontendAppName = 'react-app'\n\nparam isPrivateRegistry bool = true\n\nparam containerRegistry string\nparam containerRegistryUsername string = 'testUser'\n// function secure(): any\n// @secure() 修飾子\n// パラメーターの値はデプロイ履歴に保存されず、ログにも記録されない。\n@secure()\nparam containerRegistryPassword string = ''\n// コンテナレジストリシークレットのキー名\n// ただのキー名だけど、\"Password\"という言葉に反応して、secure-secrets-in-params警告が出るため、\n// #disable-next-line で沈黙させる。\n#disable-next-line secure-secrets-in-params\nparam registryPassword string = 'registry-password'\n\n// Container Apps 環境モジュール\n// module <symbolic-name> '<path-to-file>' = {\n//   name: '<linked-deployment-name>'\n//   params: {\n//     <parameter-names-and-values>\n//   }\n// }\n// <path-to-file>は、テンプレートのようなもので、値を渡して何回も使用可能。\n// 注意：今回は、１回しか使っていない。\n// 各パラーメータの説明は、environment.bicep、ontainer-http.bicep 内に記載。\nmodule environment 'environment.bicep' = {\n  // function deployment(): deployment\n  // 現在のデプロイメント操作に関する情報を返す。\n  // ここでは、name: \"main--environment\" になる。\n  name: '${deployment().name}--environment'\n  params: {\n    environmentName: environmentName\n    location: location\n    appInsightsName: '${environmentName}-ai'\n    logAnalyticsWorkspaceName: '${environmentName}-la'\n  }\n}\n\n// React App\nmodule reactFrontend 'container-http.bicep' = {\n  name: '${deployment().name}--${reactFrontendAppName}'\n  dependsOn: [\n    environment\n  ]\n  params: {\n    enableIngress: true\n    isExternalIngress: true\n    location: location\n    environmentName: environmentName\n    containerAppName: reactFrontendAppName\n    containerImage: reactImage\n    containerPort: reactPort\n    minReplicas: minReplicas\n    isPrivateRegistry: isPrivateRegistry\n    containerRegistry: containerRegistry\n    registryPassword: registryPassword\n    containerRegistryUsername: containerRegistryUsername\n    revisionMode: 'Multiple'\n    secrets: [\n      {\n        name: registryPassword\n        value: containerRegistryPassword\n      }\n    ]\n  }\n}\n\noutput reactFqdn string = reactFrontend.outputs.fqdn\n```\n\n<br />\n\n```bicep:./deploy/environment.bicep\nparam environmentName string\nparam logAnalyticsWorkspaceName string\nparam appInsightsName string\nparam location string\n\n// ログ分析ワークスペース 作成\n// resource キーワードを使用してリソースを追加。\n// リソースのシンボリック名を設定。シンボリック名はリソース名と同じものではない。\n// シンボリック名は、Bicep ファイルの他の部分にあるリソースを参照するために使用。\n// resource <symbolic-name> '<full-type-name>@<api-version>' = {\n//   <resource-properties>\n// }\nresource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = {\n  name: logAnalyticsWorkspaceName\n  location: location\n  properties: any({\n    // ワークスペースのデータ保持日数。 -1 は、Unlimited Sku の場合、無制限。\n    // 他のすべての Sku で許可される最大日数は 730 日。\n    retentionInDays: 30\n    // ？どこにもこの項目の説明が無い。\n    features: {\n      searchVersion: 1\n    }\n    // SKU（Stock Keeping Unit）\n    // Basic、Standardなどの課金形態のこと。\n    // 2018 年 4 月の価格モデルを選択したサブスクリプションで Log Analytics ワークスペースを作成または構成する場合、\n    // 有効な Log Analytics 価格レベルは PerGB2018 のみ。\n    sku: {\n      name: 'PerGB2018'\n    }\n  })\n}\n\n// Application Insights 作成\n// Application Insights は Azure Monitor の拡張機能であり、\n// アプリケーション パフォーマンス監視 (\"APM\" とも呼ばれる) 機能を提供。\n// APM ツールは、開発、テスト、運用環境からアプリケーションを監視するのに役立つ。\nresource appInsights 'Microsoft.Insights/components@2020-02-02' = {\n  name: appInsightsName\n  location: location\n  // このコンポーネントが参照するアプリケーションの種類。\n  // 任意の文字列で、値は通常、web、ios、other、store、java、phone のいずれか。\n  kind: 'web'\n  properties: {\n    // 監視アプリケーションの種類\n    Application_Type: 'web'\n    // データが取り込まれるログ分析ワークスペースのリソース ID\n    // （logAnalyticsWorkspace は上で作成しているリソースを参照している）\n    WorkspaceResourceId:logAnalyticsWorkspace.id\n  }\n}\n\n// マネージド環境（Azure Container Apps の環境）作成\nresource environment 'Microsoft.App/managedEnvironments@2022-03-01' = {\n  // 環境名：'env-${uniqueString(resourceGroup().id)}'\n  name: environmentName\n  location: location\n  properties: {\n    // サービスをサービス通信テレメトリにエクスポートするために Dapr によって使用される Azure Monitor インストルメンテーション キー\n    // Application Insights インストルメンテーション キー。\n    // アプリケーションが Azure Application Insights に送信されるすべてのテレメトリの送信先を識別するために使用できる読み取り専用の値。\n    // この値は、新しい各 Application Insights コンポーネントの構築時に提供される。\n    // appInsightsは、上で作成しているApplication Insights。\n    // 今回は、Dapr を導入しないため、意味無いかも。\n    daprAIInstrumentationKey:appInsights.properties.InstrumentationKey\n    appLogsConfiguration: {\n      // ログ記録先。 現在、「log-analytics」のみがサポートされている。\n      destination: 'log-analytics'\n      logAnalyticsConfiguration: {\n        // Log AnalyticsのcustomerIdとsharedKey\n        customerId: logAnalyticsWorkspace.properties.customerId\n        // function list*([apiVersion: string], [functionValues: object]): any\n        // この関数の構文は、リスト操作の名前によって異なる。\n        // 一般的な使用法には、listKeys、listKeyValue、および listSecrets がある。\n        // 各実装は、リスト操作をサポートするリソース タイプの値を返す。\n        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey\n      }\n    }\n  }\n}\n\n// output <name> <data-type> = <value>\n// デプロイが成功すると、出力値はデプロイの結果で自動的に返される。\n// Azure CLI または Azure PowerShell スクリプトでデプロイ履歴から出力値を参照できる。\n// az deployment group show \\\n// -g <resource-group-name> \\\n// -n <deployment-name> \\\n// --query properties.outputs.resourceID.value\noutput location string = location\noutput environmentId string = environment.id\n```\n\n<br />\n\n```bicep:./deploy/container-http.bicep\nparam containerAppName string\nparam location string\nparam environmentName string\nparam containerImage string\nparam containerPort int\nparam isExternalIngress bool\nparam containerRegistry string\nparam containerRegistryUsername string\nparam isPrivateRegistry bool\nparam enableIngress bool = true\n@secure()\nparam registryPassword string\nparam minReplicas int = 0\nparam secrets array = []\nparam env array = []\nparam revisionMode string = 'Single'\n\n// existing\n// デプロイ済みのリソースを参照。→この後、environment.id で使っている。\n// existing キーワードを使って参照した場合、リソースは再デプロイされない。\n// 今回の場合、environment.bicepで作成される マネージド環境（Azure Container Apps の環境）への参照。\nresource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {\n  name: environmentName\n}\n\nvar resources = [\n  {\n    cpu: '0.25'\n    memory: '0.5Gi'\n  }\n  {\n    cpu: '0.5'\n    memory: '1.0Gi'\n  }\n  {\n    cpu: '0.75'\n    memory: '1.5Gi'\n  }\n  {\n    cpu: '1.0'\n    memory: '2.0Gi'\n  }\n  {\n    cpu: '1.25'\n    memory: '2.5Gi'\n  }\n  {\n    cpu: '1.5'\n    memory: '3.0Gi'\n  }\n  {\n    cpu: '1.75'\n    memory: '3.5Gi'\n  }\n  {\n    cpu: '2.0'\n    memory: '4.0Gi'\n  }\n]\n\n// Azure Container Apps 作成\nresource containerApp 'Microsoft.App/containerApps@2022-03-01' = {\n  name: containerAppName\n  location: location\n  properties: {\n    // コンテナー アプリの環境のリソース ID\n    managedEnvironmentId: environment.id\n    configuration: {\n      // activeRevisionsMode: 'Multiple' | 'Single' | string\n      // ActiveRevisionsMode は、コンテナー アプリのアクティブなリビジョンの処理方法を制御。\n      // Multiple: 複数のリビジョンをアクティブにできる。\n      // Single: 一度にアクティブにできるリビジョンは 1 つだけ。\n      // Singleモードでは、リビジョン ウェイトは使用できない。\n      // 値が指定されていない場合、Singleモードがデフォルト。（今回は、Multipleを指定）\n      activeRevisionsMode: revisionMode\n      // コンテナ アプリで使用されるシークレットのコレクション\n      // 今回は、main.bicepから以下が渡されている。docker pull するために必要。\n      // [\n      //   {\n      //     name: registryPassword\n      //     value: containerRegistryPassword\n      //   }\n      // ]\n      secrets: secrets\n      // コンテナー アプリで使用されるコンテナーのプライベート コンテナー レジストリ資格情報のコレクション\n      // 今回は、プライベート（非公開）で、isPrivateRegistry=trueのため、nullではなく、セットされる。\n      registries: isPrivateRegistry ? [\n        {\n          // GitHub Container Registry （ghcr.io）\n          server: containerRegistry\n          // containerRegistryUsername=${{ github.actor }}\n          username: containerRegistryUsername\n          // containerRegistryPassword=${{ secrets.GITHUB_TOKEN }}←Settings -> Developer settings -> Personal access tokens\n          passwordSecretRef: registryPassword\n        }\n      ] : null\n      // イングレス設定\n      // Ingress：外部からのアクセス（主にHTTP）を管理するもの\n      // インターネット - イングレス - コンテナ\n      ingress: enableIngress ? {\n        // httpsのみ\n        allowInsecure: false\n        // インターネットに公開（true）\n        external: isExternalIngress\n        // コンテナ側のポート（3000）\n        targetPort: containerPort\n        // ポート転送方式（auto,http,http2,tcp）\n        transport: 'auto'\n        // トラフィック ルールを設定\n        traffic: [\n          {\n            // デプロイされた最新のリビジョン\n            latestRevision: true\n            // リビジョンに割り当てられたトラフィックの重み→100＝トラフィック分割しない。\n            weight: 100\n            // weight: 70 //ブルーグリーン・デプロイメントする場合、ここに割り振っておく。\n          }\n          // {\n          //   revisionName: 'react-app--prh39yx'←直近のリビジョン（自動で指定されるような工夫が必要だとは思うが、とりあえず、即値）\n          //   weight: 30\n          // }\n        ]\n      } : null\n      // Daprの設定\n      dapr: {\n        // Daprサイドカー無効\n        enabled: false\n      }\n    }\n    // コンテナー アプリのバージョン管理されたアプリケーション定義。\n    template: {\n      // コンテナ アプリのコンテナ定義のリスト。\n      containers: [\n        {\n          // カスタムイメージタグ\n          image: containerImage\n          // カスタムコンテナ名\n          name: containerAppName\n          // env: EnvironmentVar[]\n          // コンテナ環境変数。\n          // 今回は、特に設定しないが、\n          // env: [\n          //   {\n          //     name: 'HOGEHOGE'\n          //     value: 'foobar'\n          //   }\n          // ]\n          // とした場合、例えば、nodeアプリの場合、アプリ側ソースコードで、process.env.HOGEHOGE のように参照できる。\n          env: env\n          // cpu: '0.25' memory: '0.5Gi'\n          resources: resources[0]\n          // 正常性プローブ\n          // Container Apps では、次のプローブがサポートされている。\n          // Liveness: レプリカの全体的な正常性を報告。\n          // Readiness: レプリカがトラフィックを受け入れる準備ができていることを示す。\n          // Startup: startup probe を使用して、遅いアプリの健全性または準備状態のレポートを遅延。\n          probes: [\n            {\n              // プローブが成功した後に失敗したと見なされる最小連続失敗回数。\n              // デフォルトは 3。最小値は 1。最大値は 10。\n              // failureThreshold: 3\n              // 実行する http 要求を指定\n              httpGet: {\n                // 接続先のホスト名。デフォルトはポッド IP。\n                // host: 'string'\n                // リクエストに設定するカスタム ヘッダー。\n                // httpHeaders: [\n                //   {\n                //     name: 'string'\n                //     value: 'string'\n                //   }\n                // ]\n                // アクセス先のパス、ポート番号\n                path: '/'\n                port: 3000\n                // ホストへの接続に使用するスキーム。 デフォルトは HTTP\n                scheme: 'HTTP'\n              }\n              // liveness プローブが開始されるまでのコンテナーの起動後の秒数。 最小値は 1。最大値は 60。\n              initialDelaySeconds: 60\n              // プローブを実行する頻度 (秒単位)。 デフォルトは 10 秒。 最小値は 1。最大値は 240。\n              periodSeconds: 30\n              // プローブが失敗した後に成功したと見なされるための最小連続成功。デフォルトは 1。LivenessとStartupには 1 である必要がある。 最小値は 1。最大値は 10。\n              // successThreshold: 1\n              // TCPSocket は、TCP ポートに関するアクションを指定。 TCP フックはまだサポートされていない。\n              // tcpSocket: {\n              //   host: 'string'\n              //   port: int\n              // }\n              // プローブの失敗時に Pod が正常に終了する必要があるオプションの期間 (秒単位)。\n              // terminationGracePeriodSeconds: int\n              // プローブがタイムアウトになるまでの秒数。 デフォルトは 1 秒。 最小値は 1。最大値は 240。\n              timeoutSeconds: 10\n              // type: 'Liveness' | 'Readiness' | 'Startup' | string\n              // プローブの種類\n              type: 'Startup'\n            }\n          ]\n        }\n      ]\n      scale: {\n        // minReplicas: int\n        // オプション。 コンテナー レプリカの最小数。0を指定して、使われていないと、無課金の待機状態になる。\n        minReplicas: minReplicas\n        // オプション。 コンテナー レプリカの最大数。 設定されていない場合、デフォルトは 10。\n        maxReplicas: 1\n      }\n    }\n  }\n}\n\n// fqdn = ホスト名\n// 今回、全コンテナ enableIngress=true のため、ホスト名が返る。\noutput fqdn string = enableIngress ? containerApp.properties.configuration.ingress.fqdn : 'Ingress not enabled'\n```\n\n<br />\n\n# サービスプリンシパル作成\n\nGitHub から Azure の操作を許すためにサービスプリンシパルを作成します。（Azure Active Direcotry の アプリの登録に相当します。）\n\n<br />\n\nAzure CLI で、サインインします。\n\n```shellsession\n$ az login --use-device-code\nTo sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H*******W to authenticate.\n```\n\n`https://microsoft.com/devicelogin` にブラウザでアクセスして、表示されているコード（`H*******W`）を入力して、サインインします。\n\nAzure CLI 最新版を使っているか確認します。\n\n```shellsession\n$ az upgrade\n```\n\nサービスプリンシパルを作成します。\n\n```shellsession\n$ az ad sp create-for-rbac \\\n  --name aca-app \\\n  --role \"contributor\" \\\n  --scopes /subscriptions/ea******-****-****-****-**********80 \\\n  --sdk-auth\n```\n\n`aca-app` は、アプリ名で、任意です。\n`ea******-****-****-****-**********80` は、サブスクリプション ID です。\nサインインした後の `\"id\": ・・・` に表示されます。\n\n成功したら、以下のような出力が得られます。\n<span style=\"color: red;\"><strong>この出力は後で使います。</strong></span>\n\n```json\n{\n  \"clientId\": \"d9******-****-****-****-**********e3\",\n  \"clientSecret\": \"V9************************************wS\",\n  \"subscriptionId\": \"ea******-****-****-****-**********80\",\n  \"tenantId\": \"0c******-****-****-****-**********2b\",\n  \"activeDirectoryEndpointUrl\": \"https://login.microsoftonline.com\",\n  \"resourceManagerEndpointUrl\": \"https://management.azure.com/\",\n  \"activeDirectoryGraphResourceId\": \"https://graph.windows.net/\",\n  \"sqlManagementEndpointUrl\": \"https://management.core.windows.net:8443/\",\n  \"galleryEndpointUrl\": \"https://gallery.azure.com/\",\n  \"managementEndpointUrl\": \"https://management.core.windows.net/\"\n}\n```\n\n<br />\n\n# GitHub 準備\n\n## リポジトリ作成\n\nGitHub にプライベートリポジトリ `aca-app-repo` を作成します。（リポジトリの作り方については省略します。）  \nリポジトリ名は任意です。（サービスプリンシパルのアプリ名に合わせる必要もありません。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image2.png\" alt=\"リポジトリ作成\" width=\"1200\" height=\"451\" loading=\"lazy\"></a>\n\n<br />\n\n## Personal access tokens\n\n・GitHub ワークフロー実行権限（`workflow`）  \n・GitHub Container Registry への push 権限（`write:packages`）  \nを有効にします。\n\n<br />\n\n**Settings** -> 左下の **Developer settings** -> **Personal access tokens** -> **Tokens (classic)**  \nにて、`workflow` と `write:packages` にチェックを入れ、`Update token` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image3.png\" alt=\"Personal access tokens手順　Settings\" width=\"1200\" height=\"557\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image4.png\" alt=\"Personal access tokens手順　Developer settings\" width=\"1200\" height=\"673\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image5.png\" alt=\"Personal access tokens手順　Update token\" width=\"1200\" height=\"868\" loading=\"lazy\"></a>\n\n<br />\n\n## シークレット\n\nシークレット（ワークフロー内で使う秘密の値）を設定します。\n\n<br />\n\nリポジトリ `aca-app-repo` に戻って、\n\n**Settings** -> **Secrets** -> **Actions** -> **New repository secret** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image6.png\" alt=\"New repository secret\" width=\"1156\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\nここに、以下を設定します。  \n**AZURE_CREDENTIALS**：先ほどサービス プリンシパルの作成のときの JSON 出力  \n**RESOURCE_GROUP**：my-containerapp-store（Azure リソースグループ名。任意。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image7.png\" alt=\"AZURE_CREDENTIALS、RESOURCE_GROUP\" width=\"1163\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\n# commit & push\n\nリポジトリ `aca-app-repo` main ブランチに push します。\n\n```shellsession\n$ git init\n$ git config --local user.name \"AAAAA BBBBB\"\n$ git config --local user.email \"xxxxx@example.com\"\n$ git add .\n$ git commit -m \"first commit\"\n$ git branch -M main\n$ git remote add origin https://github.com/<github user name>/aca-app-repo.git\n$ git push -u origin main\n```\n\n`<github user name>` 部分は、GitHub ユーザー名です。\n\n<br />\n\n# Run workflow\n\n**Actions** -> **Build and Deploy** -> **Run workflow**  \nをクリックして、`Branch: main` のままにして、**Run workflow** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image8.png\" alt=\"Run workflow\" width=\"1152\" height=\"603\" loading=\"lazy\"></a>\n\n<br />\n\n**`Set Environment Vairables` ジョブ**  \n後続のジョブで使用する値を生成しています。  \n↓  \n**`package-services` ジョブ**  \ndocker コンテナをビルドして、コンテナレジストリ（`ghcr.io`）にビルドしたコンテナを push しています。  \n↓  \n**`deploy`** ジョブ  \nAzure リソースをデプロイしています。  \n（既にデプロイ済みの場合は、Azure Container Apps アプリ`react-app`のリビジョンが一つ増えます。）\n\n<br />\n\nと進んで、全て緑色のチェックが付いたら、デプロイ完了です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image9.png\" alt=\"デプロイ完了\" width=\"1200\" height=\"557\" loading=\"lazy\"></a>\n\n`deploy` ジョブで作成されるリソースは、以下です。  \n・ログ分析ワークスペース  \n・Application Insights  \n・Azure Container Apps の環境  \n・Azure Container Apps のアプリ\n\n<br />\n\nAzure Container Apps アプリ `react-app` のアプリ名は、`./deploy/main.bicep` の  \n`var reactFrontendAppName = 'react-app'`  \nに書かれています。（アプリ名は任意です。）\n\n<br />\n\n# 動作確認１\n\n**Azure Portal**（`https://portal.azure.com/`） -> **コンテナ― アプリ** -> **`react-app`** -> **リビジョン管理**  \nを見ると、リビジョンが一つだけ有ることが分かります。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image10.png\" alt=\"コンテナ― アプリ選択\" width=\"1134\" height=\"323\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image11.png\" alt=\"リビジョン管理　リビジョンが一つだけ有る\" width=\"1156\" height=\"732\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** から **アプリケーション URL** にアクセスすると、アプリが起動しています。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image12.png\" alt=\"概要　アプリケーション URL\" width=\"1143\" height=\"726\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image1.png\" alt=\"アプリ起動確認\" width=\"582\" height=\"403\" loading=\"lazy\"></a>\n\n<br />\n\n# CI/CD\n\n`App.tsx` のメッセージに \"New\" という言葉を足して、commit & push します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image13.png\" alt=\"Newという言葉追加\" width=\"708\" height=\"323\" loading=\"lazy\"></a>\n\n```shellsession\n$ git add .\n$ git commit -m \"second commit\"\n$ git push\n```\n\n<br />\n\n# 動作確認２\n\n**Azure Portal**（`https://portal.azure.com/`） -> **コンテナ― アプリ** -> **`react-app`** -> **リビジョン管理**\nを見ると、リビジョンが二つ有ることが分かります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image14.png\" alt=\"リビジョン管理　リビジョンが二つ有る\" width=\"1160\" height=\"735\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** から **アプリケーション URL** にアクセスすると、アプリが起動しています。\"New\" という言葉が反映された最新アプリということが分かります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image15.png\" alt=\"Newという言葉が追加されている\" width=\"666\" height=\"530\" loading=\"lazy\"></a>\n\n<br />\n\n一連の作成物は、以下のコマンドでリソースグループごと削除することで、まとめて削除できます。\n\n```shellsession\n$ az group delete \\\n  --resource-group my-containerapp-store\n```\n\n<br />\n\n<a class=\"anchor\" id=\"caution\"></a>\n\n# 注意点\n\nシングルリビジョン、スケーリング 1Pod 固定（0Pod になることは無い）の場合、安定しましたが、マルチリビジョンでトラフィックの分割、ブルーグリーンデプロイメントを試そうかと思いましたが、問題が生じました。\n\n<blockquote class=\"warn\">\n<p>2022年11月現在の情報です。ただ勘違いしているだけの可能性もありますので、ご了承ください。</p>\n</blockquote>\n\n<br />\n\n## 初回起動に失敗することがある？\n\n「<a href=\"https://learn.microsoft.com/ja-jp/azure/container-apps/dapr-github-actions?tabs=bash\" target=\"_blank\">チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする</a>」  \nでも同じなのですが、初回起動に失敗し、**リビジョン管理** - **プロビジョニングの状態** が **`Failed`** になることがありました。\n\n<br />\n\n**リビジョン管理**→<strong>[リビジョン名]</strong> をクリックして、  \nリビジョンの詳細からコンソールログを見ると、Pod 起動直後に SIGQUIT されていました。\n\n<br />\n\n<span style=\"color: #e70500;background-color: #ffebe7;\">[notice] 1#1: signal 3 (SIGQUIT) received, shutting down</span>\n\n<br />\n\n`Microsoft.App/containerApps@2022-03-01` のところの `template: {` に Startup プローブを追加して、起動待ち時間を長くしたら、安定して起動するようになりました。（注意：そんな気がしただけかもしれません。）\n\n```bicep:./deploy/container-http.bicep\n    template: {\n      containers: [\n        {\n          image: containerImage\n          name: containerAppName\n・・・\n          probes: [\n            {\n              httpGet: {\n                path: '/'\n                port: 3000\n                scheme: 'HTTP'\n              }\n              initialDelaySeconds: 60\n              periodSeconds: 30\n              timeoutSeconds: 10\n              type: 'Startup'\n            }\n          ]\n        }\n      ]\n      scale: {\n・・・\n      }\n    }\n```\n\n<br />\n\n## Pod 更新時 pull できない？\n\nアクティブを非アクティブにしたり、トラフィックを変更したりすると、以下のエラーになりました。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image16.png\" alt=\"非アクティブにしたらエラー\" width=\"1200\" height=\"345\" loading=\"lazy\"></a>\n\n<span style=\"color: #e70500;background-color: #ffebe7;\">新しいリビジョンをデプロイできませんでした: The following field(s) are either invalid or missing. Invalid value: \"ghcr.io/xxxxx/aca-app-repo/react-frontend:sha-86eefdb\": GET https:?scope=repository%3Axxxxx%2Faca-app-repo%2Freact-frontend%3Apull&service=ghcr.io: DENIED: denied: template.containers.react-app.image.</span>\n\n<br />\n\nPod の再作成時、コンテナレジストリからの pull に失敗しているようでした。\n\n<br />\n\nまた、しばらく何もしないと、Pod が 0 になり、アクセスすると、1 になるはずですが、そこでも pull に失敗し、アクセスできなくなりました。\n\n<br />\n\nGitHub Container Registry（ghcr.io）のコンテナを Private から Public に変更しても同様でしたので、権限の問題ではないように思えました。\n\n<br />\n\nBicep 経由でトラフィックを変更すると、問題無く変更できましたが、pull に失敗する状況は改善しませんでした。\n\n<br />\n\n非アクティブにするのは、コマンドラインで実行すると、成功しましたが、やはり、pull に失敗する状況は改善しませんでした。\n\n```shellsession\n$ az containerapp revision deactivate \\\n  --revision react-app--8l8e708 \\\n  --resource-group my-containerapp-store\n```\n\n<br />\n\n## チュートリアルの通りの Bicep でエラー\n\n<blockquote class=\"alert\">\n<p>2022年11月13日 追記：</p>\n<p><span style=\"color: red;\"><strong>以下のエラーは、Bicep CLI v0.12.1 → Bicep CLI v0.12.40 に更新にて、発生しなくなりました。</strong></span>【修正前】が正しい形になります。</p>\n<p>検証時のタイミングが悪かったようです。</p>\n<p>VSCodeのBicep拡張機能の方は、いつの間にか、v0.12.40になっていて、エラーにならなくなりました。（以前は、コードにエラー表示されていました。）</p>\n<p>GitHub Actionsの方は、現在はエラーにならなくなっていました。</p>\n</blockquote>\n\n<br />\n\n「<a href=\"https://learn.microsoft.com/ja-jp/azure/container-apps/dapr-github-actions?tabs=bash\" target=\"_blank\">チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする</a>」  \nの Bicep でも発生しましたが、そのままの場合、  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Error BCP070: Argument of type \"null | object\" is not assignable to parameter of type \"Ingress | null\".</span>  \nのエラーになって、デプロイに失敗しました。\n\n<br />\n\n今回の場合、`enableIngress` は、常に `true` のため、【修正前】から、【修正後】に修正しました。\n\n<br />\n\n【修正前】\n\n```bicep:./deploy/container-http.bicep\n      ingress: enableIngress ? {\n        external: isExternalIngress\n        targetPort: containerPort\n        transport: 'auto'\n        traffic: [\n          {\n            latestRevision: true\n            weight: 100\n          }\n        ]\n      } : null\n```\n\n<br />\n\n【修正後】\n\n```bicep:./deploy/container-http.bicep\n      ingress: {\n        external: isExternalIngress\n        targetPort: containerPort\n        transport: 'auto'\n        traffic: [\n          {\n            latestRevision: true\n            weight: 100\n          }\n        ]\n      }\n```\n\n<br />\n\n<s>`enableIngress` の `true` `false` により切り替える正解の形は分かりませんでした。</s>\n\n<br />\n","description":"Azure Container Apps へ bicep、GitHub Container Registry、GitHub Actions を使って React Web アプリをデプロイしてみました。一連の手順の紹介です。","reflect_updatedAt":true,"reflect_revisedAt":true,"seo_images":[{"id":"0oj8yh6crapt","createdAt":"2022-11-12T10:50:42.971Z","updatedAt":"2022-11-12T10:50:42.971Z","publishedAt":"2022-11-12T10:50:42.971Z","revisedAt":"2022-11-12T10:50:42.971Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/ITC_Engineering_Blog.png","alt":"Azure Container Appsへbicep,GitHub Container Registry,GitHub Actionsを使ってデプロイ","width":1200,"height":630}],"seo_authors":[]},{"id":"linux-windows-copy","createdAt":"2021-11-05T11:08:29.744Z","updatedAt":"2021-12-29T09:17:05.859Z","publishedAt":"2021-11-05T11:08:29.744Z","revisedAt":"2021-12-29T09:17:05.859Z","title":"Ubuntu-Windows間コピー方法いろいろ 全日本語文字検証","category":{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},"topics":[{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false},{"id":"umqsrvfrv7","createdAt":"2021-08-29T10:56:17.442Z","updatedAt":"2021-08-31T12:02:21.915Z","publishedAt":"2021-08-29T10:56:17.442Z","revisedAt":"2021-08-31T12:02:21.915Z","topics":"Unix/Linux","logo":"/logos/Linux.png","needs_title":true},{"id":"cs5ixa-odo","createdAt":"2021-09-23T13:40:54.567Z","updatedAt":"2021-09-23T13:40:54.567Z","publishedAt":"2021-09-23T13:40:54.567Z","revisedAt":"2021-09-23T13:40:54.567Z","topics":"Windows","logo":"/logos/Windows.png","needs_title":true}],"content":"Ubuntu(に限らず Linux 全般) - Windows を相互にファイルを共有する方法がいろいろあります。今回、いろいろなコピー方法、ファイル名が文字化けしないかの確認と、コピースピードについて検証しました。文字化けについては、Unicode の日本語範囲全て確認しました。\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS(コピー元)</code></p>\n<p>　<code>PHP 8.0.12</code></p>\n<p>　<code>samba 4.11.6-Ubuntu</code></p>\n<p>　<code>rsync 3.1.3</code></p>\n<p><code>Windows 10 Pro x64(操作端末)</code></p>\n<p>　<code>FastCopy v3.92</code></p>\n<p>　<code>cwrsync_6.2.3_x64_free</code></p>\n<p><code>Windows Server 2019 Datacenter Desktop(ファイルサーバー、NAS)</code></p>\n</blockquote>\n\n<br />\n\n# 構成\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu1.png\" alt=\"Ubuntu - Windows 間コピー 構成\" width=\"562\" height=\"101\" loading=\"lazy\"></a>\n\n↑  \nおのおののケースで改めて図示しますが、基本的にこの構成とします。\n\n<br />\n\nLinux をいじれる人、Windows(操作端末)をいじれる人、ファイルサーバー、NAS をいじれる人が別々にいて、最終的に Windows(操作端末)をいじれる人でコピー作業を行う想定です。（最後の mount は例外ケースです。）\n\n<br />\n\n# 日本語文字化け検証\n\nファイル名が文字化けしていたら、共有方法として使えないと思い、検証してみましたが、全ての方法で問題無く転送できました。  \nUbuntu（Linux）側が UTF-8 以外の SJIS 等他の文字コードの場合、いろいろと問題が生じると思われます。\n\n<br />\n\nコピー先が日本語 Windows なので、日本語のファイル名だけテストしました。  \nUnicode のどの範囲が日本語なのか？ふと疑問に思い、調べると、意外と情報が無く、英語のサイトに明確に書かれていました。  \n<a href=\"http://www.localizingjapan.com/blog/2012/01/20/regular-expressions-for-japanese-text/\">http://www.localizingjapan.com/blog/2012/01/20/regular-expressions-for-japanese-text/</a>\n\n<br />\n\nサイトに書かれている通り、以下の範囲の文字を検証対象としました。  \n**ひらがな：** 0x3041-0x3096  \n**全角カタカナ：** 0x30A0-0x30FF  \n**漢字：** 0x3400-0x4DB5、0x4E00-0x9FCB、xF900-0xFA6A  \n**漢字部首：** 0x2E80-0x2FD5  \n**半角カタカナ：** 0xFF5F-0xFF9F  \n**日本語の記号と句読点：** 0x3000-0x303F  \n**その他の日本語の記号と文字：** 0x31F0-0x31FF、0x3220-0x3243、0x3280-0x337F  \n**全角英数字と句読点：** 0xFF01-0xFF5E  \n上記すべての範囲の日本語をファイル名に使用したダミーファイルを出力するプログラムを作成しました。\n\n<br />\n\n`[Unicode]_[文字]というファイルを１個100KBで28469個作成するphpプログラム：`\n\n```php\n<?php\nfor ($i = 0x3041; $i <= 0xff5e; $i++) {\n    if (\n        (0x3041 <= $i && $i <= 0x3096) ||\n        (0x30a0 <= $i && $i <= 0x30ff) ||\n        (0x3400 <= $i && $i <= 0x4db5) ||\n        (0x4e00 <= $i && $i <= 0x9fcb) ||\n        (0xf900 <= $i && $i <= 0xfa6a) ||\n        (0x2e80 <= $i && $i <= 0x2fd5) ||\n        (0xff5f <= $i && $i <= 0xff9f) ||\n        (0x3000 <= $i && $i <= 0x303f) ||\n        (0x31f0 <= $i && $i <= 0x31ff) ||\n        (0x3220 <= $i && $i <= 0x3243) ||\n        (0x3280 <= $i && $i <= 0x337f) ||\n        (0xff01 <= $i && $i <= 0xff5e)\n    ) {\n        $filename = dechex($i) . \"_\" . mb_chr($i, \"UTF-8\");\n        $cmd = \"dd if=/dev/random of=\" . $filename . \" bs=1024 count=\" . 100;//100KB\n        system($cmd);\n    }\n}\n```\n\n<br />\n\n`10文字ずつのUnicodeをファイル名に使用する版（１個1MB×2846個）：`\n\n```php\n<?php\n$count = 0;\n$filename = \"\";\nfor ($i = 0x3041; $i <= 0xff5e; $i++) {\n    if (\n        (0x3041 <= $i && $i <= 0x3096) ||\n        (0x30a0 <= $i && $i <= 0x30ff) ||\n        (0x3400 <= $i && $i <= 0x4db5) ||\n        (0x4e00 <= $i && $i <= 0x9fcb) ||\n        (0xf900 <= $i && $i <= 0xfa6a) ||\n        (0x2e80 <= $i && $i <= 0x2fd5) ||\n        (0xff5f <= $i && $i <= 0xff9f) ||\n        (0x3000 <= $i && $i <= 0x303f) ||\n        (0x31f0 <= $i && $i <= 0x31ff) ||\n        (0x3220 <= $i && $i <= 0x3243) ||\n        (0x3280 <= $i && $i <= 0x337f) ||\n        (0xff01 <= $i && $i <= 0xff5e)\n    ) {\n        $count++;\n        $filename .= mb_chr($i, \"UTF-8\");\n        if ($count == 10) {\n            $count = 0;\n            $cmd = \"dd if=/dev/random of=\" . $filename . \" bs=1024 count=\" . 1024;//1MB\n            system($cmd);\n            $filename = \"\";\n        }\n    }\n}\n```\n\n<br />\n\nファイル数が多すぎると、コピーに時間がかかるため、10 文字ずつの Unicode をファイル名に使用する版で検証することにしました。\n\n<br />\n\nUbuntu で以下のようにダミーデータを作成しました。\n\n<blockquote class=\"warn\">\n<p><code>mb_chrを使っていますので、phpは、7.2.0以上が必要です。</code></p>\n</blockquote>\n\n/home/share/create_all_Japanese_Unicode_DummyFiles2.php\nにプログラムを配置して、システムロケールを LANG=ja_JP.UTF8 と変更し、実行します。\n\n```shellsession\n# apt install language-pack-ja\n# update-locale LANG=ja_JP.UTF8\n# mkdir -p /home/share/dummy\n# cd /home/share/dummy\n# php ../create_all_Japanese_Unicode_DummyFiles2.php\n# ls\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image1.png\" alt=\"VSCode 10文字ずつのUnicodeをファイル名に使用 ls\" width=\"791\" height=\"308\" loading=\"lazy\"></a>\n\n<br />\n\n本題から逸れますが、teraterm の場合、ファイル名を`echo`したり`ls`すると、一部?表示になりました。vscode のターミナルや、Ubuntu のターミナルでは正常に表示されました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image2.png\" alt=\"teraterm 10文字ずつのUnicodeをファイル名に使用 ls\" width=\"727\" height=\"360\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image3.png\" alt=\"ubuntu 10文字ずつのUnicodeをファイル名に使用 ls\" width=\"500\" height=\"318\" loading=\"lazy\"></a>\n\n<br />\n\n# scp\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu2.png\" alt=\"Ubuntu - Windows 間コピー scp\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"101\" oading=\"lazy\"></a>\n\n単発の作業の場合、scp を使うと、Windows 側、Linux 側何も準備しなくてもコピーできます。  \nただし、今回試した方法では一番遅かったです。\n\n```shellsession\n> scp -rp admin@192.168.12.110:/home/admin/dummy //192.168.12.219/fileserver/\n```\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**3 分 48 秒**でした。\n\n<br />\n\n# samba Linux→Windows→NAS\n\nWindows 端末から Linux のファイルを共有できるように samba をインストールします。\n\n```shellsession\n# apt install samba -y\n# mkdir /home/share\n# chmod 777 /home/share\n# vi /etc/samba/smb.conf\n```\n\n```ini\n[Share]\n   # 共有フォルダーを指定\n   path = /home/share\n   # 書き込みを許可する\n   writable = yes\n   # ゲストユーザー (nobody) を許可する\n   guest ok = yes\n   # 全てゲストユーザーとして扱う\n   guest only = yes\n   # ファイル作成時のパーミッションを [777] とする\n   force create mode = 777\n   # フォルダー作成時のパーミッションを [777] とする\n   force directory mode = 777\n```\n\n```shellsession\n# systemctl restart smbd\n```\n\n以降、Windows 端末から Linux 側が `\\\\192.168.12.110\\Share\\` で見られるようになった想定です。\n\n<br />\n\n# エクスプローラー\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu3.png\" alt=\"Ubuntu - Windows 間コピー samba エクスプローラー\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"151\"  loading=\"lazy\"></a>\n\nエクスプローラーでコピーします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image4.png\" alt=\"Ubuntu - Windows 間コピー エクスプローラー\" width=\"829\" height=\"533\" loading=\"lazy\"></a>\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**1 分 52 秒**でした。\n\n<br />\n\n# xcopy コマンド\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu4.png\" alt=\"Ubuntu - Windows 間コピー samba xcopy\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"151\" loading=\"lazy\"></a>\n\n`xcopy`でコピーします。\n\n```batch\n> powershell -C (Measure-Command {xcopy \\\\192.168.12.110\\Share\\dummy \\\\192.168.12.219\\fileserver\\dummy /I}).TotalSeconds\n```\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**1 分 57 秒**でした。\n\n<br />\n\n# robocopy コマンド\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu5.png\" alt=\"Ubuntu - Windows 間コピー samba robocopy\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"151\" loading=\"lazy\"></a>\n\n`robocopy`でコピーします。\n\n```batch\n> powershell -C (Measure-Command {robocopy \\\\192.168.12.110\\Share\\dummy \\\\192.168.12.219\\fileserver\\dummy}).TotalSeconds\n```\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**1 分 38 秒**でした。\n\n<br />\n\n# FastCopy\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu6.png\" alt=\"Ubuntu - Windows 間コピー samba FastCopy\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"151\" loading=\"lazy\"></a>\n\n`FastCopy`という無料の高速ファイル・コピーツールをインストールして、コピーしました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image5.png\" alt=\"Ubuntu - Windows 間コピー FastCopy画面\" width=\"330\" height=\"432\" loading=\"lazy\"></a>\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**1 分 9 秒**でした。  \n※インストールしてすぐ起動して、何もチューニングしていません。\n\n<br />\n\n# rsync\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu7.png\" alt=\"Ubuntu - Windows 間コピー rsync\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"151\" loading=\"lazy\"></a>\n\nsamba を使わない場合、コピーは、Windows 版 rsync で行うという手もあります。\n\n<br />\n\n<a href=\"https://itefix.net/cwrsync\">https://itefix.net/cwrsync</a> の以下の\"Rsync client\" - \"download\"タブのページからダウンロードします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/image6.png\" alt=\"Ubuntu - Windows 間コピー Windows 版 rsync\" width=\"992\" height=\"523\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>今回、`cwrsync_5.5.0_x86_free.zip`を使いました。</strong></span>  \n`cwrsync_6.2.3_x64_free.zip`  \n`cwrsync_6.2.2_x64_free.zip`  \n`cwrsync_6.2.1_x64_free.zip`  \n`cwrsync_6.2.0_x64_free.zip`  \nの場合、コピー先ディレクトリのパーミッションがおかしくなって、\n\n```log\nrsync: [receiver] failed to set times on \"//192.168.12.219/fileserver/dummy/.リルレロヮワヰヱヲン.PnHmYc\": Permission denied (13)\n```\n\nとエラーになりました。\n\n<br />\n\n`cwrsync_5.5.0_x86_free.zip`を展開します。  \n展開すると、`cwrsync.cmd`というファイルが有るのですが、それの一番下の方を以下のようにします。\n\n```batch\nREM ** CUSTOMIZE ** Enter your rsync command(s) here\nrsync -av 192.168.12.110::TEST //192.168.12.219/fileserver/dummy\n```\n\nLinux 側で rsync の設定をして、デーモンモードで起動します。今回使用した Ubuntu の環境には、rsync が最初からインストールされていました。\n\n```shellsession\n# /usr/bin/rsync --version\nrsync  version 3.1.3  protocol version 31\n# vi /etc/rsyncd.conf\n```\n\n```ini\nuid = root\ngid = root\nuse chroot = false\nhosts allow = *\nhosts deny = *\n[TEST]\npath = /home/share/dummy\nread only = true\n```\n\n```shellsession\n# rsync --daemon --config /etc/rsyncd.conf\n```\n\nWindows 側で rsync クライアントを起動します。\n\n```batch\n> cd C:\\Users\\admin.AD\\Documents\\cwrsync_5.5.0_x86_free\n> cwrsync\n```\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**3 分 35 秒**でした。\n\n<br />\n\n# mount Linux→Windows\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/zu8.png\" alt=\"Ubuntu - Windows 間コピー cifs cp\" style=\"margin-bottom: 8px;\" width=\"562\" height=\"162\" loading=\"lazy\"></a>\n\n前提が崩れますが、Linux から直接ファイルサーバー、NAS をマウントできます。  \nまず、ファイルサーバー、NAS をマウントします。\n\n```shellsession\n# apt install cifs-utils\n# mkdir /mnt/windows\n# mount -t cifs -o user=admin,password=passwd //192.168.12.219/fileserver /mnt/windows\n```\n\n/mnt/windows として、見えるので、普通に`cp`コマンドでコピーします。\n\n```shellsession\n# /usr/bin/time cp -rp /home/share/dummy /mnt/windows/dummy\n```\n\n文字化けはしませんでした。  \nタイムスタンプは維持されました。  \nコピー時間は、**1 分 50 秒**でした。\n\n<br />\n\n# まとめ\n\n| コピー方法       | 実行時間   | 文字化け | タイムスタンプ | 差分コピー |\n| ---------------- | ---------- | -------- | -------------- | ---------- |\n| scp              | 3 分 48 秒 | 無し     | 維持           | ❌         |\n| エクスプローラー | 1 分 52 秒 | 無し     | 維持           | ❌         |\n| xcopy            | 1 分 57 秒 | 無し     | 維持           | ❌         |\n| robocopy         | 1 分 38 秒 | 無し     | 維持           | ✔          |\n| FastCopy         | 1 分 9 秒  | 無し     | 維持           | ✔          |\n| rsync            | 3 分 35 秒 | 無し     | 維持           | ✔          |\n| cp               | 1 分 50 秒 | 無し     | 維持           | ❌         |\n\n<br />\n","description":"Ubuntu(に限らず Linux 全般) - Windows を相互にファイルを共有する方法がいろいろあります。いろいろなコピー方法、ファイル名が文字化けしないかの確認と、コピースピードについて検証しました。文字化けについては、Unicode の日本語範囲全て確認しました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"e-l3y_6l9","createdAt":"2021-11-05T11:03:00.878Z","updatedAt":"2021-11-05T11:03:00.878Z","publishedAt":"2021-11-05T11:03:00.878Z","revisedAt":"2021-11-05T11:03:00.878Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/linux-windows-copy/ITC_Engineering_Blog.png","alt":"Ubuntu-Windows間コピー方法いろいろ 全日本語文字検証","width":1200,"height":630}],"seo_authors":[]},{"id":"jetson-nano","createdAt":"2022-08-18T09:48:02.634Z","updatedAt":"2022-08-18T09:48:02.634Z","publishedAt":"2022-08-18T09:48:02.634Z","revisedAt":"2022-08-18T09:48:02.634Z","title":"NVIDIA Jetson Nanoを冷却ファン付きアクリルケースに入れてセットアップ","category":{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},"topics":[{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\n\nNVIDIA の小型コンピューター Jetson Nano 開発キット B01 4GB を購入し、冷却ファン付きアクリルケースに入れて、セットアップしました。今回、アクリルケースに組付け、OS 起動までの手順を紹介していきたいと思います。\n\n<blockquote class=\"alert\">\n<p>Jetson Nanoとは とかの情報は割愛します。また、この記事は、OSをインストールまでで、Jetson Nano の活用情報は有りません。</p>\n</blockquote>\n\n<br />\n\n# アクリルケースに組付け\n\n<blockquote class=\"warn\">\n<p>アクリルケースにマニュアルは入っていませんでした。</p>\n</blockquote>\n\nまず、アクリルケースは保護テープがきっちり貼られていますので、剥がします。（非常に剥がしにくく、剥がすのに時間がかかりました。）\n\n<br />\n\nアクリルケース底面と Jetson 本体との位置関係は、以下のようになります。このまま上に載せる形になります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image1.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image1.jpg\" alt=\"アクリルケース底面とJetson本体との位置関係\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n<br />\n\nちょうどアクリルケースの穴と Jetson の穴の位置が同じになり、スペーサーを４つ差し込んで取り付けます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image2.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image2.jpg\" alt=\"スペーサーを４つ差し込んで取り付け１\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image3.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image3.jpg\" alt=\"スペーサーを４つ差し込んで取り付け２\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n<br />\n\n小さいナットをくるくると回し入れると、以下のようになります。上から、ナット、スペーサー、小さいネジ という位置関係です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image4.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image4.jpg\" alt=\"ナット、スペーサー、小さいネジ\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n<br />\n\n冷却ファンを放熱フィンに取り付けて、冷却ファンの電源を差し込みます。5V ピン（赤）と GND ピン（黒）です。\n\n<blockquote class=\"warn\">\n<p>冷却ファンを黒いネジで取り付けるのですが、ぎりぎりに届く長さで、放熱フィンに押し付けながら、木ネジを締め付けるイメージで、けっこう力が要りました。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>写真がどこに刺さっているか分かりにくくなってしまいましたが、図が正解です。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image5.png\" alt=\"冷却ファンの電源　ピン　図\" width=\"521\" height=\"521\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image6.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image6.jpg\" alt=\"冷却ファンの電源　ピン\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n<br />\n\n側面、上面のパネルをはめます。各位置関係は、**端子のある方を前とすると、** 写真のとおりです。\n\n<br />\n\n前：  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image7.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image7.jpg\" alt=\"アクリルパネル　前\" width=\"626\" height=\"388\" loading=\"lazy\"></a>\n\n<br />\n\n右側側面：  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image8.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image8.jpg\" alt=\"アクリルパネル　右側側面\" width=\"640\" height=\"487\" loading=\"lazy\"></a>\n\n<br />\n\n後ろ：  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image9.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image9.jpg\" alt=\"アクリルパネル　後ろ\" width=\"640\" height=\"402\" loading=\"lazy\"></a>\n\n<br />\n\n左側側面：  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image10.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image10.jpg\" alt=\"アクリルパネル　左側側面\" width=\"640\" height=\"483\" loading=\"lazy\"></a>\n\n<br />\n\n４本の大きいネジを下から挿して、ナットで止めれば完成です。\n\n<blockquote class=\"info\">\n<p>もしかしたら、上下逆かもしれませんが、下をネジ山にした方が机にやさしいので、この方向にしました。</p>\n<p>あと、この場合、ナットを外して、上をパカっと開けられます。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image11.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image11.jpg\" alt=\"上下１\" width=\"521\" height=\"521\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image12.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image12.jpg\" alt=\"上下２\" width=\"521\" height=\"521\" loading=\"lazy\"></a>\n\n<br />\n\n# ＯＳイメージ書き込み\n\n<blockquote class=\"info\">\n<p>今回、microSDカードは、SanDisk Extreme 64GB A2 U3を使って作業しています。</p>\n</blockquote>\n\n<br />\n\n<a href=\"https://developer.nvidia.com/embedded/downloads\" target=\"_blank\">`https://developer.nvidia.com/embedded/downloads`</a>  \nから  \n`Jetson Nano Developer Kit SD Card Image` をダウンロードします。\n\n<blockquote class=\"info\">\n<p>2GB版Jetson nanoの場合は、<code>Jetson Nano 2GB Developer Kit SD Card Image</code> の方と思われます。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image13.png\" alt=\"Jetson Nano Developer Kit SD Card Image ダウンロード\" width=\"1366\" height=\"1069\" loading=\"lazy\"></a>\n\n<br />\n\nダウンロードしたファイルは、今回の場合、\n`jetson-nano-jp461-sd-card-image.zip`\nというファイル名でした。\nOS の種類としては、`ubuntu 18.04 LTS` でした。\n\n<br />\n\nこれを Etcher というアプリを使って、書き込みます。\n\n<blockquote class=\"warn\">\n<p>作業ＰＣをWindows10 として、Etcherをインストールしてから使う場合の手順です。</p>\n</blockquote>\n\n<a href=\"https://www.balena.io/etcher/\" target=\"_blank\">`https://www.balena.io/etcher/`</a>  \nから Etcher インストーラーをダウンロードしたら、ダブルクリックで Etcher をインストールします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image14.png\" alt=\"Etcherインストーラーをダウンロード\" width=\"1338\" height=\"991\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>ダウンロードリンクは、スクロールして、少し下の方にあります。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image15.png\" alt=\"ダブルクリックでEtcherをインストール\" width=\"91\" height=\"113\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>microSD カードを作業ＰＣに認識させて</strong></span>、Etcher を起動します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image16.png\" alt=\"Etcherを起動\" width=\"793\" height=\"503\" loading=\"lazy\"></a>\n\n<br />\n\n`Flash from file` で `jetson-nano-jp461-sd-card-image.zip` を選択\n\n↓\n\n`Select target` で、microSD カードドライブを選択\n\n↓\n\n`Flash!`  \nをクリックして、書き込みます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image17.png\" alt=\"Flashをクリック\" width=\"793\" height=\"503\" loading=\"lazy\"></a>\n\n<br />\n\n# Jetson 起動\n\n書き込み終わったら、microSD カードを Jetson 本体に差し込みます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image19.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image19.jpg\" alt=\"microSDカードをJetson本体に差し込み\" width=\"481\" height=\"351\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><span style=\"color: red;\"><strong>裏（金色の端子部が見える方）が上向き</strong></span>です。</p>\n</blockquote>\n\n<br />\n\nHDMI ケーブル、USB 　マウス、キーボードを差し込んで、Micro USB ケーブル（5V2A 給電）を差し込みます。  \n<span style=\"color: red;\"><strong>Jetson には、電源ボタンが無いため、給電直後に起動してきます。</strong></span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image18.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image18.jpg\" alt=\"HDMIケーブル、USB　マウス、キーボードを差し込んで、Micro USB ケーブル（5V2A給電）を差し込み\" width=\"640\" height=\"563\" loading=\"lazy\"></a>\n\n<br />\n\n給電直後、NVIDIA ロゴが表示されます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image20.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image20.jpg\" alt=\"Nvida ロゴ\" width=\"581\" height=\"329\" loading=\"lazy\"></a>\n\n<br />\n\nロゴの後、黒い画面にメッセージが表示されますが、一度、以下のようなメッセージ表示から先に進まないことがありました。  \nその時は、<span style=\"color: red;\">USB マウスとキーボードを抜いて通電後に挿し直す</span>処置が必要でした。\n\n```sh\n[    **] (2 of 2) A start job is running for End-user configuration after initial OEM installation (Debconf UI) (21s / no limit)\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image21.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image21.jpg\" alt=\"メッセージ表示\" width=\"607\" height=\"221\" loading=\"lazy\"></a>\n\n<br />\n\n# 初期設定\n\n`I accept the terms of these licenses` にチェックを入れて、`Continue` をクリックします。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image22.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image22.jpg\" alt=\"I accept the terms of these licenses\" width=\"1180\" height=\"651\" loading=\"lazy\"></a>\n\n（以降、画像は、真ん中のウィンドウ部分だけの切り抜き表示です。）\n\n<br />\n\n`日本語` を選択して、`続ける` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image23.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image23.jpg\" alt=\"日本語を選択１\" width=\"518\" height=\"381\" loading=\"lazy\"></a>\n\n<br />\n\n`日本語` を選択して、`続ける` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image24.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image24.jpg\" alt=\"日本語を選択２\" width=\"497\" height=\"378\" loading=\"lazy\"></a>\n\n<br />\n\n`Tokyo` を選択して、`続ける` をクリックします。（日本国内の都市を細かく選択できますが、おそらく、時刻設定のため、Tokyo で良いと思います。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image25.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image25.jpg\" alt=\"Tokyoを選択\" width=\"493\" height=\"364\" loading=\"lazy\"></a>\n\n<br />\n\n名前、パスワードを入力して、`続ける` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image26.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image26.jpg\" alt=\"名前、パスワードを入力\" width=\"501\" height=\"371\" loading=\"lazy\"></a>\n\n<br />\n\n利用するパーティションの容量を入力します。microSD カードをフル利用する値が自動入力されていますので、そのままにして、続けるをクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image27.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image27.jpg\" alt=\"利用するパーティションの容量を入力\" width=\"487\" height=\"354\" loading=\"lazy\"></a>\n\n<br />\n\n`続ける` をクリックします。（以前 OS を入れていたら、それは二度と起動できなくなるということです。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image28.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image28.jpg\" alt=\"続ける\" width=\"500\" height=\"364\" loading=\"lazy\"></a>\n\n<br />\n\nSelect Nvpmodel Mode は、`MAXN - (Default)` を選択して、`続ける` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image29.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image29.jpg\" alt=\"Select Nvpmodel Mode\" width=\"494\" height=\"361\" loading=\"lazy\"></a>\n\n<br />\n\nMAXN: 使用 CPU4 個  \n5W: 使用 CPU2 個（最大 5W までの低消費電力）  \nの違いがあるようです。  \nこの後、AC アダプタ給電に変更するため、`MAXN` です。\n\n<blockquote class=\"info\">\n<p>MAXN、5W は、後で変更できます。</p>\n</blockquote>\n\n<br />\n\n処理が終わるまで待ちます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image30.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image30.jpg\" alt=\"処理中\" width=\"608\" height=\"175\" loading=\"lazy\"></a>\n\n<br />\n\nログイン画面になるので、ログインします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image31.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image31.jpg\" alt=\"ログイン画面１\" width=\"598\" height=\"336\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image32.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image32.jpg\" alt=\"ログイン画面２\" width=\"595\" height=\"337\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image33.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image33.jpg\" alt=\"ログイン完了\" width=\"599\" height=\"333\" loading=\"lazy\"></a>\n\n<br />\n\n# AC アダプタ給電\n\nMAXN: 使用 CPU4 個 （最大 10W 5V 1A ～ 2A）のモードで動かすため、5V 4A のアダプタを別に購入して、電源供給することにしました。（この場合、Micro USB の給電の方は不要です。）\n\n<blockquote class=\"info\">\n<p><code>5V 4A Jetson</code> でググって、ノーブランドのものを購入しましたが、特に問題有りませんでした。</p>\n</blockquote>\n\nしかし、購入したぞと喜び勇んで、<span style=\"color: red;\"><strong>DC ジャックに差し込んでも初期状態では反応しませんでした。</strong></span>\n\n<br />\n\nジャンパーピンを設定する必要があります。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image34.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image34.jpg\" alt=\"ジャンパーピン\" width=\"638\" height=\"561\" loading=\"lazy\"></a>\n\n<br />\n\n最初、そういう情報を見て、確認して、既に付いていると思いましたが、<span style=\"color: red;\"><strong>片側しかかかっていません。</strong></span>この状態では、付いていないに等しく、Micro USB の給電になります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image35.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image35.jpg\" alt=\"片側しかかかっていません\" width=\"521\" height=\"521\" loading=\"lazy\"></a>\n\n<br />\n\n一旦外して、両方のピンが刺さった状態にします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image36.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image36.jpg\" alt=\"ジャンパーピンを外す\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n↓\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image37.jpg\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/image37.jpg\" alt=\"両方のピンが刺さった状態にする\" width=\"640\" height=\"640\" loading=\"lazy\"></a>\n\n<br />\n\nこの状態で、DC ジャックに差し込むと、AC アダプタから給電されて、電源が入ります。\n\n<br />\n\n# Wifi について\n\nJetson Nano 開発キット B01 4GB は、Wifi 機能が有りません。（有線 LAN は有ります。）\n\n<br />\n\n`2.4G / 5G 8265 AC ネットワーク カード` なるもので、組付けても良いですが、面倒で、こだわりが無いため、USB ドングルを購入しました。\n\n<br />\n\nネットで数名の方が異口同音にお勧めしているのを見て `TP-Link TL-WN725N 150Mbps ナノUSB Wi-Fi子機` を買ったのですが、起動しているときに差し込むだけで認識しました。\n","description":"Jetson Nano 開発キット B01 4GB を購入し、冷却ファン付きアクリルケースに入れて、セットアップしました。アクリルケースに組付け、OS 起動までの手順を紹介します。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"k1pkashr8n","createdAt":"2022-08-18T09:47:42.546Z","updatedAt":"2022-08-18T09:47:42.546Z","publishedAt":"2022-08-18T09:47:42.546Z","revisedAt":"2022-08-18T09:47:42.546Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/jetson-nano/ITC_Engineering_Blog.png","alt":"NVIDIA Jetson Nanoを冷却ファン付きアクリルケースに入れてセットアップ","width":1200,"height":630}],"seo_authors":[]},{"id":"kotless-localstack-s3","createdAt":"2022-04-29T11:52:24.729Z","updatedAt":"2022-06-06T11:09:39.732Z","publishedAt":"2022-04-29T11:52:24.729Z","revisedAt":"2022-06-06T11:09:39.732Z","title":"KotlessとLocalStackで疑似サーバーレス - AWS S3ダウンロード","category":{"id":"v7qhii097q","createdAt":"2022-04-29T11:49:40.704Z","updatedAt":"2022-04-29T11:49:40.704Z","publishedAt":"2022-04-29T11:49:40.704Z","revisedAt":"2022-04-29T11:49:40.704Z","topics":"AWS","logo":"/logos/AWS.png","needs_title":false},"topics":[{"id":"v7qhii097q","createdAt":"2022-04-29T11:49:40.704Z","updatedAt":"2022-04-29T11:49:40.704Z","publishedAt":"2022-04-29T11:49:40.704Z","revisedAt":"2022-04-29T11:49:40.704Z","topics":"AWS","logo":"/logos/AWS.png","needs_title":false},{"id":"6jp855sldd","createdAt":"2022-04-29T11:49:21.447Z","updatedAt":"2022-04-29T11:49:21.447Z","publishedAt":"2022-04-29T11:49:21.447Z","revisedAt":"2022-04-29T11:49:21.447Z","topics":"Kotlin","logo":"/logos/Kotlin.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\n\nKotlin のサーバーレスフレームワーク、Kotless を使って実装したアプリケーションを LocalStack へデプロイ →LocalStack の AWS S3 互換機能のオブジェクトをダウンロード\nとやってみました。\n\n<br />\n\n基本的に、参考サイトと<a href=\"https://github.com/JetBrains/kotless\" target=\"_blank\">GitHub の README</a>を見て、やっていきましたが、本物の AWS へデプロイするのではなく、LocalStack へデプロイして、LocalStack の AWS S3 オブジェクトをダウンロードになります。\n\n<blockquote class=\"info\">\n<p>【 参考サイト 】</p>\n<a href=\"https://blog.takehata-engineer.com/entry/deploy-kotlin-applications-to-aws-lambda-using-kotless\" target=\"_blank\">https://blog.takehata-engineer.com/entry/deploy-kotlin-applications-to-aws-lambda-using-kotless</a>\n<a href=\"https://qiita.com/hisayuki/items/b5381409378277320e48\" target=\"_blank\">https://qiita.com/hisayuki/items/b5381409378277320e48</p>\n</blockquote>\n\n<br />\n\n図に示すと、以下のような感じです。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/kotless-s3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/kotless-s3.png\" alt=\"Kotlessを使って実装したアプリケーションをLocalStackへデプロイ→LocalStackのAWS S3互換機能のオブジェクトをダウンロード　の図\" width=\"801\" height=\"733\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>図中の右向き矢印を本物のAWSに向けると、本物のAWSでも動作すると思いますが、<span style=\"color: red;\">本物では、デプロイ、動作確認していません。</span></p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 LocalStack 】</p>\n<p>オンプレ上のAWSのようなものです。疑似的なAWSを構築して、本物のAWSへ接続しなくてもAWSサービスを利用したプログラムの動作確認ができます。AWSサービスの一部、例えば、Cognitoなどは、有料になります。</p>\n</blockquote>\n\n<br />\n\n今回、全て１台の Ubuntu 20.04 LTS で作業しています。\n\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p>VMware Workstation Pro 16</p>\n<p>　Ubuntu 20.04.2 LTS</p>\n<p>　　Docker 20.10.14</p>\n<p>　　openjdk 17.0.3</p>\n<p>　　Gradle 7.3.3</p>\n<p>　　Kotlin 1.5.31</p>\n<p>　　LocalStack 0.11.2</p>\n</blockquote>\n\n<br />\n\n# Docker インストール\n\nLocalStack は、`docker pull` を利用しますので、Docker をインストールします。\n\n```shellsession\n# apt update\n# apt install -y apt-transport-https ca-certificates curl software-properties-common\n# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n# add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\"\n# apt update\n# apt install -y docker-ce\n```\n\n<br />\n\n# samba インストール\n\nUbuntu のファイルを Windows から見えるようにします。（<span style=\"color: red;\">必須ではありません。</span>なんとなく、Windows の共有フォルダからソースコードを見たかったからで、ほとんど意味無いです。）\n\n```shellsession\n# apt install samba\n# mkdir /home/share\n# chmod 777 /home/share\n# vi /etc/samba/smb.conf\n[share]\npath = /home/share/\nbrowsable = yes\nwritable = yes\nguest ok = yes\nread only = no\n```\n\nこの時点で、Ubuntu の IP アドレスを 192.168.11.6 とすると、\\\\192.168.11.6\\share にて、/home/share が見えます。\n\n<br />\n\n# リモートデスクトップ\n\nUbuntu 20.04 LTS を VMware で起動していて、使い勝手が悪かったため、リモートデスクトップ接続できるようにしました。（<span style=\"color: red;\">必須ではありません。</span>）\n\n```shellsession\n# apt install xrdp\n# systemctl start xrdp\n# systemctl enable xrdp\n```\n\n`Authentication is required to create a color managed device` と認証を聞かれるのを回避するため、設定を追加します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image1.png\" alt=\"Authentication is required to create a color managed device\" width=\"788\" height=\"504\" loading=\"lazy\"></a>\n\n```shellsession\n# vi /etc/polkit-1/localauthority/50-local.d/45-allow-colord.pkla\n[Allow Colord all Users]\nIdentity=unix-user:*\nAction=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile\nResultAny=no\nResultInactive=no\nResultActive=yes\n```\n\nこれにて、Ubuntu 20.04 LTS へ Windows からリモートデスクトップ接続ができます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image2.png\" alt=\"Ubuntu 20.04 LTSへWindowsからリモートデスクトップ接続\" width=\"975\" height=\"525\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>リモートデスクトップ接続しようとしているユーザーが既にデスクトップにログインしている場合、ログアウトが必要です。</span></p>\n</blockquote>\n\n<br />\n\n# JDK インストール\n\nopenjdk-17-jdk をインストールします。この後、Gradle が関係してきますが、今回使用する `Gradle7.3.3` の適用範囲が version 8 ～ 17 だからです。\n\n```shellsession\n# apt install openjdk-17-jdk\n```\n\n<br />\n\n# IntelliJ Linux\n\nIntelliJ IDEA Community 　 Linux を下記サイトからダウンロードします。  \n<a href=\"https://www.jetbrains.com/ja-jp/idea/download/#section=linux\" target=\"_blank\">https://www.jetbrains.com/ja-jp/idea/download/#section=linux</a>\n\n<blockquote class=\"info\">\n<p>【 IntelliJ IDEA 】</p>\n<p>Kotlinに対応した統合開発環境です。今回、プロジェクトのひな形作成、Kotlessプログラム作成に使います。Community版は無料、Ultimate版は有料です。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image3.png\" alt=\"IntelliJ IDEA Community　Linux をダウンロード\" width=\"1372\" height=\"788\" loading=\"lazy\"></a>\n\nスタンドアロンインストールを行います。今回、ダウンロードしたのは、`ideaIC-2022.1.tar.gz` です。\n\n```shellsession\n# tar -xzf ideaIC-2022.1.tar.gz -C /opt\n# cd /opt/idea-IC-221.5080.210/bin\n# ./idea.sh\n```\n\n<blockquote class=\"info\">\n<p>スタンドアロンの他に、インストールするアプリを選択できるToolbox App（<code>jetbrains-toolbox-1.23.11849.tar.gz</code>）というのが有るのですが、なぜか、起動しようとしても反応しませんでした。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;\"><strong>デスクトップ（リモートデスクトップではない。）にログインして、Terminalアプリで行う必要があるようです。</strong></span></p>\n<p><span style=\"color: red;\">teratermの場合、以下のエラーになりました。</span></p>\n<p><span style=\"background-color: cornsilk; color: red;\">Startup Error</span></p>\n<p><span style=\"background-color: cornsilk; color: red;\">Unable to detect graphics environment</span></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;\">リモートデスクトップ接続して、Terminalアプリで起動すると、以下のエラーになりました。</span></p>\n<p><span style=\"background-color: cornsilk; color: red;\">java.awt.AWTError: Can't connect to X11 window server using ':10.0' as the value of the DISPLAY variable.</span></p>\n<p>なお、インストールはできませんが、インストール後は、リモートデスクトップで使えました。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>Windowsから、\\\\192.168.11.6\\share を参照すればWindows版IntelliJで開発可能かと思ったのですが、プロジェクト作成後、</p>\n<p><span style=\"background-color: cornsilk; color: red;\">intellij external file changes sync may be slow</span></p>\n<p><span style=\"background-color: cornsilk; color: red;\">IntelliJ IDEA cannot receive filesystem event notifications for the project. Is it on a network drive?</span></p>\n<p>とエラーになりました。</p>\n</blockquote>\n\n「IntelliJ IDEA User Agreement」画面がポップアップしてきますので、先に進めます。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image4.png\" alt=\"「IntelliJ IDEA User Agreement」画面がポップアップ\" width=\"597\" height=\"459\" loading=\"lazy\"></a>\n\n<br />\n\n「Create Desktop Entry...」をクリックして、デスクトップにインストールします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image5.png\" alt=\"「Create Desktop Entry...」をクリックして、デスクトップにインストール\" width=\"792\" height=\"788\" loading=\"lazy\"></a>\n\n<br />\n\n一旦閉じて、起動し、\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image6.png\" alt=\"一旦閉じて、起動\" width=\"1433\" height=\"894\" loading=\"lazy\"></a>\n\n<br />\n\n「New Project」をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image7.png\" alt=\"「New Project」をクリック\" width=\"792\" height=\"649\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image8.png\" alt=\"「New Project」をクリック２\" width=\"798\" height=\"614\" loading=\"lazy\"></a>\n\nName: のプロジェクト名は任意ですが、ここでは、  \n`kotless-localstack-s3`  \nとします。\n\n<br />\n\nLocation: は、どこでも良いですが、今回は、先ほど、samba で Windows から見えるようにした場所にします。\n\n<br />\n\nJDK は、先ほどインストールした 17 が自動的に選択されています。\n\n<br />\n\nLanguage: `Kotlin`  \nBuild system: `Gradle`  \nGradle DSL: `Kotlin`  \nGroupId: `org.example`（任意）  \nArtifactId: `kotless-localstack-s3`（任意）  \nとし、Create をクリックします。\n\n<br />\n\n# プログラム作成\n\n`build.gradle.kts` を開くと、最初以下のようになっていますので、<a href=\"https://github.com/JetBrains/kotless\" target=\"_blank\">https://github.com/JetBrains/kotless</a> の README に従って書き換えます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image9.png\" alt=\"build.gradle.ktsを開く\" width=\"968\" height=\"711\" loading=\"lazy\"></a>\n\n<br />\n\nまず、`settings.gradle.kts` を編集して、プラグインの場所を Gradle に指示する必要があります。\n\n```kotlin:settings.gradle.kts\nrootProject.name = \"kotless-localstack-s3\"\n\npluginManagement {\n    resolutionStrategy {\n        this.eachPlugin {\n            if (requested.id.id == \"io.kotless\") {\n                useModule(\"io.kotless:gradle:${this.requested.version}\")\n            }\n        }\n    }\n\n    repositories {\n        maven(url = uri(\"https://packages.jetbrains.team/maven/p/ktls/maven\"))\n        gradlePluginPortal()\n        mavenCentral()\n    }\n}\n```\n\n<br />\n\n`build.gradle.kts` を以下のように書き換えます。\n\n```kotlin:build.gradle.kts\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\nimport io.kotless.plugin.gradle.dsl.kotless\n\nplugins {\n    //kotlin(\"jvm\") version \"1.6.20\"\n    kotlin(\"jvm\") version \"1.5.31\" apply true\n    id(\"io.kotless\") version \"0.2.0\" apply true\n}\n\ngroup = \"org.example.kotless-localstack-s3\"\nversion = \"1.0-SNAPSHOT\"\n\nrepositories {\n    mavenCentral()\n    maven(url = uri(\"https://packages.jetbrains.team/maven/p/ktls/maven\"))\n}\n\nval awsSdkKotlinVersion: String by project\n\ndependencies {\n    testImplementation(kotlin(\"test\"))\n    implementation(\"io.kotless\", \"kotless-lang\", \"0.2.0\")\n    implementation(\"io.kotless\", \"kotless-lang-aws\", \"0.2.0\")\n    implementation(\"aws.sdk.kotlin:s3:$awsSdkKotlinVersion\")\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\n\ntasks.withType<KotlinCompile> {\n    kotlinOptions.jvmTarget = \"1.8\"\n}\n\nkotless {\n    config {\n        aws {\n            storage {\n                bucket = \"kotless.s3.example.com\"\n            }\n\n            profile = \"example\"\n            region = \"eu-west-1\"\n        }\n    }\n    extensions {\n        local {\n            port = 8080\n            //enables AWS emulation (disabled by default)\n            useAWSEmulation = true\n        }\n    }\n}\n```\n\n<span style=\"color: red;\">**`kotlin(\"jvm\") version \"1.5.31\" apply true`**</span>  \n<span style=\"color: red;\">にしないと、以下の `NoSuchMethodError` エラーになりました。</span>  \n<span style=\"background-color: cornsilk; color: red;\">`Caused by: java.lang.NoSuchMethodError: 'org.jetbrains.kotlin.analyzer.AnalysisResult org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(org.jetbrains.kotlin.com.intellij.openapi.project.Project, java.util.Collection, org.jetbrains.kotlin.resolve.BindingTrace, org.jetbrains.kotlin.config.CompilerConfiguration, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope, java.util.List, java.util.List, java.util.List, int, java.lang.Object)'`</span>\n\n<br />\n\n**`kotless {`**  \n**` config {`**  \n**` aws {`**  \n本物の AWS へのデプロイ用設定ですが、これを書かないと、`Task :download_terraform`のところで、エラーになりましたので、適当に設定しています。\n\n<br />\n\n**`port = 8080`**  \nkotless が待ち受けるポートです。この説明のためにあえて書きましたが、何も書かない場合も 8080 です。\n\n<blockquote class=\"info\">\n<p>【 Terraform 】</p>\n<p>構成ファイルでは、手動で操作することなくインフラ構成を自動で管理できます。今回、gradlewによって、自動でTerraformが起動します。</p>\n</blockquote>\n\n<br />\n\n**`$awsSdkKotlinVersion`**  \n直接値を書いても良いですが、`gradle.properties` に以下のように設定します。\n\n```kotlin:gradle.properties\nkotlin.code.style=official\nawsSdkKotlinVersion=0.+\n```\n\n<br />\n\n**`useAWSEmulation = true`**  \nLocalStack を使うときに `true` をセットします。（デフォルトは、`false`です。）\n\n<br />\n\nIntelliJ の左側ペインから、  \nNew -> Package  \n`org.example.kotless_localstack_s3`  \n↓  \nNew -> Kotlin Class/File -> File  \n`/home/share/kotless-s3-example/src/main/kotlin/org/example/kotless_localstack_s3/Main.kt`  \nを作成し、以下の内容とします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image10.png\" alt=\"org/example/kotless_localstack_s3作成\" width=\"1068\" height=\"319\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/image11.png\" alt=\"Main.kt作成\" width=\"1079\" height=\"319\" loading=\"lazy\"></a>\n\n<br />\n\n```kotlin:src/main/kotlin/org/example/kotless_localstack_s3/Main.kt\npackage org.example.kotless_localstack_s3\n\nimport aws.sdk.kotlin.runtime.endpoint.AwsEndpoint\nimport aws.sdk.kotlin.runtime.endpoint.AwsEndpointResolver\nimport aws.sdk.kotlin.services.s3.S3Client\nimport aws.sdk.kotlin.services.s3.model.GetObjectRequest\nimport aws.smithy.kotlin.runtime.content.writeToFile\nimport io.kotless.dsl.lang.http.Get\nimport kotlinx.coroutines.runBlocking\nimport java.io.File\nimport org.slf4j.LoggerFactory\n\nprivate val logger = LoggerFactory.getLogger(\"kotless-localstack-s3\")\n\n@Get(\"/\")\nfun main() = \"Hello world!\"\n\nconst val downloadDirPath = \"/tmp/media-down\"\n\n@Get(\"/download\")\nfun download(bucketName: String = \"\"): String = getObjects(bucketName)\n\nfun getObjects(bucketName: String): String {\n    class LocalHostS3: AwsEndpointResolver {\n        override suspend fun resolve(service: String, region: String): AwsEndpoint\n                = AwsEndpoint(System.getenv(\"AWS_ENDPOINT\") ?: \"http://172.17.0.3:4566\")\n    }\n\n    runBlocking {\n        val client = S3Client {\n            region = \"us-east-1\"\n            endpointResolver = LocalHostS3()\n        }\n        client.use { client ->\n            client.listObjects { bucket = bucketName }.contents?.forEach { obj ->\n                client.getObject(GetObjectRequest { key = obj.key; bucket = bucketName }) { response ->\n                    val outputFile = File(downloadDirPath, obj.key!!)\n                    response.body?.writeToFile(outputFile).also { size ->\n                        logger.info(\"Downloaded $outputFile ($size bytes) from S3\")\n                    }\n                }\n            }\n        }\n    }\n\n    return \"OK\"\n}\n```\n\n動作内容は、  \n`# curl http://localhost:8080/download?bucketName=sample-bucket`  \nのようにバケット名を指定して起動されたら、該当の S3 バケットにあるファイルを `/tmp/media-down` へダウンロードします。\n\n<br />\n\n**`import aws.sdk.kotlin.services.s3.S3Client`**  \n`aws.sdk.kotlin` の Kotlin 用 AWS SDK を使っています。当然、本物の AWS 用ですが、LocalStack にも通用します。\n\n<br />\n\n**`@Get(\"/\")`**  \nKotless DSL（Kotless 独自の作法）の書き方です。他にも、Ktor、Spring Boot の書き方が使えるようですが、ここでは、触れません。\n\n<br />\n\n**`= AwsEndpoint(\"http://172.17.0.3:4566\")`**  \n<span style=\"color: red\">エンドポイント（AWS S3 の URL）を LocalStack のエンドポイントに差し替えています。</span>これを行わないと、本物の方へ行ってしまいます。\n\n<br />\n\n**`runBlocking`**  \n`listObjects`が `suspend fun listObjects`（非同期関数）なのですが、`fun download` を `suspend fun download` とするとおかしくなるため、`runBlocking` により、同期処理に変更しています。\n\n<br />\n\n`gradlew` のパーミッションを調整します。\n\n```shellsession\n$ cd /home/share/kotless-localstack-s3\n$ chmod 755 gradlew\n```\n\n<blockquote class=\"info\">\n<p>【 gradlew 】</p>\n<p>シェルスクリプトで、いろいろ準備して、gradleをインストールして、実行してくれます。</p>\n</blockquote>\n\n<br />\n\nビルドできるか確認します。\n\n```shellsession\n$ ./gradlew plan\n```\n\n`Initializing the backend...`  \nのところで、  \n<span style=\"background-color: cornsilk; color: red;\">`Error: error configuring S3 Backend: no valid credential sources for S3 Backend found.`</span>  \nエラーになって止まりますが、これは、本物の AWS S3 の credential が準備されていないからで、今回、LocalStack のため、ここまで来たら、ＯＫとします。\n\n<br />\n\n本物の AWS へデプロイするときは、\n\n```shellsession\n$ ./gradlew deploy\n```\n\nですが、今回は、行いません。\n\n<br />\n\nLocalStack にデプロイする専用の `local` オプションが有りますので、それを使って、LocalStack にデプロイします。\n\n<blockquote class=\"info\">\n<p>内部でdocker pullをしていて、docker利用権限が無いと、エラーになるため、rootで実行しました。</p>\n</blockquote>\n\n```shellsession\n# ./gradlew local\n・・・\n23:07:26.651 [main] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'\n23:07:26.651 [main] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2\n23:07:26.655 [main] INFO  org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.\n<===========--> 90% EXECUTING [6m 27s]\n> :local\n```\n\n`90% EXECUTING` まで来ると、起動しています。\n\n<br />\n\n動作確認します。\n\n```shellsession\n$ curl http://localhost:8080\nHello world!\n```\n\nヨシ！\n\n<br />\n\n# AWS CLI インストール\n\nS3 バケットにあらかじめサンプルファイルを入れるために、aws cli コマンドをインストールします。\n\n```shellsession\n# apt install -y awscli\n# aws --version\naws-cli/1.18.69 Python/3.8.10 Linux/5.13.0-39-generic botocore/1.16.19\n```\n\n<br />\n\n# S3 バケットダウンロード\n\n`curl http://localhost:8080/download?bucketName=sample-bucket`  \nで `fun download(bucketName: String = \"\"): String = getObjects(bucketName)` を起動して、LocalStack S3 のバケットをダウンロードします。\n\n<br />\n\nLocalStack S3 の  \nAWS_ACCESS_KEY_ID  \nAWS_SECRET_ACCESS_KEY  \nをあらかじめ設定しておく必要がありますので、<span style=\"color: red;\">環境変数をセットしてから、起動</span>します。\n\n```shellsession\n# export AWS_ACCESS_KEY_ID=dummy-access-key-id\n# export AWS_SECRET_ACCESS_KEY=dummy-secret-access-key\n# ./gradlew local\n```\n\n<br />\n\nAWS プロファイルを作成します。（`s3dummy` のところは、任意です。）\n\n```shellsession\n# aws configure --endpoint-url=http://172.17.0.3:4566 --profile s3dummy\nAWS Access Key ID [None]: dummy-access-key-id\nAWS Secret Access Key [None]: dummy-secret-access-key\nDefault region name [None]: us-east-1\nDefault output format [None]: json\n```\n\n<span style=\"color: red;\">`us-east-1` は、 `Main.kt` に直接書いてありますので、それに合わせます。</span>\n\n<blockquote class=\"warn\">\n<p>エンドポイントの http://172.17.0.3:4566 は、LocalStack dokckerコンテナに割り当てられたIPアドレスです。</p>\n<p><code># docker container exec -it 2179f4793d31 ifconfig</code></p>\n<p>で調べたIPアドレスです。</p>\n<p>http://localhost:4566 でいけるかと思ったのですが、4566 部分がランダムになり、直接アクセスするようにしました。</p>\n<p><code>http://172.17.0.3:4566</code> と異なる場合、<code># export AWS_ENDPOINT=http://172.17.0.5:4566</code>と環境変数をセットして起動します。</p>\n</blockquote>\n\n<br />\n\n確認します。\n\n```shellsession\n# cat ~/.aws/credentials\n[s3dummy]\naws_access_key_id =  dummy-access-key-id\naws_secret_access_key = dummy-secret-access-key\n# cat ~/.aws/config\n[profile s3dummy]\nregion = us-east-1\noutput = json\n```\n\n<br />\n\nS3 バケットを作成します。\n\n```shellsession\n# aws --endpoint-url=http://172.17.0.3:4566 --profile s3dummy s3api create-bucket --bucket sample-bucket\n```\n\n<br />\n\n確認します。\n\n```shellsession\n# aws s3 ls --endpoint-url=http://172.17.0.3:4566 --profile s3dummy\n2022-04-28 23:11:34 sample-bucket\n```\n\n<br />\n\n適当にファイルを作成します。\n\n```shellsession\n# vi samplefile.txt\nsamplefile\n```\n\n<br />\n\nsample-bucket にアップロードします。\n\n```shellsession\n# aws s3 cp samplefile.txt s3://sample-bucket/ --endpoint-url=http://172.17.0.3:4566 --acl public-read --profile=s3dummy\nupload: ./samplefile.txt to s3://sample-bucket/samplefile.txt\n```\n\n<br />\n\n確認します。\n\n```shellsession\n# aws s3 ls s3://sample-bucket/ --endpoint-url=http://172.17.0.3:4566 --profile s3dummy\n2022-04-28 23:14:11          7 sample.txt\n```\n\n<br />\n\nダウンロードします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/kotless-s3-2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/kotless-s3-2.png\" alt=\"LocalStackのAWS S3互換機能のオブジェクトをダウンロード　の図\" width=\"801\" height=\"733\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n```shellsession\n# mkdir /tmp/media-down\n# curl http://localhost:8080/download?bucketName=sample-bucket\nOK\n# cat /tmp/media-down/samplefile.txt\nsamplefile\n```\n\n<br />\n\nヨシ！！\n\n<br />\n\n<blockquote class=\"info\">\n<p><code># ./gradlew local</code> を停止したら、LocalStackコンテナは消滅し、バケットは消えます。</p>\n</blockquote>\n\n<br />\n\n# エラーメモ\n\nビルドで発生したエラーについて、まとめて記述します。\n\nエラーは、以下のコマンドの出力の一部です。\n\n```shellsession\n# ./gradlew local --stacktrace\n```\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`Caused by: java.lang.NoSuchMethodError: 'org.jetbrains.kotlin.analyzer.AnalysisResult org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(org.jetbrains.kotlin.com.intellij.openapi.project.Project, java.util.Collection, org.jetbrains.kotlin.resolve.BindingTrace, org.jetbrains.kotlin.config.CompilerConfiguration, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope, java.util.List, java.util.List, java.util.List, int, java.lang.Object)'`</span>\n\n<br />\n\n**対処内容：**  \n`build.gradle.kts` を github の README 通りに  \n`kotlin(\"jvm\") version \"1.5.31\" apply true`  \nに修正。\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`2022-04-28T23:00:50.415+0900 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger] > Task :download_terraform FAILED`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`> FAILURE: Build failed with an exception`</span>\n\n<br />\n\n**対処内容：**  \n`build.gradle.kts` に  \n`kotless {`  \n` config {`  \n` aws {`  \n追加。\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`> Task :localstack_start FAILED`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`Could not find a valid Docker environment. Please check configuration. Attempted configurations were:`</span>  \n<span style=\"background-color: cornsilk; color: red;\">` UnixSocketClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause IOException (native connect() failed : Permission denied)`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`As no valid configuration was found, execution cannot continue`</span>\n\n<br />\n\n**対処内容：**  \ndocker 起動権限が有るユーザー（今回は、root）で起動。\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**\n\n<span style=\"background-color: cornsilk; color: red;\">`23:23:37.817 [qtp1586845078-17] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function download`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`java.lang.IllegalArgumentException: Callable expects 2 arguments, but 1 were provided.`</span>  \n<span style=\"background-color: cornsilk; color: red;\">` at kotlin.reflect.jvm.internal.calls.Caller$DefaultImpls.checkArguments(Caller.kt:20)`</span>\n\n<br />\n\n**対処内容：**  \n`suspend function download` の場合、パラメータを受け取れずにエラーになった。→ `suspend` 削除\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`Suspend function 'listObjects' should be called only from a coroutine or another suspend function`</span>\n\n<br />\n\n**対処内容：**  \n`suspend function` をやめると、`listObjects` でエラー。→`runBlocking {}` を使用。\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`> Task :localstack_start FAILED`</span>\n<span style=\"background-color: cornsilk; color: red;\">`Failure when attempting to lookup auth config (dockerImageName: testcontainers/ryuk:0.3.0, configFile: /root/.docker/config.json. Falling back to docker-java default behaviour. Exception message: /root/.docker/config.json (No such file or directory)`</span>\n\n<br />\n\n**対処内容：**  \ntestcontainers/ryuk:0.3.0 　を事前に pull。  \n（<span style=\"color: red;\">最初からやり直したら、出なかったので、タイムアウトしただけで、必要無かったかも。</span>）\n\n```shellsession\n# docker pull testcontainers/ryuk:0.3.0\n```\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**\n\n<span style=\"background-color: cornsilk; color: red;\">`` Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: Missing value for environment variable `AWS_ACCESS_KEY_ID` ``</span>\n<span style=\"background-color: cornsilk; color: red;\">`Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: could not find source profile default`</span>\n<span style=\"background-color: cornsilk; color: red;\">`` Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: Required field `roleArn` could not be automatically inferred for StsWebIdentityCredentialsProvider. Either explicitly pass a value, set the environment variable `AWS_ROLE_ARN`, or set the JVM system property `aws.roleArn` ``</span>\n<span style=\"background-color: cornsilk; color: red;\">`Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: Container credentials URI not set`</span>\n<span style=\"background-color: cornsilk; color: red;\">`Suppressed: aws.sdk.kotlin.runtime.auth.credentials.CredentialsProviderException: failed to load instance profile`</span>\n<span style=\"background-color: cornsilk; color: red;\">`Caused by: io.ktor.network.sockets.ConnectTimeoutException: Connect timeout has expired [url=http://169.254.169.254:80/latest/api/token, connect_timeout=unknown ms]`</span>\n<span style=\"background-color: cornsilk; color: red;\">`Caused by: java.net.SocketTimeoutException: Connect timed out`</span>\n\n<br />\n\n**対処内容：**  \nAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY 　を事前に環境変数にセット。\n\n```shellsession\n# export AWS_ACCESS_KEY_ID=dummy-access-key-id\n# export AWS_SECRET_ACCESS_KEY=dummy-secret-access-key\n# ./gradlew local\n```\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`23:48:46.620 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'`</span>\n<span style=\"background-color: cornsilk; color: red;\">`23:48:46.620 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2`</span>\n<span style=\"background-color: cornsilk; color: red;\">`23:48:46.624 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.`</span>\n<span style=\"background-color: cornsilk; color: red;\">`23:48:56.160 [qtp1586845078-17] ERROR a.s.k.r.http.engine.ktor.KtorEngine - throwing`</span>\n<span style=\"background-color: cornsilk; color: red;\">`java.net.ConnectException: Failed to connect to localhost/127.0.0.1:49186`</span>\n<span style=\"background-color: cornsilk; color: red;\">` at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.kt:297)`</span>\n<span style=\"background-color: cornsilk; color: red;\">` at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:207)`</span>\n\n<br />\n\n**対処内容：**  \nkotless->LocalStack の S3 接続エラー。ポートがランダムに決まる。→ コンテナの IP アドレスへ直接接続。（例：`http://172.17.0.3:4566`）\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`23:39:44.827 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.`</span>\n<span style=\"background-color: cornsilk; color: red;\">`23:41:17.302 [qtp1586845078-17] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function download`</span>\n<span style=\"background-color: cornsilk; color: red;\">`aws.sdk.kotlin.services.s3.model.NoSuchBucket: null`</span>\n<span style=\"background-color: cornsilk; color: red;\">` at aws.sdk.kotlin.services.s3.model.NoSuchBucket$Builder.build(NoSuchBucket.kt:46)`</span>\n<span style=\"background-color: cornsilk; color: red;\">` at aws.sdk.kotlin.services.s3.transform.NoSuchBucketDeserializer.deserialize(NoSuchBucketDeserializer.kt:20)`</span>\n<span style=\"background-color: cornsilk; color: red;\">` at aws.sdk.kotlin.services.s3.transform.ListObjectsOperationDeserializerKt.throwListObjectsError(ListObjectsOperationDeserializer.kt:71)`</span>\n\n<br />\n\n**対処内容：**\n\nバケットが無い。→ バケット名を正しくするか、該当バケットをあらかじめ作成する。\n\n<br />\n\n<hr style=\"height:1px;border-width:0;color:gray;background-color:gray\">\n\n**エラー内容：**  \n<span style=\"background-color: cornsilk; color: red;\">`23:31:10.821 [qtp1586845078-17] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function download`</span>\n<span style=\"background-color: cornsilk; color: red;\">`java.io.FileNotFoundException: /tmp/media-down/samplefile.txt (No such file or directory)`</span>\n\n<br />\n\n**対処内容：**\n\nダウンロード先ディレクトリ作成。\n\n```shellsession\n# mkdir /tmp/media-down\n```\n","description":"KotlinのサーバーレスフレームワークKotlessでAWS S3のバケットをダウンロードするプログラムを作ってみました。本物のAWSではなく、LocalStackで動作します。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"bpy-7yyrif","createdAt":"2022-04-29T11:48:50.190Z","updatedAt":"2022-04-29T11:48:50.190Z","publishedAt":"2022-04-29T11:48:50.190Z","revisedAt":"2022-04-29T11:48:50.190Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-localstack-s3/ITC_Engineering_Blog.png","alt":"KotlessとLocalStackで疑似サーバーレス - AWS S3ダウンロード","width":1200,"height":630}],"seo_authors":[]},{"id":"azure-aca-dapr-bicep","createdAt":"2022-12-06T13:56:28.680Z","updatedAt":"2022-12-30T11:47:40.566Z","publishedAt":"2022-12-06T13:56:28.680Z","revisedAt":"2022-12-30T11:47:40.566Z","title":"Bicepを使ってAzure Container AppsとDaprのマイクロサービスをデプロイ","category":{"id":"r2upy60kf","createdAt":"2022-12-06T10:13:03.471Z","updatedAt":"2022-12-06T10:13:03.471Z","publishedAt":"2022-12-06T10:13:03.471Z","revisedAt":"2022-12-06T10:13:03.471Z","topics":"Bicep","logo":"/logos/Bicep.png","needs_title":true},"topics":[{"id":"r2upy60kf","createdAt":"2022-12-06T10:13:03.471Z","updatedAt":"2022-12-06T10:13:03.471Z","publishedAt":"2022-12-06T10:13:03.471Z","revisedAt":"2022-12-06T10:13:03.471Z","topics":"Bicep","logo":"/logos/Bicep.png","needs_title":true},{"id":"t84zpw1nk-j","createdAt":"2022-12-06T10:12:43.070Z","updatedAt":"2022-12-06T10:12:43.070Z","publishedAt":"2022-12-06T10:12:43.070Z","revisedAt":"2022-12-06T10:12:43.070Z","topics":"Dapr","logo":"/logos/Dapr.png","needs_title":false},{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"hyb2dlkbyj-y","createdAt":"2021-06-03T13:49:36.431Z","updatedAt":"2021-06-03T13:49:36.431Z","publishedAt":"2021-06-03T13:49:36.431Z","revisedAt":"2021-06-03T13:49:36.431Z","topics":"GitHub","logo":"/logos/GitHub.png","needs_title":false},{"id":"l7nk1-m8q","createdAt":"2021-05-09T08:36:28.831Z","updatedAt":"2021-08-31T12:05:09.792Z","publishedAt":"2021-05-09T08:36:28.831Z","revisedAt":"2021-08-31T12:05:09.792Z","topics":"Node.js","logo":"/logos/NodeJS.png","needs_title":false},{"id":"91zw54wj7d","createdAt":"2021-06-05T07:05:37.594Z","updatedAt":"2021-08-31T12:03:57.429Z","publishedAt":"2021-06-05T07:05:37.594Z","revisedAt":"2021-08-31T12:03:57.429Z","topics":"Python","logo":"/logos/python.png","needs_title":false},{"id":"xego85dtzyu","createdAt":"2021-06-03T13:50:33.576Z","updatedAt":"2021-08-31T12:04:26.367Z","publishedAt":"2021-06-03T13:50:33.576Z","revisedAt":"2021-08-31T12:04:26.367Z","topics":"React","logo":"/logos/React.png","needs_title":false}],"content":"# はじめに\n\n前回記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/dapr-local-dev\" target=\"_blank\">Node.js,Python,React で Dapr の状態管理アプリを作成してローカル環境で動作確認</a>」のアプリを Azure Container Apps ＋ Dapr へ Bicep、Azure Container Registry、GitHub Actions を使ってデプロイしてみました。一連の手順を紹介していきたいと思います。\n\n<br />\n\n**Azure Container Apps**：フル マネージド サーバーレス コンテナー サービスです。Azure Container Apps を使うと、Kubernetes のオーケストレーションやインフラストラクチャを気にすることなく、コンテナー化されたアプリケーションを実行できます。  \n**Bicep**：Bicep は、宣言型の構文を使用して Azure リソースをデプロイするドメイン固有言語 (DSL) です。無人で Azure をいじるときに使うプログラミング言語のようなものです。  \n**GitHub Actions**：GitHub Actions は、GitHub が提供する CI/CD サービスです。 GitHub と高度に統合されており、GitHub に公開されたコードを自動でビルド・テスト・デプロイを行うのが主目的です。  \n**Azure Container Registry**：Azure のコンテナレジストリです。Docker Hub の Azure 版のようなものです。<span style=\"color: red;\">有料です。</span>  \n**Web アプリ／マイクロサービス**：画面は、React をビルドしたものです。node のサーバーで画面、 API 要求（ Dapr 経由でアクセス）を振り分けています。マイクロサービスとは言うものの、python の /order API 一つです。これが役割毎に増えていけばマイクロサービスアーキテクチャということです。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/draw1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/draw1.png\" alt=\"Azure Container Appsデプロイ全体像の図\" width=\"1612\" height=\"1302\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"info\">\n<p>【 コンテナレジストリ 】</p>\n<p>Azure Container Registry、GitHub Container Registry（ghcr.io）、Docker Registry、etc...の内、今回は、Azure Container Registry を使います。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>Dapr についての説明は、前回記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/dapr-local-dev\" target=\"_blank\">Node.js,Python,ReactでDaprの状態管理アプリを作成してローカル環境で動作確認</a>」にありますので、省略します。</p>\n</blockquote>\n\n<br />\n\n基本的には、learn.microsoft.com の「<a href=\"https://learn.microsoft.com/ja-jp/azure/container-apps/dapr-github-actions?tabs=bash\" target=\"_blank\">チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする</a>」を見ながら進めましたが、参考にしただけで、同一ではありません。Go で実装されたサービスは省略しました。  \nまた、learn.microsoft.com は、GitHub Container Registry（ghcr.io）を使っていますが、今回は、Azure Container Registry に差し替えています。\n\n<br />\n\n<blockquote class=\"alert\">\n<p>本記事情報により何らかの問題が生じても、一切責任を負いません。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>コンソールは、Ubuntu 20.04 LTS の bash で作業しています。</p>\n<p>git コマンドや az コマンド、npx などはインストール済みで使えるものとします。</p>\n</blockquote>\n\n<br />\n\n# ソースコード準備シナリオ\n\n<blockquote class=\"info\">\n<p>作成済みの全体ソースコードは、 <a href=\"https://github.com/itc-lab/azure-dapr-bicep-simple-app\" target=\"_blank\">https://github.com/itc-lab/azure-dapr-bicep-simple-app</a> にアップしました。</p>\n</blockquote>\n\nアプリ作成（前回記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/dapr-local-dev\" target=\"_blank\">Node.js,Python,React で Dapr の状態管理アプリを作成してローカル環境で動作確認</a>」で作成したものです。）  \n↓  \nコンテナビルドのための `Dockerfile` 作成  \n↓  \nGitHub Actions ワークフローファイル `.github/workflows/build-and-deploy.yaml` 作成  \n↓  \nAzure Container Registry デプロイ用に `./deploy/create-acr.bicep` 作成  \n↓  \nAzure Container Apps 他、Azure へのリソースデプロイ用に  \n`./deploy/main.bicep`  \n`./deploy/environment.bicep`  \n`./deploy/key-vault.bicep`  \n`./deploy/cosmosdb.bicep`  \n`./deploy/key-vault-secret.bicep`  \n`./deploy/dapr-component.bicep`  \n`./deploy/container-http.bicep`  \n作成  \nと準備していきます。\n\n<br />\n\n結果、以下のようになります。\n\n```sh\n.\n├── dapr-components\n│   └── local\n│       └── statestore.yaml\n├── deploy\n│   ├── container-http.bicep\n│   ├── cosmosdb.bicep\n│   ├── create-acr.bicep\n│   ├── dapr-component.bicep\n│   ├── environment.bicep\n│   ├── key-vault.bicep\n│   ├── key-vault-secret.bicep\n│   └── main.bicep\n├── node-service\n│   ├── client\n│   │   ├── build\n│   │   │   ├── asset-manifest.json\n│   │   │   ├── favicon.ico\n│   │   │   ├── index.html\n│   │   │   ├── logo192.png\n│   │   │   ├── logo512.png\n│   │   │   ├── manifest.json\n│   │   │   ├── robots.txt\n│   │   │   └── static\n│   │   │       ├── css\n│   │   │       │   ├── main.073c9b0a.css\n│   │   │       │   └── main.073c9b0a.css.map\n│   │   │       └── js\n│   │   │           ├── 787.c4e7f8f9.chunk.js\n│   │   │           ├── 787.c4e7f8f9.chunk.js.map\n│   │   │           ├── main.a92ca6f7.js\n│   │   │           ├── main.a92ca6f7.js.LICENSE.txt\n│   │   │           └── main.a92ca6f7.js.map\n│   │   ├── package.json\n│   │   ├── package-lock.json\n│   │   ├── public\n│   │   │   ├── favicon.ico\n│   │   │   ├── index.html\n│   │   │   ├── logo192.png\n│   │   │   ├── logo512.png\n│   │   │   ├── manifest.json\n│   │   │   └── robots.txt\n│   │   ├── README.md\n│   │   ├── src\n│   │   │   ├── App.css\n│   │   │   ├── App.test.tsx\n│   │   │   ├── App.tsx\n│   │   │   ├── index.css\n│   │   │   ├── index.tsx\n│   │   │   ├── logo.svg\n│   │   │   ├── react-app-env.d.ts\n│   │   │   ├── reportWebVitals.ts\n│   │   │   └── setupTests.ts\n│   │   └── tsconfig.json\n│   ├── Dockerfile\n│   ├── index.js\n│   ├── package.json\n│   └── package-lock.json\n└── python-service\n    ├── app.py\n    ├── Dockerfile\n    └── requirements.txt\n```\n\n<br />\n\n# Dockerfile 作成\n\nnode サービス側の `Dockerfile` を準備します。  \nnode サービスは、  \n`/order`  \n`/delete`  \nのアクセスの時、自身のサイドカーの Dapr へ転送して、結果、python サービスの方へリクエストが行きます。  \nそれ以外のアクセスの時、`node-service/client/build/` の生成物（html,js）を返します。\n\n```dockerfile:./node-service/Dockerfile\nFROM node:17-alpine\nWORKDIR /usr/src/app\nCOPY . .\nRUN npm install\n\n# Build the client\nRUN --mount=type=secret,id=REACT_APP_MY_API_URL \\\nexport REACT_APP_MY_API_URL=$(cat /run/secrets/REACT_APP_MY_API_URL) && \\\ncd client && npm i && npm run build\n\nEXPOSE 3000\n\nCMD [ \"npm\", \"run\", \"start\" ]\n```\n\n<br />\n\npython サービス側の `Dockerfile` を準備します。\n\n```dockerfile:./python-service/Dockerfile\nFROM python:3.9\nCOPY requirements.txt /app/\nWORKDIR /app\nRUN pip install -r requirements.txt\nCOPY . .\nENTRYPOINT [\"python\"]\nEXPOSE 5000\nCMD [\"app.py\"]\n```\n\n<blockquote class=\"warn\">\n<p><code>app.py</code> を起動しているだけですので、起動すると、以下の警告が出力されます。ちゃんと WSGI を使ったサーバーにした方が良いと思いますが、あくまでもお試し版ということで、このままでいきます。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.</span></p>\n</blockquote>\n\n<br />\n\n# build-and-deploy.yaml 作成\n\nGitHub Actions パイプラインのワークフローファイル `.github/workflows/build-and-deploy.yaml` を作成します。\n\n<blockquote class=\"info\">\n<p>意味の説明は、以前の記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/acr-aca-bicep\" target=\"_blank\">Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ</a>」に詳しく書きましたので、そちらを参照してください。このときとやっていることはだいたい同じです。</p>\n</blockquote>\n\n```yaml:./.github/workflows/build-and-deploy.yaml\nname: Build and Deploy\non:\n  push:\n    branches: [main]\n    tags: [\"v*.*.*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \".vscode/**\"\n  workflow_dispatch:\n\njobs:\n  set-env:\n    name: Set Environment Variables\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.main.outputs.version }}\n      created: ${{ steps.main.outputs.created }}\n      repository: ${{ steps.main.outputs.repository }}\n    steps:\n      - id: main\n        run: |\n          echo version=$(echo ${GITHUB_SHA} | cut -c1-7) >> $GITHUB_OUTPUT\n          echo created=$(date -u +'%Y-%m-%dT%H:%M:%SZ') >> $GITHUB_OUTPUT\n          echo repository=$GITHUB_REPOSITORY >> $GITHUB_OUTPUT\n\n  package-services:\n    runs-on: ubuntu-latest\n    needs: set-env\n    permissions:\n      contents: read\n      packages: write\n    outputs:\n      containerImage-node: ${{ steps.image-tag.outputs.image-node-service }}\n      containerImage-python: ${{ steps.image-tag.outputs.image-python-service }}\n    strategy:\n      matrix:\n        services:\n          [\n            { \"appName\": \"node-service\", \"directory\": \"./node-service\" },\n            { \"appName\": \"python-service\", \"directory\": \"./python-service\" },\n          ]\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n      - name: Log into registry ${{ secrets.REGISTRY_LOGIN_SERVER }}\n        if: github.event_name != 'pull_request'\n        uses: azure/docker-login@v1\n        with:\n          login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}\n          username: ${{ secrets.REGISTRY_USERNAME }}\n          password: ${{ secrets.REGISTRY_PASSWORD }}\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v3\n        with:\n          images: ${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.set-env.outputs.repository }}/${{ matrix.services.appName }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=ref,event=branch\n            type=sha\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v2\n        with:\n          context: ${{ matrix.services.directory }}\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          secrets: |\n            \"REACT_APP_MY_API_URL=${{ secrets.REACT_APP_MY_API_URL }}\"\n      - name: Output image tag\n        id: image-tag\n        run: |\n          echo image-${{ matrix.services.appName }}=$GITHUB_REPOSITORY/${{ matrix.services.appName }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT\n\n  deploy:\n    runs-on: ubuntu-latest\n    needs: package-services\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n\n      - name: Azure Login\n        uses: azure/login@v1\n        with:\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n\n      - name: Deploy bicep\n        uses: azure/CLI@v1\n        with:\n          inlineScript: |\n            az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep \\\n              -p \\\n                minReplicas=1 \\\n                nodeImage='${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.package-services.outputs.containerImage-node }}' \\\n                nodePort=3000 \\\n                pythonImage='${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.package-services.outputs.containerImage-python }}' \\\n                pythonPort=5000 \\\n                containerRegistry=${{ secrets.REGISTRY_LOGIN_SERVER }} \\\n                containerRegistryUsername=${{ secrets.REGISTRY_USERNAME }} \\\n                containerRegistryPassword='${{ secrets.REGISTRY_PASSWORD }}'\n```\n\n<br />\n\n# ACR デプロイ用 Bicep 作成\n\nAzure Container Registry デプロイ用に `./deploy/create-acr.bicep` を作成します。これは、以降の手順で出てきますが、手動で実行します。\n\n```bicep:./deploy/create-acr.bicep\n@minLength(5)\n@maxLength(50)\n@description('Provide a globally unique name of your Azure Container Registry')\nparam acrName string = 'acr${uniqueString(resourceGroup().id)}'\n\n@description('Provide a location for the registry.')\nparam location string = resourceGroup().location\n\n@description('Provide a tier of your Azure Container Registry.')\nparam acrSku string = 'Basic'\n\nresource acrResource 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {\n  name: acrName\n  location: location\n  sku: {\n    name: acrSku\n  }\n  properties: {\n    adminUserEnabled: false\n  }\n}\n\n@description('Output the login server property for later use')\noutput loginServer string = acrResource.properties.loginServer\n```\n\n<br />\n\n# ACA 他デプロイ用 Bicep 作成\n\nAzure Container Apps 他、Azure へのリソースデプロイ用に  \n`./deploy/main.bicep`  \n`./deploy/environment.bicep`  \n`./deploy/key-vault.bicep`  \n`./deploy/cosmosdb.bicep`  \n`./deploy/key-vault-secret.bicep`  \n`./deploy/dapr-component.bicep`  \n`./deploy/container-http.bicep`  \nを準備します。  \nこれは、GitHub Actions の 最後に  \n`az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep・・・`  \nで実行されています。\n\n<br />\n\n## Key Vault について\n\n今回、Dapr - Cosmos DB 紐付けの為だけに、Key Vault を使っています。\n\n<br />\n\nなぜかと言うと、\n\n```bicep\noutput primaryMasterKey string = listKeys(accountName_resource.id, accountName_resource.apiVersion).primaryMasterKey\n```\n\nの行が、  \n<span style=\"color: #e70500;background-color: #ffebe7;\">function list\\*(resourceNameOrIdentifier: string, apiVersion: string, [functionValues: object]): any</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">The syntax for this function varies by name of the list operations. Each implementation returns values for the resource type that supports a list operation. The operation name must start with list. Some common usages are listKeys, listKeyValue, and listSecrets.</span>  \nと  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Outputs should not contain secrets. Found possible secret: function 'listKeys'bicep corehttps://aka.ms/bicep/linter/outputs-should-not-contain-secrets</span>  \nの警告になったからです。  \nprimaryMasterKey は、daprComponents のデプロイ箇所で以下のように渡す必要があるのですが、<span style=\"color: red;\"><strong>秘密の値を output するな</strong></span>と怒られています。\n\n```bicep\nresource stateDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = {\n  name: '${environmentName}/orders'\n  dependsOn: [\n    environment\n  ]\n  properties: {\n    componentType: 'state.azure.cosmosdb'\n    version: 'v1'\n    secrets: [\n      {\n        name: 'masterkey'\n        value: cosmosdb.outputs.primaryMasterKey\n      }\n    ]\n```\n\n<br />\n\nこれだけのために、\n\n<br />\n\nKey Vault デプロイ  \n↓  \nKey Vault Secret に primaryMasterKey の値登録  \n↓  \ndaprComponents のデプロイ箇所で渡す\n\n<br />\n\nとしています。\n\n<br />\n\nただし、Key Vault から取り出した値を渡せば良いかと言うとそうでもなく、\n\n```bicep\nresource stateDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = {\n  name: '${environmentName}/orders'\n  dependsOn: [\n    environment\n  ]\n  properties: {\n    componentType: 'state.azure.cosmosdb'\n    version: 'v1'\n    secrets: [\n      {\n        name: 'masterkey'\n        value: kv.getSecret('CosmosDbPrimaryMasterKey')\n      }\n    ]\n```\n\nのように、`kv.getSecret('CosmosDbPrimaryMasterKey')` を直接渡すと、  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Function \"getSecret\" is not valid at this location. It can only be used when directly assigning to a module parameter with a secure decorator.</span>  \nのエラーになりますので、  \n<span style=\"color: red;\"><strong>`@secure()` で保護した param を渡す</strong></span>必要がありました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/zu2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/zu2.png\" alt=\"bicep CosmosDbPrimaryMasterKeyのところ\" width=\"1613\" height=\"410\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n## Bicep 内容\n\n<blockquote class=\"info\">\n<p><span style=\"color: red;\">意味の説明は、コメントとして記述しました。ただし、Dapr に関する事、Cosmos DB に関する事、Key Vault に関する事以外は省略しています。</span>その他の部分の説明は、以前の記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/acr-aca-bicep\" target=\"_blank\">Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ</a>」に詳しく書きましたので、そちらを参照してください。</p>\n</blockquote>\n\n```bicep:./deploy/main.bicep\nparam location string = resourceGroup().location\nparam environmentName string = 'env-${uniqueString(resourceGroup().id)}'\n\nparam appName string = 'hello-dapr'\n\nparam minReplicas int = 0\n\nparam nodeImage string\nparam nodePort int = 3000\nvar nodeServiceAppName = 'node-app'\n\nparam pythonImage string\nparam pythonPort int = 5000\nvar pythonServiceAppName = 'python-app'\n\nparam isPrivateRegistry bool = true\n\nparam containerRegistry string\n@secure()\nparam containerRegistryUsername string = ''\n@secure()\nparam containerRegistryPassword string = ''\n#disable-next-line secure-secrets-in-params\nparam registryPassword string = 'registry-password'\n\n// Container Apps Environment\nmodule environment 'environment.bicep' = {\n  name: '${deployment().name}--environment'\n  params: {\n    environmentName: environmentName\n    location: location\n    appInsightsName: '${environmentName}-ai'\n    logAnalyticsWorkspaceName: '${environmentName}-la'\n  }\n}\n\n// Key Vault\nmodule keyvault 'key-vault.bicep' = {\n  name: 'keyvault-deployment'\n  params: {\n    location: location\n    appName: appName\n    tenantId: tenant().tenantId\n  }\n}\n\n// Cosmosdb\nmodule cosmosdb 'cosmosdb.bicep' = {\n  name: '${deployment().name}--cosmosdb'\n  params: {\n    location: location\n    primaryRegion: location\n    keyVaultName: keyvault.outputs.keyVaultName\n  }\n}\n\nresource kv 'Microsoft.KeyVault/vaults@2022-07-01' existing = {\n  name: keyvault.outputs.keyVaultName\n}\n\n// Dapr Component\n// マネージド環境で Dapr コンポーネントを作成\nmodule daprComponent 'dapr-component.bicep' = {\n  name: '${deployment().name}--daprComponent'\n  // dependsOn: (module[] | (resource | module) | resource[])[]\n  // リソースをデプロイするとき、一部のリソースが他のリソースより前に確実にデプロイされるようにすることが必要な場合があります。\n  // たとえば、データベースをデプロイする前に論理 SQL\n  // このリレーションシップは、あるリソースが他のリソースに依存しているとマークすることで確立します。\n  // 明示的な依存関係は、dependsOn プロパティで宣言されます。\n  dependsOn: [\n    environment\n    keyvault\n  ]\n  params: {\n    cosmosDbPrimaryMasterKey: kv.getSecret('CosmosDbPrimaryMasterKey')\n    documentEndpoint: cosmosdb.outputs.documentEndpoint\n    environmentName: environmentName\n    pythonServiceAppName: pythonServiceAppName\n  }\n}\n\n// Python App\nmodule pythonService 'container-http.bicep' = {\n  name: '${deployment().name}--${pythonServiceAppName}'\n  dependsOn: [\n    environment\n  ]\n  params: {\n    enableIngress: true\n    isExternalIngress: false\n    location: location\n    environmentName: environmentName\n    containerAppName: pythonServiceAppName\n    containerImage: pythonImage\n    containerPort: pythonPort\n    isPrivateRegistry: isPrivateRegistry\n    minReplicas: minReplicas\n    containerRegistry: containerRegistry\n    registryPassword: registryPassword\n    containerRegistryUsername: containerRegistryUsername\n    revisionMode: 'Single'\n    secrets: [\n      {\n        name: registryPassword\n        value: containerRegistryPassword\n      }\n    ]\n  }\n}\n\n// Node App\nmodule nodeService 'container-http.bicep' = {\n  name: '${deployment().name}--${nodeServiceAppName}'\n  dependsOn: [\n    environment\n  ]\n  params: {\n    enableIngress: true\n    isExternalIngress: true\n    location: location\n    environmentName: environmentName\n    containerAppName: nodeServiceAppName\n    containerImage: nodeImage\n    containerPort: nodePort\n    minReplicas: minReplicas\n    isPrivateRegistry: isPrivateRegistry\n    containerRegistry: containerRegistry\n    registryPassword: registryPassword\n    containerRegistryUsername: containerRegistryUsername\n    revisionMode: 'Multiple'\n    env: [\n      {\n        name: 'PYTHON_SERVICE_NAME'\n        value: pythonServiceAppName\n      }\n    ]\n    secrets: [\n      {\n        name: registryPassword\n        value: containerRegistryPassword\n      }\n    ]\n  }\n}\n\noutput nodeFqdn string = nodeService.outputs.fqdn\noutput pythonFqdn string = pythonService.outputs.fqdn\n```\n\n<br />\n\n```bicep:./deploy/environment.bicep\nparam environmentName string\nparam logAnalyticsWorkspaceName string\nparam appInsightsName string\nparam location string\n\nresource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = {\n  name: logAnalyticsWorkspaceName\n  location: location\n  properties: any({\n    retentionInDays: 30\n    features: {\n      searchVersion: 1\n    }\n    sku: {\n      name: 'PerGB2018'\n    }\n  })\n}\n\nresource appInsights 'Microsoft.Insights/components@2020-02-02' = {\n  name: appInsightsName\n  location: location\n  kind: 'web'\n  properties: {\n    Application_Type: 'web'\n    WorkspaceResourceId:logAnalyticsWorkspace.id\n  }\n}\n\nresource environment 'Microsoft.App/managedEnvironments@2022-03-01' = {\n  name: environmentName\n  location: location\n  properties: {\n    daprAIInstrumentationKey:appInsights.properties.InstrumentationKey\n    appLogsConfiguration: {\n      destination: 'log-analytics'\n      logAnalyticsConfiguration: {\n        customerId: logAnalyticsWorkspace.properties.customerId\n        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey\n      }\n    }\n  }\n}\n\noutput location string = location\noutput environmentId string = environment.id\n```\n\n<br />\n\n```bicep:./deploy/key-vault.bicep\nparam appName string\n\n// Key Vault名（最大長24文字）\n// 今回の場合、kv-hello-dapr-q4******gv（リソースグループID）\n@maxLength(24)\nparam vaultName string = '${'kv-'}${appName}-${substring(uniqueString(resourceGroup().id), 0, 23 - (length(appName) + 3))}' // must be globally unique\nparam location string = resourceGroup().location\nparam sku string = 'Standard'\nparam tenantId string // テナントID\n\n// 下記参照（渡しているところに説明あり）\nparam enabledForDeployment bool = true\nparam enabledForTemplateDeployment bool = true\nparam enabledForDiskEncryption bool = true\nparam enableRbacAuthorization bool = true\nparam softDeleteRetentionInDays int = 90\n\n// ネットワークアクセスルール（特に無し）\nparam networkAcls object = {\n  ipRules: []\n  virtualNetworkRules: []\n}\n\nresource keyvault 'Microsoft.KeyVault/vaults@2022-07-01' = {\n  name: vaultName\n  location: location\n  properties: {\n    tenantId: tenantId\n    sku: {\n      family: 'A'\n      name: sku\n    }\n    // シークレットとして格納されている証明書をキー コンテナーから取得することを\n    // Azure Virtual Machines に許可するかどうかを指定するプロパティ\n    enabledForDeployment: enabledForDeployment\n    // Azure Disk Encryption がコンテナーからシークレットを取得し、\n    // キーをアンラップすることを許可するかどうかを指定するプロパティ\n    enabledForDiskEncryption: enabledForDiskEncryption\n    // Azure Resource Manager がキー コンテナーからシークレットを\n    // 取得することを許可するかどうかを指定するプロパティ\n    enabledForTemplateDeployment: enabledForTemplateDeployment\n    // softDelete（論理削除）データ保持日数（7以上、90以下）\n    softDeleteRetentionInDays: softDeleteRetentionInDays\n    // データ アクションの承認方法を制御するプロパティ。\n    // true の場合、キー コンテナーはデータ アクションの承認に役割ベースの\n    // アクセス制御 (RBAC) を使用し、コンテナーのプロパティで指定された\n    // アクセス ポリシーは無視されます (警告: これはプレビュー機能です)。\n    // false の場合、キー コンテナーはコンテナーのプロパティで指定された\n    // アクセス ポリシーを使用し、Azure Resource Manager に格納されている\n    // ポリシーはすべて無視されます。 null または指定されていない場合、\n    // vault はデフォルト値の false で作成されます。 管理アクションは常に\n    // RBAC で承認されることに注意してください。\n    enableRbacAuthorization: enableRbacAuthorization\n    // 特定のネットワークの場所からキー コンテナーへのアクセスを管理する規則\n    networkAcls: networkAcls\n  }\n}\n\noutput keyVaultName string = keyvault.name\noutput keyVaultId string = keyvault.id\n```\n\n<br />\n\n```bicep:./deploy/cosmosdb.bicep\n// function description(text: string): any\n// @description というデコレータをつけて、パラメータの説明を記載\n// @description デコレータによる説明は、パラメータ入力で ? を入力した際に表示される\n// VSCode拡張機能の場合、マウスオーバー時の説明に表示される。\n@description('Cosmos DB アカウント名、最大長 44 文字、小文字')\nparam accountName string = 'cosmos-${uniqueString(resourceGroup().id)}'\n\n@description('Cosmos DB アカウントのロケーション')\nparam location string\n\n@description('Cosmos DB アカウントのプライマリ レプリカ リージョン')\nparam primaryRegion string\n\n@description('Cosmos DB アカウントの既定の整合性レベル')\n// @allowed\n// パラメーターに使用できる値を定義できます。 使用できる値は配列で指定します。\n// 使用できる値の 1 つではない値がパラメーターに渡された場合、検証時にデプロイは失敗します。\n// ここに書かれているのは、Cosmos DB 整合性の種類\n// Eventual（最終的）\n// Consistent Prefix（一貫性のあるプレフィックス）\n// Session（セッション）\n// Bounded Staleness（有界整合性制約）\n// Strong（強固）\n@allowed([\n  'Eventual'\n  'ConsistentPrefix'\n  'Session'\n  'BoundedStaleness'\n  'Strong'\n])\nparam defaultConsistencyLevel string = 'Session'\n\n// @minValue\n// @maxValue\n// 文字列と配列のパラメーターの最小長と最大長を指定できます。 一方または両方の制約を設定できます。\n// 文字列の場合、長さは文字数を示します。 配列の場合、長さは配列内の項目数を示します。\n@description('古いリクエストの最大数。BoundedStaleness（有界整合性制約）に必要です。有効な範囲、シングル リージョン: 10 ～ 1000000。マルチ リージョン: 100000 ～ 1000000。')\n@minValue(10)\n@maxValue(2147483647)\nparam maxStalenessPrefix int = 100000\n\n@description('最大遅延時間 (分)。BoundedStaleness（有界整合性制約）に必要です。有効な範囲、シングル リージョン: 5 ～ 84600。マルチ リージョン: 300 ～ 86400。')\n@minValue(5)\n@maxValue(86400)\nparam maxIntervalInSeconds int = 300\n\n@description('データベースの名前')\nparam databaseName string = 'ordersDb'\n\n// ここでいうコンテナとは、Cosmos DBの「コンテナ」（テーブルに相当）\n@description('コンテナの名前')\nparam containerName string = 'orders'\n\n// コンテナーの最大スループット\n@description('Maximum throughput for the container')\n@minValue(4000)\n@maxValue(1000000)\nparam autoscaleMaxThroughput int = 4000\n\nparam keyVaultName string\n\n// 指定された文字列を小文字に変換します。\nvar accountNameVar = toLower(accountName)\nvar consistencyPolicy = {\n  Eventual: {\n    // ConsistencyLevel＝整合性レベル\n    // Eventual (最終的)\n    // Write が複数発生した場合の順序保証無し\n    // https://qiita.com/everpeace/items/cbbace418f7bc297631f\n    // 読み込めるデータが 最新である保証はない\n    // 読み込むプロセスによって(同一プロセスであっても)は 先祖返りするかもしれない\n    // でも、すべてのreplicaが いつか同じ状態に収束する\n    // これは5つの内一番整合性が弱いかわりに読込書込がとても速い\n    defaultConsistencyLevel: 'Eventual'\n  }\n  // 一貫性のあるプレフィックス\n  // 読み込めるデータが最新である保証はない,先祖返りするかもしれない, いつか同じ状態に収束する　のはEventualと同じ\n  // それに加えてreplicaに適用される書込リクエストの順序は同じ\n  // 例えば、A, B, Cという書込リクエストが複数のreplicaに適用されるとすると、クライアントからみると、\n  // A, A,B, A,B,Cと書込リクエストが処理されたデータは読み込まれる可能性はあるが、\n  // A,Cとか、B,A,Cという風な順で書込がなされたデータが見える可能性はない\n  ConsistentPrefix: {\n    defaultConsistencyLevel: 'ConsistentPrefix'\n  }\n  // セッション\n  // Consistent Prefixは、先祖返りが起きたり、\n  // 読込リクエストごとに見えるデータの世代が違う、ことが起きるけれど、\n  // Sessionでは、いわゆる\n  // Monotonic Read, Monotonic Write, Read Your own Write(RYW)を保証する\n  // Monotonic Read: あるプロセスから見て読み込めるデータは先祖返りしない\n  // Monotonic Write: あるプロセスからなされるデータXへの書込順序は保存される\n  // Read Your own Write(RYW): あるプロセスが書込処理を行ったら、\n  // そのプロセスはすぐその書込処理完了後のデータが読める\n  Session: {\n    defaultConsistencyLevel: 'Session'\n  }\n  // 有界整合性制約\n  // 読み込めるデータは古いことはあるが、\n  // t単位時間以降はK世代以内のデータが読めることを保証する\n  // ユーザはこのK, tをチューニングできる。\n  // この整合性レベルは、強い整合性を求めたいけどデータの\n  // availabilityが99.99%でよくて、かつ低レイテンシが欲しい場合に有効\n  BoundedStaleness: {\n    defaultConsistencyLevel: 'BoundedStaleness'\n    // 古いリクエストの最大数。 BoundedStaleness に必要。\n    // 有効な範囲、シングル リージョン: 10 ～ 1000000。\n    // マルチ リージョン: 100000 ～ 1000000。\n    maxStalenessPrefix: maxStalenessPrefix\n    // 最大遅延時間 (分)。 BoundedStaleness に必要。\n    // 有効な範囲、シングル リージョン: 5 ～ 84600。マルチ リージョン: 300 ～ 86400。\n    maxIntervalInSeconds: maxIntervalInSeconds\n  }\n  // 厳密、強固\n  // いわゆるLinearizabileを保証する、あるプロセスが書き込めたら、\n  // どのプロセスが読んでも常に最新の値が読める。\n  // majority quorumで実装できる\n  // Linerizabile（線形化可能性）\n  // 並行プログラミングにおいて操作（または操作の集合）は、\n  // 呼び出しイベントと応答イベント\n  // （コールバック）の順序付きリストで構成されており、\n  // 応答イベントを追加することで以下のように拡張できる場合、線形化可能である。\n  // 1.拡張されたリストは逐次履歴として再表現することができる（直列化可能である）。\n  // 2.その逐次履歴は元の拡張されていないリストの部分集合である。\n  // quorumとは分散システムにおいて、分散トランザクションが処理を\n  // 実行するために必要な最低限の票の数である。\n  // quorumベースの技術は分散システムにおいて、処理の整合性をとるために実装される。\n  Strong: {\n    defaultConsistencyLevel: 'Strong'\n  }\n}\nvar locations = [\n  {\n    locationName: primaryRegion\n    // フェールオーバー優先度\n    failoverPriority: 0\n    // ゾーン冗長\n    isZoneRedundant: false\n  }\n]\n\n// Cosmos DB 作成\n// データベースアカウント\n// Azure Cosmos DB リソース モデル\n// https://learn.microsoft.com/ja-jp/azure/cosmos-db/account-databases-containers-items\nresource accountName_resource 'Microsoft.DocumentDB/databaseAccounts@2021-01-15' = {\n  name: accountNameVar\n  // 作成する Cosmos DB データベース アカウントの種類\n  // GlobalDocumentDB, MongoDB, Parse\n  kind: 'GlobalDocumentDB'\n  // リソースが属するリソース グループの場所\n  location: location\n  properties: {\n    // consistencyPolicy＝整合性についての設定\n    // 上で、param defaultConsistencyLevel string = 'Session'　があるため、\n    // 今回の場合、Session。\n    consistencyPolicy: consistencyPolicy[defaultConsistencyLevel]\n    locations: locations\n    // 申し込みタイプ？Standardしかない？\n    databaseAccountOfferType: 'Standard'\n  }\n}\n\n// Cosmos DB 子リソース 個別のデータベース\nresource accountName_databaseName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-01-15' = {\n  parent: accountName_resource\n  name: databaseName\n  // Azure Cosmos DB SQL データベースを作成および更新するためのプロパティ\n  properties: {\n    resource: {\n      // Cosmos DB SQL データベースの名前\n      id: databaseName\n    }\n  }\n}\n\n// Cosmos DB 子リソース 個別のデータベース コンテナ―\n// データが格納される場所\nresource accountName_databaseName_containerName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-01-15' = {\n  parent: accountName_databaseName\n  name: containerName\n  // Azure Cosmos DB コンテナを作成および更新するためのプロパティ\n  properties: {\n    resource: {\n      // Cosmos DB SQL コンテナの名前\n      id: containerName\n      // 論理パーティション（パーティションキー）\n      partitionKey: {\n        // コンテナー内のデータをパーティション分割できるパスのリスト\n        paths: [\n          '/partitionKey'\n        ]\n        // kind: 'Hash' | 'MultiHash' | 'Range' | string\n        // パーティショニングに使用されるアルゴリズムの種類。\n        // MultiHash の場合、コンテナの作成で複数のパーティション キー (最大 3 つまで) が\n        // サポートされる。\n        kind: 'Hash'\n      }\n    }\n    options: {\n      autoscaleSettings: {\n        // maxThroughput: int\n        // リソースがスケールアップできる最大スループット。\n        // 上で定義されている4000。param autoscaleMaxThroughput int = 4000\n        maxThroughput: autoscaleMaxThroughput\n      }\n    }\n  }\n}\n\n// Key Vault に primaryMasterKey 格納\nmodule setCosmosDbPrimaryMasterKey 'key-vault-secret.bicep' = {\n  name: 'setCosmosDbPrimaryMasterKey'\n  params: {\n    // Key Vault の名前\n    keyVaultName: keyVaultName\n    // キー名\n    secretName: 'CosmosDbPrimaryMasterKey'\n    // 値\n    secretValue: listKeys(accountName_resource.id, accountName_resource.apiVersion).primaryMasterKey\n  }\n}\n\noutput documentEndpoint string = accountName_resource.properties.documentEndpoint\n// Cosmos DB のエンドポイント。\n// 以下のように Dapr の設定に使用。\n// metadata: [\n//   {\n//     name: 'url'\n//     value: documentEndpoint\n//   }\n```\n\n<br />\n\n```bicep:./deploy/key-vault-secret.bicep\nparam keyVaultName string\nparam secretName string\n@secure()\nparam secretValue string\n\n// 渡されたシークレットの キーと値をKey Vault に格納\n// 今回は、Cosmos DB の PrimaryMasterKey のみに使用。\nresource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {\n  name: '${keyVaultName}/${secretName}'\n  properties: {\n    value: secretValue\n  }\n}\n```\n\n<br />\n\n```bicep:./deploy/dapr-component.bicep\nparam environmentName string\nparam documentEndpoint string\nparam pythonServiceAppName string\n\n@secure()\nparam cosmosDbPrimaryMasterKey string = ''\n\n// マネージド環境で Dapr コンポーネントを作成\nresource stateDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = {\n  name: '${environmentName}/orders'\n  // Dapr コンポーネント リソース固有のプロパティ\n  properties: {\n    // コンポーネントの種類\n    // 他の例（何を見たら分かる？）\n    // secretstores.azure.keyvault\n    // pubsub.azure.servicebus\n    // state.azure.blobstorage\n    // bindings.cron\n    // bindings.smtp\n    componentType: 'state.azure.cosmosdb'\n    // コンポーネントバージョン\n    version: 'v1'\n    // Dapr コンポーネントによって使用されるシークレットのコレクション\n    secrets: [\n      {\n        name: 'masterkey'\n        value: cosmosDbPrimaryMasterKey\n      }\n    ]\n    // コンポーネントメタデータ\n    // Cosmos DBの接続情報\n    metadata: [\n      {\n        name: 'url'\n        value: documentEndpoint\n      }\n      {\n        name: 'database'\n        value: 'ordersDb'\n      }\n      {\n        name: 'collection'\n        value: 'orders'\n      }\n      {\n        name: 'masterkey'\n        // メタデータ プロパティ値を取得する Dapr コンポーネント シークレットの名前。\n        secretRef: 'masterkey'\n      }\n    ]\n    // この Dapr コンポーネントを使用できるコンテナー アプリの名前\n    scopes: [\n      pythonServiceAppName\n    ]\n  }\n}\n```\n\n<br />\n\n```bicep:./deploy/container-http.bicep\nparam containerAppName string\nparam location string\nparam environmentName string\nparam containerImage string\nparam containerPort int\nparam isExternalIngress bool\nparam containerRegistry string\n@secure()\nparam containerRegistryUsername string\nparam isPrivateRegistry bool\nparam enableIngress bool = true\n@secure()\nparam registryPassword string\nparam minReplicas int = 0\nparam secrets array = []\nparam env array = []\nparam revisionMode string = 'Single'\n\n\nresource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {\n  name: environmentName\n}\n\nresource containerApp 'Microsoft.App/containerApps@2022-03-01' = {\n  name: containerAppName\n  location: location\n  properties: {\n    managedEnvironmentId: environment.id\n    configuration: {\n      activeRevisionsMode: revisionMode\n      secrets: secrets\n      registries: isPrivateRegistry ? [\n        {\n          server: containerRegistry\n          username: containerRegistryUsername\n          passwordSecretRef: registryPassword\n        }\n      ] : null\n      ingress: enableIngress ? {\n        external: isExternalIngress\n        targetPort: containerPort\n        transport: 'auto'\n        traffic: [\n          {\n            latestRevision: true\n            weight: 100\n          }\n        ]\n      } : null\n      dapr: {\n        enabled: true\n        appPort: containerPort\n        appId: containerAppName\n      }\n    }\n    template: {\n      containers: [\n        {\n          image: containerImage\n          name: containerAppName\n          env: env\n        }\n      ]\n      scale: {\n        minReplicas: minReplicas\n        maxReplicas: 1\n      }\n    }\n  }\n}\n\noutput fqdn string = enableIngress ? containerApp.properties.configuration.ingress.fqdn : 'Ingress not enabled'\n```\n\n<br />\n\n# コンテナレジストリ作成\n\nコンテナを push できる場所のコンテナレジストリ（Azure Container Registry のインスタンス）を作成します。\n\n<br />\n\nAzure CLI で、サインインします。\n\n```shellsession\n$ az login --use-device-code\nTo sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H*******W to authenticate.\n```\n\n`https://microsoft.com/devicelogin` にブラウザでアクセスして、表示されているコード（`H*******W`）を入力して、サインインします。\n\n<br />\n\nAzure CLI 最新版を使っているか確認します。\n\n```shellsession\n$ az upgrade\n```\n\n<br />\n\n東日本リージョン `japaneast`（任意）に  \nリソースグループ `my-containerapp-store`（任意）を作成します。\n\n```shellsession\n$ az group create --name my-containerapp-store --location japaneast\n```\n\n<br />\n\n作成したリソースに Azure Container Registry のインスタンスを作成します。\n\n```shellsession\n$ az deployment group create --resource-group my-containerapp-store --template-file ./deploy/create-acr.bicep --parameters acrName=hellodapracrtest\n```\n\n<blockquote class=\"warn\">\n<p>ここで、bicepを使っていますが、特に重要な意味はありません。普通にコマンドラインで作成しても良いです。</p>\n<p>コマンドライン：<code>az acr create --resource-group $RES_GROUP --name $ACR_NAME --sku Basic --location japaneast</code></p>\n</blockquote>\n\n`my-containerapp-store` は、先ほど作成したリソースグループです。`hellodapracrtest` は任意ですが、以下の注意点があります。\n\n<br />\n\n<span style=\"color: red;\"><strong>注意：</strong></span>  \n・`acrName=hellodapracrtest` とすると、`hellodapracrtest.azurecr.io` が作成されます。これは、全世界で重複しない必要があります。重複すると、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">The registry DNS name hellodapracrtest.azurecr.io is already in use. You can check if the name is already claimed using following API:</span>\n\n<br />\n\n・`acrName=hello-dapr-acr-test` のように記号は使えません。アルファベットと数字のみです。また、5 ～ 50 文字である必要があります。名前がまずい場合、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Invalid resource name: 'hello-dapr-acr-test'. Resource names may contain alpha numeric characters only and must be between 5 and 50 characters.. For more information, please refer resource name requirements at</span>\n\n<br />\n\n# サービスプリンシパル作成\n\n共同作成者のロールを持ち、コンテナー レジストリのリソース グループをスコープとするサービス プリンシパルを作成します。\n\n```shellsession\n$ groupId=$(az group show \\\n  --name my-containerapp-store \\\n  --query id --output tsv)\n$ az ad sp create-for-rbac \\\n  --scope $groupId \\\n  --role Contributor \\\n  --sdk-auth\n```\n\n`my-containerapp-store` は、先ほど作成したリソースグループ名です。\n\n<br />\n\n成功したら、以下のような出力が得られます。\n<span style=\"color: red;\"><strong>この出力は後で使います。</strong></span>\n\n```json\n{\n  \"clientId\": \"d9******-****-****-****-**********e3\",\n  \"clientSecret\": \"V9************************************wS\",\n  \"subscriptionId\": \"ea******-****-****-****-**********80\",\n  \"tenantId\": \"0c******-****-****-****-**********2b\",\n  \"activeDirectoryEndpointUrl\": \"https://login.microsoftonline.com\",\n  \"resourceManagerEndpointUrl\": \"https://management.azure.com/\",\n  \"activeDirectoryGraphResourceId\": \"https://graph.windows.net/\",\n  \"sqlManagementEndpointUrl\": \"https://management.core.windows.net:8443/\",\n  \"galleryEndpointUrl\": \"https://gallery.azure.com/\",\n  \"managementEndpointUrl\": \"https://management.core.windows.net/\"\n}\n```\n\n<br />\n\nAzure サービス プリンシパルの資格情報を更新して、コンテナー レジストリに対するプッシュとプルのアクセスを許可します。  \nこの手順により、GitHub ワークフローでサービス プリンシパルを使用して、コンテナー レジストリに対する認証と、  \nDocker イメージのプッシュおよびプルを実行できます。  \nコンテナー レジストリのリソース ID を取得します。  \n環境変数 `registryId` に一旦、先ほど作成した Azure Container Registry のインスタンス のリソースＩＤを格納します。\n\n```shellsession\n$ registryId=$(az acr show \\\n  --name hellodapracrtest \\\n  --resource-group my-containerapp-store \\\n  --query id --output tsv)\n$ echo $registryId\n/subscriptions/ea0c9***-****-****-****-*******a0c80/resourceGroups/my-containerapp-store/providers/Microsoft.ContainerRegistry/registries/hellodapracrtest\n```\n\n`my-containerapp-store` は、先ほど作成したリソースグループ名です。`hellodapracrtest` は、先ほど作成した Azure Container Registry です。\n\n<br />\n\n`az role assignment create` を使用して、レジストリに対するプッシュおよびプル アクセスを付与する AcrPush ロールを割り当てます。\n\n```shellsession\n$ az role assignment create \\\n  --assignee d9******-****-****-****-**********e3 \\\n  --scope $registryId \\\n  --role AcrPush\n```\n\n<span style=\"color: red;\"><strong>`--assignee d9******-****-****-****-**********e3` のところは、先ほど JSON 出力にあった clientId です。</strong></span>\n\n<br />\n\n# リソースプロバイダーの登録\n\n今回、Key Vault を使うのですが、リソースプロバイダーに登録が無いと、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">The subscription is not registered to use namespace 'Microsoft.KeyVault'.</span>\n\n<br />\n\nこのエラーにより、デプロイに失敗しますので、リソースプロバイダーを登録しておきます。\n\n```shellsession\n$ az provider register --namespace Microsoft.KeyVault\nRegistering is still on-going. You can monitor using 'az provider show -n Microsoft.KeyVault'\n```\n\n<br />\n\n# GitHub 準備\n\n## リポジトリ作成\n\nGitHub にプライベートリポジトリ `aca-app-repo` を作成します。（リポジトリの作り方については省略します。）  \nリポジトリ名は任意です。（サービスプリンシパルのアプリ名に合わせる必要もありません。）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image2.png\" alt=\"リポジトリ作成\" width=\"1200\" height=\"451\" loading=\"lazy\"></a>\n\n<br />\n\n## Personal access tokens\n\n・GitHub ワークフロー実行権限（`workflow`）  \n・GitHub Container Registry への push 権限（`write:packages`）  \nを有効にします。\n\n<br />\n\n**Settings** -> 左下の **Developer settings** -> **Personal access tokens** -> **Tokens (classic)**  \nにて、`workflow` と `write:packages` にチェックを入れ、`Update token` をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image3.png\" alt=\"Personal access tokens手順　Settings\" width=\"1200\" height=\"557\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image4.png\" alt=\"Personal access tokens手順　Developer settings\" width=\"1200\" height=\"673\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image5.png\" alt=\"Personal access tokens手順　Update token\" width=\"1200\" height=\"868\" loading=\"lazy\"></a>\n\n<br />\n\n## シークレット\n\nシークレット（ワークフロー内で使う秘密の値）を設定します。\n\n<br />\n\nリポジトリ `aca-app-repo` に戻って、\n\n**Settings** -> **Secrets** -> **Actions** -> **New repository secret** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image6.png\" alt=\"New repository secret\" width=\"1156\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\nここに、以下を設定します。  \n**AZURE_CREDENTIALS**：先ほどのサービス プリンシパルの作成の JSON 出力全体  \n**REGISTRY_LOGIN_SERVER**：レジストリのログイン サーバー名（今回は、`hellodapracrtest.azurecr.io`）  \n**REGISTRY_USERNAME**：サービス プリンシパルの作成 JSON 出力の clientId（今回は、`d9******-****-****-****-**********e3`）  \n**REGISTRY_PASSWORD**：サービス プリンシパルの作成 JSON 出力の clientSecret（今回は、`V9************************************wS`）  \n**RESOURCE_GROUP**：サービス プリンシパルのスコープ指定に使用したリソース グループの名前（今回は、`my-containerapp-store`）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image7.png\" alt=\"AZURE_CREDENTIALS、RESOURCE_GROUP\" width=\"1163\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\n# commit & push\n\nリポジトリ `aca-app-repo` main ブランチに push します。\n\n```shellsession\n$ rm -rf node-service/client/.git\n$ git init\n$ git config --local user.name \"AAAAA BBBBB\"\n$ git config --local user.email \"xxxxx@example.com\"\n$ git add .\n$ git commit -m \"first commit\"\n$ git branch -M main\n$ git remote add origin https://github.com/<github user name>/aca-app-repo.git\n$ git push -u origin main\n```\n\n`<github user name>` 部分は、GitHub ユーザー名です。\n\n<br />\n\n# Run workflow\n\n**Actions** -> **Build and Deploy** -> **Run workflow**  \nをクリックして、`Branch: main` のままにして、**Run workflow** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-container-bicep/image8.png\" alt=\"Run workflow\" width=\"1152\" height=\"603\" loading=\"lazy\"></a>\n\n<br />\n\n**`Set Environment Vairables` ジョブ**  \n後続のジョブで使用する値を生成しています。  \n↓  \n**`package-services` ジョブ**  \ndocker コンテナをビルドして、コンテナレジストリ（今回の場合、`hellodapracrtest.azurecr.io`）にビルドしたコンテナを push しています。  \n<span style=\"color: red;\">今回、コンテナが２つありますので、２つ並行して実行されます。</span>  \n↓  \n**`deploy`** ジョブ  \nAzure リソースをデプロイしています。  \n（既にデプロイ済みの場合は、Azure Container Apps アプリ `node-app` のリビジョンが一つ増えます。`python-app` の方は、シングルリビジョンのため、リビジョンは増えません。）\n\n<br />\n\nと進んで、全て緑色のチェックが付いたら、デプロイ完了です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image1.png\" alt=\"\" width=\"1200\" height=\"611\" loading=\"lazy\"></a>\n\n`deploy` ジョブで作成されるリソースは、以下です。  \n・Key Vault  \n・Azure Cosmos DB  \n・ログ分析ワークスペース  \n・Application Insights  \n・Azure Container Apps の環境  \n・Azure Container Apps のアプリ（`node-app` と `python-app`）\n\n<br />\n\nAzure Container Apps の２つのアプリ名は、`./deploy/main.bicep` の  \n`var nodeServiceAppName = 'node-app'`  \n`var pythonServiceAppName = 'python-app'`  \nに書かれています。（アプリ名は任意です。<span style=\"color: red;\"><strong>Dapr は、この名前で通信します。何でもいいように、環境変数になっています。</strong></span>）\n\n<br />\n\n# API の URL について\n\n画面 →`https://<デプロイにより得られたFQDN>/order`  \n画面 →`https://<デプロイにより得られたFQDN>/delete`  \nという API アクセスがあるのですが、  \n`<デプロイにより得られたFQDN>` はデプロイ後に決まるため、React をビルドしている最中には分かりません。  \nそのため、以下の作業を行い、再度、パイプライン起動が必要です。  \n（<span style=\"color: red;\">FQDN が最初から決まっている場合、最初から REACT_APP_MY_API_URL を Secrets に登録しておけば、問題無いです。</span>）\n\n<br />\n\n`node-app` の概要より、**アプリケーション URL** をコピーします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image2.png\" alt=\"\" width=\"1200\" height=\"271\" loading=\"lazy\"></a>\n\n<br />\n\nリポジトリの Secrets に以下の値を登録します。  \n**REACT_APP_MY_API_URL**：`<コピーしたアプリケーション URL>`\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image3.png\" alt=\"\" width=\"1146\" height=\"673\" loading=\"lazy\"></a>\n\n再度、ワークフローを起動します。\n\n```shellsession\n$ git add .\n$ git commit -m \"second commit\"\n$ git push -u origin main\n```\n\n<span style=\"color: red;\"><strong>注意：</strong></span>  \nRe-run jobs でもう一度ワークフローを起動しても Azure Container Apps のリビジョン名が commit id に関わっているため、更新されません。ここでは、改行を増やすなど、些細な修正をしたものとします。\n\n<br />\n\n# 動作確認\n\n**１．** データ無しの状態で、GET してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image4.png\" alt=\"\" width=\"761\" height=\"235\" loading=\"lazy\"></a>\n\nデータが無いという結果が返りました。\n\n<br />\n\n**２．** POST してデータを入れます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image5.png\" alt=\"\" width=\"761\" height=\"226\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>実装を最小限にするため、データは固定になっています。</p>\n</blockquote>\n\n<br />\n\n**３．** 再びデータを GET します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image6.png\" alt=\"\" width=\"761\" height=\"236\" loading=\"lazy\"></a>\n\nデータが取り出されました。\n\n<br />\n\n**４．** データを DELETE します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image7.png\" alt=\"\" width=\"761\" height=\"165\" loading=\"lazy\"></a>\n削除に成功しました。\n\n<br />\n\n**５．** 再びデータを GET します。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/image8.png\" alt=\"\" width=\"761\" height=\"211\" loading=\"lazy\"></a>\nデータ無しに戻ります。\n\n<br />\n\n一連の作成物は、以下のコマンドでリソースグループごと削除することで、まとめて削除できます。\n\n```shellsession\n$ az group delete \\\n  --resource-group my-containerapp-store\n```\n\n<span style=\"color: red;\">Key Vault については、消したことを覚えているため、以下のように完全に削除します。</span>\n\n```shellsession\n$ az keyvault list-deleted\n[\n  {\n    \"id\": \"/subscriptions/ea******-****-****-****-**********80/providers/Microsoft.KeyVault/locations/japaneast/deletedVaults/kv-hello-dapr-q4******gv\",\n    \"name\": \"kv-hello-dapr-q4******gv\",\n    \"properties\": {\n・・・\n$ az keyvault purge --name kv-hello-dapr-q4******gv\n```\n\n削除しないと、再度同じ名前でデプロイする機会があったときに以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">A vault with the same name already exists in deleted state. You need to either recover or purge existing key vault.</span>\n\n<br />\n\nマルチリビジョンや、トラフィックの変更については、こちらで触れていますので、省略します。  \n↓  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/acr-aca-bicep#h2-titile-36\" target=\"_blank\">Azure Container Apps へ bicep,Azure Container Registry,GitHub Actions を使ってデプロイ - 動作確認１～</a>\n\n<br />\n\nヨシ！\n","description":"Bicepを使ってAzure Container AppsとDaprのマイクロサービスをデプロイしました。前回記事の簡易Webアプリソースコードを使用しています。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"quqq50xl3u","createdAt":"2022-12-06T13:53:07.993Z","updatedAt":"2022-12-06T13:53:07.993Z","publishedAt":"2022-12-06T13:53:07.993Z","revisedAt":"2022-12-06T13:53:07.993Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-aca-dapr-bicep/ITC_Engineering_Blog.png","alt":"Bicepを使ってAzure Container AppsとDaprのマイクロサービスをデプロイ","width":1200,"height":630}],"seo_authors":[]},{"id":"proxies","createdAt":"2021-08-29T10:57:57.646Z","updatedAt":"2021-08-29T16:14:52.211Z","publishedAt":"2021-08-29T10:57:57.646Z","revisedAt":"2021-08-29T16:14:52.211Z","title":"いろいろなproxy - curl,apt,git,npm,pip,Next.jsのfetch,yum,dnf,composer,pkg","category":{"id":"umqsrvfrv7","createdAt":"2021-08-29T10:56:17.442Z","updatedAt":"2021-08-31T12:02:21.915Z","publishedAt":"2021-08-29T10:56:17.442Z","revisedAt":"2021-08-31T12:02:21.915Z","topics":"Unix/Linux","logo":"/logos/Linux.png","needs_title":true},"topics":[{"id":"umqsrvfrv7","createdAt":"2021-08-29T10:56:17.442Z","updatedAt":"2021-08-31T12:02:21.915Z","publishedAt":"2021-08-29T10:56:17.442Z","revisedAt":"2021-08-31T12:02:21.915Z","topics":"Unix/Linux","logo":"/logos/Linux.png","needs_title":true},{"id":"9acgdtf6pf","createdAt":"2021-06-03T13:51:31.714Z","updatedAt":"2021-08-31T12:04:14.034Z","publishedAt":"2021-06-03T13:51:31.714Z","revisedAt":"2021-08-31T12:04:14.034Z","topics":"Next.js","logo":"/logos/NextJS.png","needs_title":false},{"id":"uvtjusqhfx","createdAt":"2021-05-05T06:29:56.227Z","updatedAt":"2021-08-31T12:08:44.327Z","publishedAt":"2021-05-05T06:29:56.227Z","revisedAt":"2021-08-31T12:08:44.327Z","topics":"php","logo":"/logos/php.png","needs_title":false}],"content":"# はじめに\n以下の図のように、インターネットに直接出られない環境があるとき、Proxyサーバーを使います。ProxyサーバーのSquid、各コマンドのProxy設定、オプションをすぐに忘れて、そのたびに個別にググっていますので、ここにまとめて書きたいと思います。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/proxies/proxy1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/proxies/proxy1.png\" style=\"margin-top: 3px;\" alt=\"\" loading=\"lazy\"></a>  \n※説明簡略化のため、Squidは 認証無し のとりあえず動けば良いという設定です。これを使う前提になります。組織によっては、セキュリティ上問題あるかもしれませんので、本格運用の際は慎重に設定してください。  \n\n<br />\n\n【目次】  \n\n・[Squidインストール](#anchor1)  \n・[全体のProxy](#anchor2)  \n・[curl](#anchor3)  \n・[apt](#anchor4)  \n・[git](#anchor5)  \n・[npm](#anchor6)  \n・[pip](#anchor7)  \n・[Next.jsのfetch](#anchor8)  \n・[yum](#anchor9)  \n・[dnf](#anchor10)  \n・[phpのcopy](#anchor11)  \n・[composer-setup.php](#anchor12)  \n・[composer](#anchor13)  \n・[pkg(FreeBSD)](#anchor14)  \n\n<br />\n\n<blockquote class=\"warn\">\n<p>root権限で作業していますので、全てsudoは省略しています。</p>\n</blockquote>\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# Squidインストール\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Raspberry Pi OS(Raspberry 4 本体)</code></p>\n<p><code>/etc/debian_version</code>：10.9</p>\n<p><code>lsb_release -a</code>：Raspbian GNU/Linux 10 (buster)</p>\n<p><code>uname -a</code>：Linux raspberrypi 5.10.17-v7l+ #1414 SMP Fri Apr 30 13:20:47 BST 2021 armv7l GNU/Linux</p>\n</blockquote>\n\nSquid（スクウィッド）はプロキシ (Proxy) サーバー、ウェブキャッシュサーバーなどに利用されるフリーソフトウェアです。  \n今回、プロキシサーバーとして、これを利用する前提になります。認証設定は、無しです。http://, https:// 以外は未検証です。  \n\n<br />\n\n`apt update`でパッケージリストを更新します。\n\n```sh\n# apt update\nDo you want to accept these changes and continue updating from this repository? [y/N]y\n```\n\n※以降基本的にyのため、-yを付けます。  \n　-y は、? [y/N]: のようなときに自動的に y とするオプションです。  \n\n<br />\n\n`apt upgrade`でパッケージを更新し、Squidをインストールします。  \n※`apt update`、`apt upgrade`は環境によっては必要ありません。\n\n```sh\n# apt upgrade -y\n# apt install squid -y\n```\n\n<br />\n\nSquidの設定を作成します。\n\n```sh\n# mv /etc/squid/squid.conf /etc/squid/squid.conf.org\n# vi /etc/squid/squid.conf\n```\n\n```apacheconf\n# 接続クライアントのネットワークを指定(ご自身の環境に応じて変更が必要です)\nacl localnet src 192.168.1.0/24 \n\n# SSL(HTTPS)接続時に 443 ポート以外の CONNECT を拒否\nacl SSL_ports port 443\nacl CONNECT method CONNECT\nhttp_access deny CONNECT !SSL_ports\n\n# 接続先として指定されているポート以外を拒否\nacl Safe_ports port 80    # http\nacl Safe_ports port 21    # ftp\nacl Safe_ports port 443   # https\nacl Safe_ports port 70    # gopher\nacl Safe_ports port 210   # wais\nacl Safe_ports port 1025-65535  # unregistered ports\nacl Safe_ports port 280   # http-mgmt\nacl Safe_ports port 488   # gss-http\nacl Safe_ports port 591   # filemaker\nacl Safe_ports port 777   # multiling http\nhttp_access deny !Safe_ports\n\n# ローカルネットワークからのアクセスを許可\nhttp_access allow localnet\n\n# 自身からのアクセスを許可\nhttp_access allow localhost\n\n# キャッシュしないよう設定\nno_cache deny all\n\n# Squid が使用するポート\nhttp_port 3128\n\n# core 出力場所の設定\ncoredump_dir /var/spool/squid\n\n# QueryStringの記録\nstrip_query_terms off\n\n# ログの保存先と形式(Apache風)\nlogformat combined %>a %ui %un [%tl] \"%rm %ru HTTP/%rv\" %Hs %h\" \"%{User-Agent}>h\" %Ss:%Sh\naccess_log /var/log/squid/access.log combined\n```\n\nなお、設定は、こちらの参考記事ほぼそのままです。  \n<a href=\"https://algorithm.joho.info/raspberry-pi/squid-raspberry-pi/\" target=\"_blank\"><code>【ラズベリーパイ4】Squidでプロキシサーバを自作する方法　https://algorithm.joho.info/raspberry-pi/squid-raspberry-pi/</code></a>  \n\n<br />\n\nSquidを再起動します。\n\n```sh\n# systemctl restart squid\n```\n\n⇒何も出力されなければ、成功です。\n\n<br />\n\n<strong><span style=\"color: red;\">以降、このプロキシサーバーを 192.168.0.158:3128 として進めていきます。</span></strong>  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/proxies/proxy2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/proxies/proxy2.png\" style=\"margin-top: 3px;\" alt=\"\" loading=\"lazy\"></a>  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# 全体のProxy\n※FreeBSDの場合は、[pkg(FreeBSD)](#anchor14)を参照してください。  \n\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2</code></p>\n</blockquote>\n\n```sh\n# vi /etc/environment\n```\n\n```sh\nhttp_proxy=\"http://192.168.0.158:3128\"\nHTTP_PROXY=\"http://192.168.0.158:3128\"\nhttps_proxy=\"http://192.168.0.158:3128\"\nHTTPS_PROXY=\"http://192.168.0.158:3128\"\nftp_proxy=\"ftp://192.168.0.158:3128\"\nFTP_PROXY=\"ftp://192.168.0.158:3128\"\nno_proxy=\".example.co.jp,.test.local,server1.example.com,server2.example.com\"\n```\n\n※`no_proxy=`は、プロキシサーバーを通したくない通信先です。\n\n<blockquote class=\"warn\">\n<p>一度ログアウトしてログインしないと反映されません。もしくは、rootの場合、<code>exit</code>→<code>su -</code>で反映されます。</p>\n</blockquote>\n\n<br />\n\n`一時的な場合`  \n\n```sh\nexport http_proxy=\"http://192.168.0.158:3128\"\nexport HTTP_PROXY=\"http://192.168.0.158:3128\"\nexport https_proxy=\"http://192.168.0.158:3128\"\nexport HTTPS_PROXY=\"http://192.168.0.158:3128\"\nexport ftp_proxy=\"ftp://192.168.0.158:3128\"\nexport FTP_PROXY=\"ftp://192.168.0.158:3128\"\nexport no_proxy=\".example.co.jp,.test.local,server1.example.com,server2.example.com\"\n```\n\n<br />\n\n`動作確認`  \n\n```sh\n# curl --head https://www.yahoo.co.jp/\nHTTP/1.1 200 Connection established\n（略　以降も同様に略）\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# curl\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2</code></p>\n</blockquote>\n\n`環境変数`：○  \n↑  \n○の意味：環境変数を設定した場合、オプションや設定無しでもプロキシへ通信が行くという意味です。例えば、curlの場合、`-x`または、`--proxy`オプション無しでもプロキシサーバーが使われます。\n\n<br />\n\n```sh\n# curl --head https://www.yahoo.co.jp/ -x http://192.168.0.158:3128\nHTTP/1.1 200 Connection established\n```\n\n<br />\n\n```sh\n# curl --head https://www.yahoo.co.jp/ --proxy http://192.168.0.158:3128\nHTTP/1.1 200 Connection established\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# apt\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# vi /etc/apt/apt.conf.d/apt.conf\n```\n\n```sh\nAcquire::http::proxy \"http://192.168.0.158:3128\";\nAcquire::https::proxy \"http://192.168.0.158:3128\";\nAcquire::ftp::proxy \"ftp://192.168.0.158:3128\";\n```\n\n<br />\n\n`動作確認`  \n\n```sh\n# which curl\n\n# apt install curl\nReading package lists... Done\nBuilding dependency tree\nReading state information... Done\nThe following additional packages will be installed:\n  libcurl4\n（略）\nFetched 396 kB in 7s (55.2 kB/s)\n(Reading database ... 147843 files and directories currently installed.)\nPreparing to unpack .../libcurl4_7.68.0-1ubuntu2.6_amd64.deb ...\n（略）\n# which curl\n/usr/bin/curl\n```\n\n<blockquote class=\"warn\">\n<p>環境によっては、事前に<code>apt update</code>が必要かもしれません。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor5\"></a>\n\n# git\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# git config --global http.proxy http://192.168.0.158:3128\n# git config --global https.proxy http://192.168.0.158:3128\n```\n\n<br />\n\n`動作確認`  \n\n```sh\n# git config --global --list \nhttp.proxy=http://192.168.0.158:3128\nhttps.proxy=http://192.168.0.158:3128\n# git clone https://github.com/itc-lab/itc-blog.git\nCloning into 'itc-blog'...\nremote: Enumerating objects: 517, done.\nremote: Counting objects: 100% (517/517), done.\nremote: Compressing objects: 100% (343/343), done.\n（略）\n```\n\n<br />\n\n`Proxy設定解除`  \n\n```sh\n# git config --global --unset http.proxy\n# git config --global --unset https.proxy\n# git config --global --list\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor6\"></a>\n\n# npm\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>CentOS 7.6.1810</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# npm -g config set proxy http://192.168.0.158:3128\n# npm -g config set https-proxy http://192.168.0.158:3128\n```\n\n<br />\n\n`動作確認`  \n\n```sh\n# npm -g config list | grep proxy\nhttps-proxy = \"http://192.168.0.158:3128/\"\nproxy = \"http://192.168.0.158:3128/\"\n# npm install -g yarn\n\n> yarn@1.22.11 preinstall /usr/local/lib/node_modules/yarn\n> :; (node ./preinstall.js > /dev/null 2>&1 || true)\n\n/usr/local/bin/yarn -> /usr/local/lib/node_modules/yarn/bin/yarn.js\n/usr/local/bin/yarnpkg -> /usr/local/lib/node_modules/yarn/bin/yarn.js\n+ yarn@1.22.11\nadded 1 package in 7.238s\n```\n\n<br />\n\n`Proxy設定解除`  \n\n```sh\n# npm -g config delete proxy\n# npm -g config delete https-proxy\n# npm -g config list | grep proxy\n```\n\n\n<br />\n\n<a class=\"anchor\" id=\"anchor7\"></a>\n\n# pip\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>Raspberry Pi Desktop OS</code></p>\n<p><code>Debian GNU/Linux 10 (buster)</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# pip install wiringpi --proxy http://192.168.0.158:3128\nCollecting wiringpi\n  Using cached wiringpi-2.60.1.tar.gz (130 kB)\nBuilding wheels for collected packages: wiringpi\n  Building wheel for wiringpi (setup.py) ... done\n  Created wheel for wiringpi: filename=wiringpi-2.60.1-cp38-cp38-linux_x86_64.whl size=333652 sha256=52a39cf1be009f9c8dc61d6e7624446eb91755cf5fd6a702b5184064646832e8\n  Stored in directory: /root/.cache/pip/wheels/9e/28/68/323be0608c36361080a9172fcf56395eda64e45315c4d2de40\nSuccessfully built wiringpi\nInstalling collected packages: wiringpi\nSuccessfully installed wiringpi-2.60.1\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor8\"></a>\n\n# Next.jsのfetch\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>CentOS 7.6.1810</code></p>\n<p><code>npm view next version: 11.1.0</code></p>\n</blockquote>\n\n`環境変数`：×  \n\n<br />\n\nこのブログ、ビルド時にNext.jsのfetchでmicroCMSとtwitterのAPIをGETしているのですが、環境変数、npmをproxy設定してもfetch関数は、proxyを使わず、明示的に実装する必要がありました。\n\n<br />\n\n```sh\n$ cd itc-blog\n$ npm install https-proxy-agent\n```\n\nソースコード実装変更\n\n```typescript\n  const key = {\n    headers: header,\n  };\n  const total_count_data: ContentRootObject = await fetch(\n    `${process.env.API_URL}contents/?limit=0`,\n    key\n  )\n    .then((res) => res.json())\n    .catch(() => null);\n```\n\n↓  \n環境変数https_proxyを読み取るように変更  \n\n```typescript\nimport { HttpsProxyAgent } from 'https-proxy-agent';\n```\n\n```typescript\n  const proxy = process.env.https_proxy;\n  const key = proxy\n    ? {\n        headers: header,\n        agent: new HttpsProxyAgent(proxy),\n      }\n    : {\n        headers: header,\n      };\n  const total_count_data: ContentRootObject = await fetch(\n    `${process.env.API_URL}contents/?limit=0`,\n    key\n  )\n    .then((res) => res.json())\n    .catch(() => null);\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor9\"></a>\n\n# yum\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>CentOS 7.6.1810</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# vi /etc/yum.conf\n```\n\n以下を追記 ※`[main]`のセクション\n\n```sh\nproxy=http://192.168.0.158:3128\n```\n\n<br />\n\n`動作確認`  \n\n```\n# which python3\n/usr/bin/which: no python3 in (/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin)\n# yum install python3\n読み込んだプラグイン:fastestmirror, langpacks\nDetermining fastest mirrors\nepel/x86_64/metalink              | 7.2 kB  00:00:00\n * base: ftp-srv2.kddilabs.jp\n（略）\n総ダウンロード容量: 9.3 M\nインストール容量: 47 M\nIs this ok [y/d/N]:y\n（略）\n# which python3\n/bin/python3\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor10\"></a>\n\n# dnf\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p><code>CentOS 8.3.2011</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# vi /etc/dnf/dnf.conf\n```\n\n以下を追記 ※`[main]`のセクション\n\n```sh\nproxy=http://192.168.0.158:3128\n```\n\n<br />\n\n`動作確認`  \n\n```\n# which php\n/usr/bin/which: no php in (/home/admin/.local/bin:/home/admin/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin)\n# dnf install php\nCentOS Linux 8 - AppStream  89% [===================================================================-        ] 399 kB/s | 7.9 MB     00:02 ETA\n（略）\n# which php\n/usr/bin/php\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor11\"></a>\n\n# phpのcopy\n<p>【検証環境】</p>\n<p><code>CentOS 8.3.2011</code></p>\n<p><code>PHP 7.2.24</code></p>\n</blockquote>\n\n`環境変数`：×  \n\n<br />\n\n```sh\n# php -r \"copy('https://getcomposer.org/installer', 'composer-setup.php');\"\nPHP Warning:  copy(): php_network_getaddresses: getaddrinfo failed: Name or service not known in Command line code on line 1\nPHP Warning:  copy(https://getcomposer.org/installer): failed to open stream: php_network_getaddresses: getaddrinfo failed: Name or service not known in Command line code on line 1\n```\n\nとエラーになるので、`stream_context_create(['http' => ['proxy' => 'tcp://192.168.0.158:3128']])`を追加  \n↓  \n\n```sh\n# php -r \"copy('https://getcomposer.org/installer', 'composer-setup.php', stream_context_create(['http' => ['proxy' => 'tcp://192.168.0.158:3128']]));\"\n```\n\n⇒composer-setup.phpがカレントフォルダに置かれればＯＫ  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor12\"></a>\n\n# composer-setup.php\n<p>【検証環境】</p>\n<p><code>CentOS 8.3.2011</code></p>\n<p><code>PHP 7.2.24</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# php composer-setup.php\nAll settings correct for using Composer\nDownloading...\nThe \"https://getcomposer.org/versions\" file could not be downloaded: php_network_getaddresses: getaddrinfo failed: Name or service not known\nfailed to open stream: php_network_getaddresses: getaddrinfo failed: Name or service not known\nRetrying...\nThe \"https://getcomposer.org/versions\" file could not be downloaded: php_network_getaddresses: getaddrinfo failed: Name or service not known\nfailed to open stream: php_network_getaddresses: getaddrinfo failed: Name or service not known\nRetrying...\nThe \"https://getcomposer.org/versions\" file could not be downloaded: php_network_getaddresses: getaddrinfo failed: Name or service not known\nfailed to open stream: php_network_getaddresses: getaddrinfo failed: Name or service not known\nThe download failed repeatedly, aborting.\n```\n\nとエラーになるので、環境変数をセットして実行  \n↓  \n\n```sh\n# https_proxy=http://192.168.0.158:3128 php composer-setup.php\nAll settings correct for using Composer\nDownloading...\n\nComposer (version 2.1.6) successfully installed to: /home/admin/composer.phar\nUse it: php composer.phar\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor13\"></a>\n\n# composer\n<p>【検証環境】</p>\n<p><code>CentOS 8.3.2011</code></p>\n<p><code>PHP 7.2.24</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\n```sh\n# HTTP_PROXY=\"http://192.168.0.158:3128\" HTTPS_PROXY=\"http://192.168.0.158:3128\" composer install\nNo composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.\nLoading composer repositories with package information\nUpdating dependencies\nLock file operations: 1 install, 0 updates, 0 removals\n  - Locking monolog/monolog (1.0.2)\nWriting lock file\nInstalling dependencies from lock file (including require-dev)\nPackage operations: 1 install, 0 updates, 0 removals\n  - Installing monolog/monolog (1.0.2): Extracting archive\nGenerating autoload files\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor14\"></a>\n\n# pkg(FreeBSD)\n<p>【検証環境】</p>\n<p><code>FreeBSD 13.0-RELEASE</code></p>\n</blockquote>\n\n`環境変数`：○  \n\n<br />\n\nFreeBSDとLinuxの環境変数の設定方法は、異なります。  \nFreeBSDの場合、以下です。  \n\n<br />\n\n`ログインシェル＝csh, tcshの場合`：  \n\n```sh\n# echo $SHELL\n/bin/csh\n# setenv HTTP_PROXY http://192.168.0.158:3128\n# setenv HTTPS_PROXY http://192.168.0.158:3128\n```\n\n<br />\n\n`ログインシェル＝csh, tcshの場合の設定`：  \n\n```sh\n# vi /etc/csh.cshrc\n```\n\n以下を追記  \n```sh\nsetenv HTTP_PROXY http://192.168.0.158:3128\nsetenv HTTPS_PROXY http://192.168.0.158:3128\n```\n※一度exitしてログインしなおすか、`source /etc/csh.cshrc`コマンドで有効化します。\n\n<br />\n\n`ログインシェル＝shの場合`：  \n\n```sh\n# echo $SHELL\n/bin/sh\n# HTTP_PROXY=http://192.168.0.158:3128\n# HTTPS_PROXY=http://192.168.0.158:3128\n# export HTTP_PROXY\n# export HTTPS_PROXY\n```\n\n<br />\n\n`ログインシェル＝shの場合の設定`：  \n\n```sh\n# vi /etc/profile\n```\n\n以下を追記  \n\n```sh\nHTTP_PROXY=http://192.168.0.158:3128\nHTTPS_PROXY=http://192.168.0.158:3128\nexport HTTP_PROXY\nexport HTTPS_PROXY\n```\n\n※一度exitしてログインしなおす必要があります。\n\n<br />\n\n`pkgのProxy設定`：  \n\n<span style=\"color: red;\">pkg自体は既にインストール済みとします。（プロキシを使う場合、上記の環境変数をセットしてインストールできます。）</span>\n\n```sh\n# vi /usr/local/etc/pkg.conf\n```\n\n以下を追記  \n\n```sh\npkg_env : {\n    http_proxy: \"http://192.168.0.158:3128\"\n    https_proxy: \"http://192.168.0.158:3128\"\n}\n```\n\n<br />\n\n`動作確認`  \n\n```\n# pkg install curl\nUpdating FreeBSD repository catalogue...\nFetching meta.conf: 100%    163 B   0.2kB/s    00:01\nFetching packagesite.txz: 100%    6 MiB   3.3MB/s    00:02\nProcessing entries: 100%\nFreeBSD repository update completed. 30742 packages processed.\n（略）\n# which curl\n/usr/local/bin/curl\n```\n\n<br />\n","description":"インターネットに直接出られない環境があるとき、Proxyサーバーを使います。ProxyサーバーのSquid、各コマンドのProxy設定、オプションをすぐに忘れて、そのたびに個別にググっていますので、ここにまとめて書きたいと思います。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"js2ni-9tt67","createdAt":"2021-08-29T10:55:22.586Z","updatedAt":"2021-08-29T10:55:22.586Z","publishedAt":"2021-08-29T10:55:22.586Z","revisedAt":"2021-08-29T10:55:22.586Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/proxies/ITC_Engineering_Blog.png","alt":"いろいろなproxy - curl,apt,git,npm,pip,Next.jsのfetch,yum,dnf,composer,pkg","width":1200,"height":630}],"seo_authors":[]},{"id":"sqs-shoryuken","createdAt":"2022-05-19T11:07:42.644Z","updatedAt":"2022-05-26T12:48:27.545Z","publishedAt":"2022-05-19T11:07:42.644Z","revisedAt":"2022-05-26T12:48:27.545Z","title":"LocalStackをAWS SQSにしてRuby on Rails Shoryukenを使ってみた","category":{"id":"v7qhii097q","createdAt":"2022-04-29T11:49:40.704Z","updatedAt":"2022-04-29T11:49:40.704Z","publishedAt":"2022-04-29T11:49:40.704Z","revisedAt":"2022-04-29T11:49:40.704Z","topics":"AWS","logo":"/logos/AWS.png","needs_title":false},"topics":[{"id":"v7qhii097q","createdAt":"2022-04-29T11:49:40.704Z","updatedAt":"2022-04-29T11:49:40.704Z","publishedAt":"2022-04-29T11:49:40.704Z","revisedAt":"2022-04-29T11:49:40.704Z","topics":"AWS","logo":"/logos/AWS.png","needs_title":false},{"id":"66escguum4b","createdAt":"2022-05-19T08:11:41.194Z","updatedAt":"2022-05-19T08:11:41.194Z","publishedAt":"2022-05-19T08:11:41.194Z","revisedAt":"2022-05-19T08:11:41.194Z","topics":"Ruby","logo":"/logos/Ruby.png","needs_title":true},{"id":"xppddprow2ce","createdAt":"2022-05-19T08:11:59.213Z","updatedAt":"2022-05-19T08:11:59.213Z","publishedAt":"2022-05-19T08:11:59.213Z","revisedAt":"2022-05-19T08:11:59.213Z","topics":"Rails","logo":"/logos/Rails.png","needs_title":false},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false},{"id":"29q_dqpsz_s8","createdAt":"2022-01-21T14:10:13.121Z","updatedAt":"2022-01-21T14:10:13.121Z","publishedAt":"2022-01-21T14:10:13.121Z","revisedAt":"2022-01-21T14:10:13.121Z","topics":"Docker","logo":"/logos/Docker.png","needs_title":false}],"content":"# はじめに\n\nタイトルの SQS とは、Amazon Simple Queue Service (Amazon SQS) のことになります。  \nSQS を使うには、AWS に契約して、設定して、インターネットに接続して使わないといけないですが、  \nLocalStack を使うと、ネット接続できなくても、オフラインで SQS を使ったプログラムの動作確認ができます。  \n今回、AWS CLI を使って、本物の SQS の代わりに LocalStack へ指示を出して、キューの出し入れの動作確認と、Ruby on Rails + Shoryuken を使って、プログラムによるキューの出し入れを動作確認します。  \nLocalStack、Ruby on Rails + Shoryuken は、インストールからやっていきます。\n\n<blockquote class=\"warn\">\n<p>動作確認はオフラインでもできますが、インストール作業時は、ネット接続しています。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 SQS 】</p>\n<p>\"分散型メッセージキューイングサービス\"です。</p>\n<p>AWSが提供するサービスの一部で、サーバーレスでキューイングを実現できるサービスです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 LocalStack 】</p>\n<p>オンプレ上のAWSのようなものです。疑似的なAWSを構築して、本物のAWSへ接続しなくてもAWSサービスを利用したプログラムの動作確認ができます。AWSサービスの一部、例えば、Cognitoなどは、有料になります。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs1.png\" alt=\"AWS CLI, Ruby on Rails + Shoryuken - LocalStack SQS 全体図\" width=\"661\" height=\"421\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>図中の矢印を本物のAWSに向けると、本物のAWSでも動作すると思いますが、<span style=\"color: red;\">本物では、デプロイ、動作確認していません。</span></p>\n</blockquote>\n\n今回、全て１台の Ubuntu 20.04 LTS で作業しています。\n\n<blockquote class=\"info\">\n<p>【検証環境】</p>\n<p>VMware Workstation Pro 16</p>\n<p>　Ubuntu 20.04.2 LTS</p>\n<p>　　Docker 20.10.16</p>\n<p>　　　LocalStack 0.14.1</p>\n<p>　　aws-cli/1.18.69</p>\n<p>　　ruby 3.0.4p208</p>\n<p>　　Rails 7.0.3</p>\n<p>　　Shoryuken 6.0.0</p>\n</blockquote>\n\n<br />\n\n# LocalStack インストール\n\n## Docker インストール\n\n`docker-compose` を使いますので、まず、Docker をインストールします。\n\n<blockquote class=\"warn\">\n<p>LocalStack インストール方法として、<code>pip</code> を使う方法もありますが、その他のインストール方法は、割愛します。</p>\n<p>その他の方法は、<a href=\"https://docs.localstack.cloud/get-started/\" target=\"_blank\">公式ドキュメント（https://docs.localstack.cloud/get-started/）</a>で分かると思います。</p>\n</blockquote>\n\n```shellsession\n# apt update\n# apt install -y apt-transport-https ca-certificates curl software-properties-common\n# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n# add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\"\n# apt update\n# apt install -y docker-ce\n# docker -v\nDocker version 20.10.16, build aa7e414\n```\n\n<br />\n\n## docker-compose インストール\n\n今回、LocakStack を `docker-compose` で立ち上げようと思います。  \n<a href=\"https://docs.localstack.cloud/get-started/\" target=\"_blank\">公式ドキュメント（`https://docs.localstack.cloud/get-started/`）</a>には、`currently requires docker-compose version 1.9.0+` と書かれています（2022 年 5 月時点）ので、`docker-compose 1.29.2` をインストールします。\n\n```shellsession\n# curl -L \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n# chmod +x /usr/local/bin/docker-compose\n# docker-compose --version\ndocker-compose version 1.29.2, build 5becea4c\n```\n\n<br />\n\n## docker-compose.yml 修正\n\nlocalstack.git をクローンして、`docker-compose.yml` を編集します。\n\n```shellsession\n# git clone https://github.com/localstack/localstack.git\n# cd localstack\n# vi docker-compose.yml\n```\n\nPro 版の記述を削除して、`restart: always` を追加して、OS（Ubuntu）を再起動しても LocalStack が自動起動するようにします。\n\n```yaml:docker-compose.yml\n削除\n      - \"127.0.0.1:53:53\"                # only required for Pro (DNS)\n      - \"127.0.0.1:53:53/udp\"            # only required for Pro (DNS)\n      - \"127.0.0.1:443:443\"              # only required for Pro (LocalStack HTTPS Edge Proxy)\n\n      - LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY-}  # only required for Pro\n\n追加\n    restart: always\n```\n\n`docker-compose.yml` の全体は、以下です。\n\n```yaml:docker-compose.yml\nversion: \"3.8\"\n\nservices:\n  localstack:\n    container_name: \"${LOCALSTACK_DOCKER_NAME-localstack_main}\"\n    image: localstack/localstack\n    network_mode: bridge\n    ports:\n      - \"127.0.0.1:4510-4559:4510-4559\"  # external service port range\n      - \"127.0.0.1:4566:4566\"            # LocalStack Edge Proxy\n    environment:\n      - DEBUG=${DEBUG-}\n      - DATA_DIR=${DATA_DIR-}\n      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}\n      - HOST_TMP_FOLDER=${TMPDIR:-/tmp/}localstack\n      - DOCKER_HOST=unix:///var/run/docker.sock\n    volumes:\n      - \"${TMPDIR:-/tmp}/localstack:/tmp/localstack\"\n      - \"/var/run/docker.sock:/var/run/docker.sock\"\n    restart: always\n```\n\n<blockquote class=\"info\">\n<p>【 <code>${DEBUG-}</code> 等の末尾のマイナス記号の意味 】\n<p><code>${DEBUG}</code> かつ、環境変数 <code>DEBUG</code> がセットされていない場合、 <code>docker-compose up</code> で\n<p><code>WARNING: The DATA_DIR variable is not set. Defaulting to a blank string.</code>\n<p>と警告メッセージが出力されます。一方、<code>${DEBUG-}</code> かつ、環境変数 <code>DEBUG</code> がセットされていない場合、警告メッセージは表示されません。\n<p><code>${TMPDIR:-/tmp/}</code>のように<code>:-</code>の場合は、デフォルト値の設定です。環境変数 <code>TMPDIR</code> がセットされていなかった場合、<code>/tmp/</code> になります。\n</blockquote>\n\n<br />\n\n## LocalStack 起動\n\n`docker-compose.yml` が存在するディレクトリで、`docker-compose up` により、LocalStack 環境の構築と、起動が行われます。\n\n```shellsession\n# docker-compose up -d\nStarting localstack_main ... done\n# docker ps\nCONTAINER ID   IMAGE                   COMMAND                  CREATED              STATUS         PORTS                                                                    NAMES\n13b95c739f99   localstack/localstack   \"docker-entrypoint.sh\"   About a minute ago   Up 4 seconds   127.0.0.1:4510-4559->4510-4559/tcp, 127.0.0.1:4566->4566/tcp, 5678/tcp   localstack_main\n```\n\n<blockquote class=\"warn\">\n<p>【 :8080 Web UI(dashboard) について 】\n<p>この手順のLocalStackは8080ポートのdashboard機能は使えません。</p>\n<p><a href=\"https://stackoverflow.com/questions/57554575/localstack-cant-access-dashboard\" target=\"_blank\">https://stackoverflow.com/questions/57554575/localstack-cant-access-dashboard</a></p>\n<p>によると、<code>localstack/localstack-full</code>（Pro版）を使う必要があるようです。</p>\n</blockquote>\n\n<br />\n\n# AWS CLI インストール\n\nSQS（LocalStack のモック）の動作確認をするため、AWS CLI をインストールします。\n\n```shellsession\n# apt install -y awscli\n# aws --version\naws-cli/1.18.69 Python/3.8.5 Linux/5.8.0-49-generic botocore/1.16.19\n```\n\n<blockquote class=\"info\">\n<p>【 AWS CLI 】</p>\n<p>AWS Command Line Interface (AWS CLI) は、コマンドラインシェルでコマンドを使用して AWS サービスとやり取りするためのオープンソースツールです。AWS CLI を使用すると、最小限の設定で、任意のターミナルプログラムのコマンドプロンプトから、ブラウザベースの AWS Management Console で提供される機能と同等の機能を実装するコマンドを実行できます。</p>\n</blockquote>\n\n<br />\n\n# SQS 動作確認\n\nLocalStack の SQS を AWS CLI を使って、動作確認します。\n\n## profile 作成\n\nキューのリストを表示するコマンドは、\n\n```shellsession\n# aws sqs list-queues\n```\n\nですが、いきなり実行すると、以下のエラーになります。  \n<span style=\"background-color: cornsilk; color: red;\">You must specify a region. You can also configure your region by running \"aws configure\".</span>\n\n<br />\n\nprofile の作成が必要です。profile とは、アクセスキーやリージョンの設定のことです。\n\n<br />\n\nlocalstack という名前の profile を作成します。（値は適当でＯＫです。）\n\n```shellsession\n# aws configure --profile localstack\nAWS Access Key ID [None]: dummy\nAWS Secret Access Key [None]: dummy\nDefault region name [None]: us-east-1\nDefault output format [None]: json\n```\n\nさらに、profile を作成してもエンドポイントを LocalStack に向けていないと、以下のようにエラーになります。（エラー内容は一見 `profile` の設定の問題に見える。）\n\n```shellsession\n# aws sqs list-queues --profile localstack\n```\n\n<span style=\"background-color: cornsilk; color: red;\">An error occurred (InvalidClientTokenId) when calling the ListQueues operation: The security token included in the request is invalid.</span>\n\n本物の AWS に対しての実行と認識されていますので、`--endpoint-url=http://localhost:4566` オプションが必要です。\n\n<br />\n\n## キュー作成\n\n`bonjour` というキューを作成します。\n\n```shellsession\n# aws sqs create-queue --queue-name bonjour \\\n  --endpoint-url http://localhost:4566 \\\n  --profile localstack\n{\n    \"QueueUrl\": \"http://localhost:4566/000000000000/bonjour\"\n}\n```\n\n<br />\n\nキューが作成されたか確認します。\n\n```shellsession\n# aws sqs list-queues \\\n  --endpoint-url http://localhost:4566 \\\n  --profile localstack\n{\n    \"QueueUrls\": [\n        \"http://localhost:4566/000000000000/bonjour\"\n    ]\n}\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs2.png\" alt=\"AWS CLI で bonjour キュー作成 図\" width=\"456\" height=\"261\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n## メッセージ送信\n\nメッセージをキューに送信します。(プロデューサー側の操作)  \nこのとき、`--queue-url` は、キューを作成したときに表示される `QueueUrl` です。\n\n```shellsession\n# aws sqs send-message --queue-url http://localhost:4566/000000000000/bonjour --message-body \"Bonjour le monde\" \\\n  --profile localstack \\\n  --endpoint-url http://localhost:4566\n{\n    \"MD5OfMessageBody\": \"5eb63bbbe01eeed093cb22bb8f5acdc3\",\n    \"MessageId\": \"fb290b94-2f3a-1cb2-e2fa-97da4a464551\"\n}\n```\n\n<br />\n\nキューの詳細を確認します。  \nキューの詳細を表示するコマンドは、`aws sqs get-queue-attributes` で、`--attribute-names All` オプションで表示する属性を絞り込まないようにしています。  \n絞り込む場合は、`--attribute-names ApproximateNumberOfMessages` のように属性名を指定します。\n\n```shellsession\n# aws sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/bonjour \\\n  --attribute-names All \\\n  --profile=localstack \\\n  --endpoint-url=http://localhost:4566\n{\n    \"Attributes\": {\n        \"ApproximateNumberOfMessages\": \"1\",\n        \"ApproximateNumberOfMessagesDelayed\": \"0\",\n        \"ApproximateNumberOfMessagesNotVisible\": \"0\",\n        \"CreatedTimestamp\": \"1651914722.301067\",\n        \"DelaySeconds\": \"0\",\n        \"LastModifiedTimestamp\": \"1651914722.301067\",\n        \"MaximumMessageSize\": \"262144\",\n        \"MessageRetentionPeriod\": \"345600\",\n        \"QueueArn\": \"arn:aws:sqs:us-east-1:000000000000:bonjour\",\n        \"ReceiveMessageWaitTimeSeconds\": \"0\",\n        \"VisibilityTimeout\": \"30\"\n    }\n}\n```\n\n`\"ApproximateNumberOfMessages\": \"1\",` により、メッセージが１件登録されているのが分かります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs3.png\" alt=\"AWS CLI で bonjour キューへメッセージ登録 図\" width=\"456\" height=\"261\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n## キュー消費\n\nメッセージをキューから消費します。(コンシューマー側の操作)\n\n```shellsession\n# aws sqs receive-message --queue-url http://localhost:4566/000000000000/bonjour \\\n  --profile localstack \\\n  --endpoint-url http://localhost:4566\n{\n    \"Messages\": [\n        {\n            \"MessageId\": \"fb290b94-2f3a-1cb2-e2fa-97da4a464551\",\n            \"ReceiptHandle\": \"brndnprrbglkzgrtjerytdikwcenrppcvedvpbaevftpudbwenjqzqdvihxgmrmxtsgfdmjijvsllmtkttjtvjmunnupfblnoxqxlhacasjjkdbelbdkiqvikaqponmvqdvgzcrwgkodcgzbgkiyaiwtxzmymndcguktjgkrwqcplvqtzpjftgwyw\",\n            \"MD5OfBody\": \"5eb63bbbe01eeed093cb22bb8f5acdc3\",\n            \"Body\": \"Bonjour le monde\"\n        }\n    ]\n}\n```\n\n<br />\n\nキューの詳細を確認します。\n\n```shellsession\n# aws sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/bonjour \\\n  --attribute-names All \\\n  --profile=localstack \\\n  --endpoint-url=http://localhost:4566\n{\n    \"Attributes\": {\n        \"ApproximateNumberOfMessages\": \"0\",\n        \"ApproximateNumberOfMessagesDelayed\": \"0\",\n        \"ApproximateNumberOfMessagesNotVisible\": \"1\",\n        \"CreatedTimestamp\": \"1651914722.301067\",\n        \"DelaySeconds\": \"0\",\n        \"LastModifiedTimestamp\": \"1651914722.301067\",\n        \"MaximumMessageSize\": \"262144\",\n        \"MessageRetentionPeriod\": \"345600\",\n        \"QueueArn\": \"arn:aws:sqs:us-east-1:000000000000:bonjour\",\n        \"ReceiveMessageWaitTimeSeconds\": \"0\",\n        \"VisibilityTimeout\": \"30\"\n    }\n}\n```\n\n`\"ApproximateNumberOfMessages\": \"0\",`  \n`\"ApproximateNumberOfMessagesNotVisible\": \"1\",`  \nにより、１件処理されたことが分かります。\n\n<blockquote class=\"info\">\n<p>【 ApproximateNumberOfMessagesNotVisible 】</p>\n<p>処理中のメッセージの数。メッセージがクライアントに送信されたが、まだ削除されていない場合、または表示期限に達していない場合、メッセージは処理中とみなされます。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs4.png\" alt=\"AWS CLI で メッセージをキューから消費 図\" width=\"456\" height=\"261\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n## メッセージ削除\n\n処理中になったメッセージを削除します。  \nこのとき、`--receipt-handle` は、メッセージをキューから消費したときに表示される `ReceiptHandle` です。\n\n```shellsession\n# aws sqs delete-message --queue-url http://localhost:4566/000000000000/bonjour \\\n  --receipt-handle WCtj1aBsktWdQlFYhXWIJDBNgJGcNvwS5YvY02BdCKLbPjDI9NsXofxp19klOX9auaRr0GSVsco0CyJQ5gzGlT7ZTNbZFxyfvoEIsmdc09XWnrSZuOsQALzWfTE04KCEix9Sjpirbne3XaocPG9IEjHUpavnAw6IE59jTirCvpLJ \\\n  --profile localstack \\\n  --endpoint-url http://localhost:4566\n```\n\n<br />\n\nキューの詳細を確認します。\n\n```shellsession\n# aws sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/bonjour \\\n  --attribute-names All \\\n  --profile=localstack \\\n  --endpoint-url=http://localhost:4566\n{\n    \"Attributes\": {\n        \"ApproximateNumberOfMessages\": \"0\",\n        \"ApproximateNumberOfMessagesDelayed\": \"0\",\n        \"ApproximateNumberOfMessagesNotVisible\": \"0\",\n        \"CreatedTimestamp\": \"1651914722.301067\",\n        \"DelaySeconds\": \"0\",\n        \"LastModifiedTimestamp\": \"1651914722.301067\",\n        \"MaximumMessageSize\": \"262144\",\n        \"MessageRetentionPeriod\": \"345600\",\n        \"QueueArn\": \"arn:aws:sqs:us-east-1:000000000000:bonjour\",\n        \"ReceiveMessageWaitTimeSeconds\": \"0\",\n        \"VisibilityTimeout\": \"30\"\n    }\n}\n```\n\n`ApproximateNumberOfMessages*` が全て `\"0\"` になりました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs5.png\" alt=\"AWS CLI で bonjourキューからメッセージ削除 図\" width=\"456\" height=\"261\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n# Ruby インストール\n\nコマンドラインツール rbenv を使用して Ruby と Rails をインストールします。\n\n<blockquote class=\"info\">\n<p>【 Rbenv 】</p>\n<p>Rbenvは、Rubyの異なるバージョンを切り替えるために使用できるツールです。</p>\n</blockquote>\n\nまず、依存関係を解消しておきます。\n\n```shellsession\n# apt update\n# apt install -y git curl libssl-dev libreadline-dev zlib1g-dev autoconf bison build-essential libyaml-dev libreadline-dev libncurses5-dev libffi-dev libgdbm-dev\n```\n\n<br />\n\n<span style=\"color: red;\"><strong>ここから一般ユーザー（今回は、\"admin\"とします。）で作業します。</strong></span>\n\n<br />\n\nrbenv-installer を起動します。\n\n```shellsession\n$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash\n```\n\n<br />\n\n/home/admin/.rbenv/bin/rbenv にインストールされるため、\nパスを通し、rbenv コマンドラインユーティリティを使用できるようにします。\n\n```shellsession\n$ echo 'export PATH=\"$HOME/.rbenv/bin:$PATH\"' >> ~/.bashrc\n```\n\n<br />\n\n`eval \"$(rbenv init - bash)\"` により、rbenv が自動的にロードされるようにします。\n\n```shellsession\n$ echo 'eval \"$(rbenv init - bash)\"' >> ~/.bashrc\n$ source ~/.bashrc\n$ rbenv --version\nrbenv 1.2.0-14-gc6cc0a1\n```\n\n<br />\n\nインストールできる ruby を確認します。\n\n```shellsession\n$ rbenv install -l\n2.6.10\n2.7.6\n3.0.4\n3.1.2\njruby-9.3.4.0\nmruby-3.0.0\nrbx-5.0\ntruffleruby-22.1.0\ntruffleruby+graalvm-22.1.0\n\nOnly latest stable releases for each Ruby implementation are shown.\nUse 'rbenv install --list-all / -L' to show all local versions.\n```\n\n<br />\n\n今回、3.0.4 をインストールすることにします。\n\n```shellsession\n$ rbenv install 3.0.4\n```\n\n<br />\n\n`rbenv global` で Ruby のデフォルトバージョンとして設定します。\n\n```shellsession\n$ rbenv global 3.0.4\n$ ruby -v\nruby 3.0.4p208 (2022-04-12 revision 3fa771dded) [x86_64-linux]\n```\n\n<br />\n\n# Rails インストール\n\n```shellsession\n$ gem install rails -v 6.1.5.1\n$ rbenv rehash\n$ rails -v\nRails 6.1.5.1\n```\n\nで終わりですが、<span style=\"color: red;\"><strong>この場合、グローバルに rails がインストールされますので、今回このインストール方法ではやっていません。ローカル（プロジェクト毎）にインストールする方法にします。</strong></span>  \nプロジェクト名を `sqs-app` とします。\n\n```shellsession\n$ mkdir sqs-app\n$ cd sqs-app\n$ bundle init\nWriting new Gemfile to /home/admin/sqs-app/Gemfile\n$ vi Gemfile\n```\n\n```sh\n# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngit_source(:github) { |repo_name| \"https://github.com/#{repo_name}\" }\n\n# gem \"rails\"\n```\n\nを\n\n```sh\n# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngit_source(:github) { |repo_name| \"https://github.com/#{repo_name}\" }\n\ngem \"rails\"\n```\n\nとします。（`gem \"rails\"` を有効にする。）\n\n<br />\n\nプロジェクトのディレクトリ配下にある、vendor/bundle 配下に gem をインストールするように指定します。\n\n```shellsession\n$ bundle config set path 'vendor/bundle'\n```\n\n<br />\n\n# Rails プロジェクト作成\n\nRails プロジェクト初期化は、`$ bundle exec rails new .` ですが、そのまま実行すると、sqlite3 の gem をチェックするときに、エラーになりました。（ＤＢを何も指定しないと、SQLite とみなされます。）  \n先に `libsqlite3-dev` をインストールしておきます。\n\n```shellsession\n$ sudo apt install -y libsqlite3-dev\n```\n\n<br />\n\ngem を更新します。（`gem \"rails\"` が有効になって、rails がインストールされます。）\n\n```shellsession\n$ bundle install\n```\n\n<blockquote class=\"info\">\n<p>【 bundle install 】</p>\n<p>bundler を使って Gemfile から gem をインストールするコマンドです。</p>\n<p><code>install</code> は、デフォルトのオプションのため、<code>bundle</code> だけでも同じ意味です。</p>\n</blockquote>\n\n<br />\n\nプロジェクト `sqs-app` を Rails プロジェクトとして初期化します。今回、\"動けば良い\" でいきますので、全てデフォルトのオプション無しとします。\n\n```shellsession\n$ bundle exec rails new .\n       exist\n      create  README.md\n      create  Rakefile\n      create  .ruby-version\n      create  config.ru\n      create  .gitignore\n      create  .gitattributes\n    conflict  Gemfile\nOverwrite /home/admin/sqs-app/Gemfile? (enter \"h\" for help) [Ynaqdhm] Y\n```\n\nオプションを付けるとしたら、以下のような例になります。\n\n```shellsession\n$ bundle exec rails new . -B -d mysql --skip-test --skip-coffee\n```\n\n`-B` ： Rails プロジェクト作成時に `bundle install` を行わない。  \n`-d mysql` ： DB を mysql に変更。  \n`–skip-test` ： rails のデフォルトの minitest を使わない。  \n`–skip-coffee` ： coffee スクリプトを使わない。\n\n<br />\n\n# Shoryuken インストール\n\nshoryuken gem をインストールします。\n\n```shellsession\n$ vi Gemfile\n```\n\n以下を追加します。\n\n```sh\ngem 'shoryuken'\ngem 'aws-sdk-sqs'\n```\n\n<br />\n\n```shellsession\n$ bundle install\n```\n\n<br />\n\n# Shoryuken 実行\n\nここまで来たら、  \n<a href=\"https://github.com/ruby-shoryuken/shoryuken/wiki/Getting-Started#rails\" target=\"_blank\">`https://github.com/ruby-shoryuken/shoryuken/wiki/Getting-Started#rails`</a>  \nに従って、  \nCreate a job  \nCreate a queue  \nSet the queue backend  \nStart Shoryuken  \nEnqueue a message  \nをやっていきたいところですが、本物の AWS 前提のため、LocalStack に振り向けるように、`config/initializers/shoryuken.rb` を設定します。  \n`region`、`access_key_id`、`secret_access_key` は、環境変数、`endpoint` は `http://localhost:4566` 固定とします。\n\n```shellsession\n$ vi config/initializers/shoryuken.rb\n```\n\n```ruby:config/initializers/shoryuken.rb\nShoryuken.configure_client do |config|\n  config.sqs_client = Aws::SQS::Client.new(\n    region: ENV[\"AWS_REGION\"],\n    access_key_id: ENV[\"AWS_ACCESS_KEY_ID\"],\n    secret_access_key: ENV[\"AWS_SECRET_ACCESS_KEY\"],\n    endpoint: 'http://localhost:4566',\n    verify_checksums: false\n  )\nend\nShoryuken.configure_server do |config|\n  config.sqs_client = Aws::SQS::Client.new(\n    region: ENV[\"AWS_REGION\"],\n    access_key_id: ENV[\"AWS_ACCESS_KEY_ID\"],\n    secret_access_key: ENV[\"AWS_SECRET_ACCESS_KEY\"],\n    endpoint: 'http://localhost:4566',\n    verify_checksums: false\n  )\nend\n```\n\n<br />\n\nadmin ユーザーで profile を登録します。（profile 名=`localstack` で値は適当です。）\n\n```shellsession\n$ aws configure --profile localstack\nAWS Access Key ID [None]: dummy\nAWS Secret Access Key [None]: dummy\nDefault region name [None]: us-east-1\nDefault output format [None]: json\n```\n\n<br />\n\nあとは、Getting-Started の通りに、ジョブ実行のクラスを定義します。\n\n```shellsession\n$ vi app/jobs/hello_job.rb\n```\n\n```ruby\nclass HelloJob < ActiveJob::Base\n  queue_as 'hello'\n\n  def perform(name)\n    puts \"Hello, #{name}\"\n  end\nend\n```\n\n<br />\n\nキューを作成します。\n\n```shellsession\n$ export AWS_ACCESS_KEY_ID=dummy\n$ export AWS_SECRET_ACCESS_KEY=dummy\n$ export AWS_REGION=us-east-1\n$ bundle exec shoryuken sqs create hello --endpoint=http://localhost:4566\nQueue hello was successfully created. Queue URL http://localhost:4566/000000000000/hello\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs6.png\" alt=\"bundle exec shoryuken で helloキュー作成 図\" width=\"561\" height=\"295\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n`bundle exec shoryuken` は、shoryuken コマンドをスタンドアローンで起動しています。ただ、<span style=\"color: red;\">このままの場合、本物の AWS が対象になるため、`--endpoint=http://localhost:4566` オプションを指定して、LocalStack に向ける必要があります。</span>\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;\"><strong><code>bundle exec shoryuken</code> は、<code>config/initializers/shoryuken.rb</code> の設定は反映されません。環境変数＋ <code>--endpoint</code> オプションで切り替えが必要です。</strong></span></p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 bundle exec 】</p>\n<p>今いるプロジェクトの gem でコマンドを起動します。</p>\n</blockquote>\n\n<br />\n\nキューイングバックエンドとして、shoryuken を設定します。\n\n```shellsession\n$ vi config/application.rb\n```\n\n```ruby\nrequire_relative \"boot\"\n\nrequire \"rails/all\"\n\n# Require the gems listed in Gemfile, including any gems\n# you've limited to :test, :development, or :production.\nBundler.require(*Rails.groups)\n\nmodule SqsApp\n  class Application < Rails::Application\n    # Initialize configuration defaults for originally generated Rails version.\n    config.load_defaults 7.0\n    config.active_job.queue_adapter = :shoryuken\n\n    # Configuration for the application, engines, and railties goes here.\n    #\n    # These settings can be overridden in specific environments using the files\n    # in config/environments, which are processed later.\n    #\n    # config.time_zone = \"Central Time (US & Canada)\"\n    # config.eager_load_paths << Rails.root.join(\"extras\")\n  end\nend\n```\n\n`config.active_job.queue_adapter = :shoryuken` の１行を追加します。\n\n<br />\n\nshoryuken を起動して、キューをポーリングします。\nこのとき、<span style=\"color: red;\">`config/initializers/shoryuken.rb` のサーバー側設定 `Shoryuken.configure_server do |config|` が有効</span>になります。\n\n```shellsession\n$ bundle exec shoryuken -q hello -R\n```\n\n`-q` ： キューを指定します。  \n`-R` ： Rails で動作することを伝えます。\n\n<br />\n\n**（別コンソール起動）**\n\nHelloJob ジョブをキューに入れます。\n\n<br />\n\n`HelloJob.perform_later('Ken')`を入力します。  \nこのとき、<span style=\"color: red;\">`config/initializers/shoryuken.rb` のクライアント側設定 `Shoryuken.configure_client do |config|` が有効</span>になります。\n\n```shellsession\n$ export AWS_ACCESS_KEY_ID=dummy\n$ export AWS_SECRET_ACCESS_KEY=dummy\n$ export AWS_REGION=us-east-1\n$ bundle exec rails c\nLoading development environment (Rails 7.0.3)\nirb(main):001:0> HelloJob.perform_later('Ken')\nEnqueued HelloJob (Job ID: 1fb25dae-1a43-4e55-9a51-a1df1cfb8db3) to Shoryuken(hello) with arguments: \"Ken\"\n=>\n#<HelloJob:0x000055bb44d9dbe0\n @arguments=[\"Ken\"],\n @exception_executions={},\n @executions=0,\n @job_id=\"1fb25dae-1a43-4e55-9a51-a1df1cfb8db3\",\n @priority=nil,\n @queue_name=\"hello\",\n @sqs_send_message_parameters=\n  {:message_body=>\n    \"{\\\"job_class\\\":\\\"HelloJob\\\",\\\"job_id\\\":\\\"1fb25dae-1a43-4e55-9a51-a1df1cfb8db3\\\",\\\"provider_job_id\\\":null,\\\"queue_name\\\":\\\"hello\\\",\\\"priority\\\":null,\\\"arguments\\\":[\\\"Ken\\\"],\\\"executions\\\":0,\\\"exception_executions\\\":{},\\\"locale\\\":\\\"en\\\",\\\"timezone\\\":\\\"UTC\\\",\\\"enqueued_at\\\":\\\"2022-05-06T11:59:07Z\\\"}\",\n   :message_attributes=>{\"shoryuken_class\"=>{:string_value=>\"ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper\", :data_type=>\"String\"}}},\n @successfully_enqueued=true,\n @timezone=\"UTC\">\n```\n\n<br />\n\n<blockquote class=\"info\">\n<p>【 bundle exec rails c 】</p>\n<p>Railsコンソールを起動します。<code>bundle exec rails console</code>の略です。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 perform_later 】</p>\n<p>ジョブをキューに入れ、キューが空き次第ジョブを実行します。</p>\n</blockquote>\n\n<br />\n\nshoryuken を起動した端末に以下のような出力が有れば、成功です。（すぐに取り出されます。）\n\n```shellsession\n2022-05-06T12:49:34Z 3759 TID-c80 ActiveJob/HelloJob/hello/e064278e-b8c6-1e54-d9f8-7857361213ab INFO: started at 2022-05-06 21:49:34 +0900\nHello, Ken\n2022-05-06T12:49:34Z 3759 TID-c80 ActiveJob/HelloJob/hello/e064278e-b8c6-1e54-d9f8-7857361213ab INFO: completed in: 62.727776999999996 ms\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/sqs7.png\" alt=\"Shoryuken client server 図\" width=\"616\" height=\"295\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！\n\n<br />\n\n# エラーケースまとめ\n\n<span style=\"color: red;\"><strong>注意：全て今回の LocalStack 環境の話です。</strong></span>\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n# aws sqs list-queues\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`You must specify a region. You can also configure your region by running \"aws configure\".`</span>\n\n<br />\n\n**原因：**  \nprofile（`~/.aws/config`, `~/.aws/credentials`）が作成されていないか、作成した profile を指定していない。\n\n<br />\n\n**対処内容：**\n\nprofile を作成して、`--profile`オプションで指定する。\n\n```shellsession\n# aws configure --profile localstack\nAWS Access Key ID [None]: dummy\nAWS Secret Access Key [None]: dummy\nDefault region name [None]: us-east-1\nDefault output format [None]: json\n# aws sqs list-queues --profile localstack\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n# aws sqs list-queues --profile localstack\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`An error occurred (InvalidClientTokenId) when calling the ListQueues operation: The security token included in the request is invalid.`</span>\n\n<br />\n\n**原因：**  \n`--endpoint` オプションで、LocalStack のエンドポイント URL を指定していない。\n\n<br />\n\n**対処内容：**  \n`--endpoint` オプションで、LocalStack のエンドポイント URL を指定する。\n\n```shellsession\n# aws sqs list-queues \\\n  --endpoint-url http://localhost:4566 \\\n  --profile localstack\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n$ ruby -v\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`rbenv: ruby: command not found`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`` The `ruby' command exists in these Ruby versions: ``</span>  \n<span style=\"background-color: cornsilk; color: red;\">`  3.0.4`</span>\n\n<br />\n\n```shellsession\n$ bundle install\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`Command 'bundle' not found, but can be installed with:`</span>\n\n<br />\n\n**原因１：**  \nパスを通していない。\n\n<br />\n\n**対処内容１：**  \nパスを通す。\n\n```shellsession\n$ echo 'export PATH=\"$HOME/.rbenv/bin:$PATH\"' >> ~/.bashrc\n$ echo 'eval \"$(rbenv init - bash)\"' >> ~/.bashrc\n$ source ~/.bashrc\n```\n\n<br />\n\n**原因２：**  \nruby のバージョンを指定していない。\n\n<br />\n\n**対処内容２：**  \nruby のバージョンを指定する。\n\n```shellsession\n$ rbenv global 3.0.4\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n$ sudo apt install -y libsqlite3-dev\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`Gem::Ext::BuildError: ERROR: Failed to build gem native extension.`</span>  \n<span style=\"background-color: cornsilk; color: red;\">` current directory: /home/admin/.rbenv/versions/3.0.4/lib/ruby/gems/3.0.0/gems/sqlite3-1.4.2/ext/sqlite3`</span>\n\n<br />\n\n<span style=\"background-color: cornsilk; color: red;\">`An error occurred while installing sqlite3 (1.4.2), and Bundler cannot continue.`</span>\n\n<br />\n\n<span style=\"background-color: cornsilk; color: red;\">`Could not find gem 'sqlite3 (~> 1.4)' in locally installed gems.`</span>\n\n<br />\n\n**原因：**  \n`libsqlite3-dev` がインストールされていない。\n\n<br />\n\n**対処内容：**  \n`libsqlite3-dev` をインストール。\n\n```shellsession\n$ sudo apt install -y libsqlite3-dev\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n$ bundle exec shoryuken sqs create hello\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`bundler: failed to load command: shoryuken (/home/admin/sqs-app/vendor/bundle/ruby/3.0.0/bin/shoryuken)`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`` /home/admin/sqs-app/vendor/bundle/ruby/3.0.0/gems/aws-sdk-core-3.130.2/lib/aws-sdk-core/plugins/regional_endpoint.rb:87:in `after_initialize': No region was provided. Configure the `:region` option or export the region name to ENV['AWS_REGION'] (Aws::Errors::MissingRegionError) ``</span>\n\n<br />\n\n**原因：**  \n環境変数でリージョン等指定していない。\n\n<br />\n\n**対処内容：**  \n環境変数でリージョン等を指定。\n\n```shellsession\n$ export AWS_ACCESS_KEY_ID=dummy\n$ export AWS_SECRET_ACCESS_KEY=dummy\n$ export AWS_REGION=us-east-1\n$ bundle exec shoryuken sqs create hello --endpoint=http://localhost:4566\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n$ export AWS_ACCESS_KEY_ID=dummy\n$ export AWS_SECRET_ACCESS_KEY=dummy\n$ export AWS_REGION=us-east-1\n$ bundle exec shoryuken sqs create hello\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`bundler: failed to load command: shoryuken (/home/admin/sqs-app/vendor/bundle/ruby/3.0.0/bin/shoryuken)`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`` /home/admin/sqs-app/vendor/bundle/ruby/3.0.0/gems/aws-sdk-core-3.130.2/lib/aws-sdk-core/plugins/regional_endpoint.rb:87:in `after_initialize': No region was provided. Configure the `:region` option or export the region name to ENV['AWS_REGION'] (Aws::Errors::MissingRegionError) ``</span>\n\n<br />\n\n**原因：**  \n`--endpoint` オプションで、LocalStack のエンドポイント URL を指定していない。\n\n<br />\n\n**対処内容：**  \n`--endpoint` オプションで、LocalStack のエンドポイント URL を指定する。\n\n```shellsession\n$ export AWS_ACCESS_KEY_ID=dummy\n$ export AWS_SECRET_ACCESS_KEY=dummy\n$ export AWS_REGION=us-east-1\n$ bundle exec shoryuken sqs create hello --endpoint=http://localhost:4566\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n$ bundle exec shoryuken -q hello -R\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`bundler: failed to load command: shoryuken (/home/admin/sqs-app/vendor/bundle/ruby/3.0.0/bin/shoryuken)`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`` /home/admin/sqs-app/vendor/bundle/ruby/3.0.0/gems/aws-sdk-core-3.130.2/lib/seahorse/client/plugins/raise_response_errors.rb:17:in `call': The security token included in the request is invalid. (Aws::SQS::Errors::InvalidClientTokenId) ``</span>\n\n<br />\n\n**原因：**  \nLocalStack のエンドポイント URL を `config/initializers/shoryuken.rb` で指定していない。\n\n<br />\n\n**対処内容：**  \nLocalStack のエンドポイント URL を `config/initializers/shoryuken.rb` で指定する。\n\n```shellsession\n$ vi config/initializers/shoryuken.rb\n```\n\n```ruby\nShoryuken.configure_server do |config|\n  config.sqs_client = Aws::SQS::Client.new(\n    region: ENV[\"AWS_REGION\"],\n    access_key_id: ENV[\"AWS_ACCESS_KEY_ID\"],\n    secret_access_key: ENV[\"AWS_SECRET_ACCESS_KEY\"],\n    endpoint: 'http://localhost:4566',\n    verify_checksums: false\n  )\nend\n```\n\n※<span style=\"color: red;\">`Shoryuken.configure_server`</span> なのに注意。\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容：**\n\n```shellsession\n$ bundle exec rails c\n> HelloJob.perform_later('Ken')\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`Failed enqueuing HelloJob to Shoryuken(hello): Aws::SQS::Errors::InvalidClientTokenId (The security token included in the request is invalid.)`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`` /home/admin/sqs-app/vendor/bundle/ruby/3.0.0/gems/aws-sdk-core-3.130.2/lib/seahorse/client/plugins/raise_response_errors.rb:17:in `call': The security token included in the request is invalid. (Aws::SQS::Errors::InvalidClientTokenId) ``</span>\n\n<br />\n\n**原因：**  \nLocalStack のエンドポイント URL を `config/initializers/shoryuken.rb` で指定していない。\n\n<br />\n\n**対処内容：**  \nLocalStack のエンドポイント URL を `config/initializers/shoryuken.rb` で指定する。\n\n```shellsession\n$ vi config/initializers/shoryuken.rb\n```\n\n```ruby\nShoryuken.configure_client do |config|\n  config.sqs_client = Aws::SQS::Client.new(\n    region: ENV[\"AWS_REGION\"],\n    access_key_id: ENV[\"AWS_ACCESS_KEY_ID\"],\n    secret_access_key: ENV[\"AWS_SECRET_ACCESS_KEY\"],\n    endpoint: 'http://localhost:4566',\n    verify_checksums: false\n  )\nend\n```\n\n※<span style=\"color: red;\">`Shoryuken.configure_client`</span> なのに注意。\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n","description":"LocalStackのSQS機能を利用して、オフラインで、AWS SQS機能の動作確認をしました。さらに、Ruby on Rails ＋ キューイングバックエンドとして Shoryuken を使って、プログラムによるキューの出し入れを動作確認しました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"gnmmngcm-mg","createdAt":"2022-05-19T10:57:35.678Z","updatedAt":"2022-05-19T10:57:35.678Z","publishedAt":"2022-05-19T10:57:35.678Z","revisedAt":"2022-05-19T10:57:35.678Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/sqs-shoryuken/ITC_Engineering_Blog.png","alt":"LocalStackをAWS SQSにしてRuby on Rails Shoryukenを使ってみた","width":1200,"height":630}],"seo_authors":[]},{"id":"azure-swa","createdAt":"2022-07-31T11:46:14.577Z","updatedAt":"2022-07-31T11:46:14.577Z","publishedAt":"2022-07-31T11:46:14.577Z","revisedAt":"2022-07-31T11:46:14.577Z","title":"【swa】Azure Static Web Appsをローカル環境でデバッグ","category":{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},"topics":[{"id":"5h4qqgtwop5j","createdAt":"2022-06-29T06:12:41.058Z","updatedAt":"2022-06-29T06:12:41.058Z","publishedAt":"2022-06-29T06:12:41.058Z","revisedAt":"2022-06-29T06:12:41.058Z","topics":"Azure","logo":"/logos/Azure.png","needs_title":true},{"id":"8hcq78trdqsy","createdAt":"2021-08-16T14:36:04.888Z","updatedAt":"2021-08-31T12:02:33.146Z","publishedAt":"2021-08-16T14:36:04.888Z","revisedAt":"2021-08-31T12:02:33.146Z","topics":"VSCode","logo":"/logos/vscode.png","needs_title":true},{"id":"xego85dtzyu","createdAt":"2021-06-03T13:50:33.576Z","updatedAt":"2021-08-31T12:04:26.367Z","publishedAt":"2021-06-03T13:50:33.576Z","revisedAt":"2021-08-31T12:04:26.367Z","topics":"React","logo":"/logos/React.png","needs_title":false},{"id":"xnhxgx1v0","createdAt":"2022-04-08T10:53:39.471Z","updatedAt":"2022-04-08T10:53:39.471Z","publishedAt":"2022-04-08T10:53:39.471Z","revisedAt":"2022-04-08T10:53:39.471Z","topics":"TypeScript","logo":"/logos/TypeScript.png","needs_title":true},{"id":"91zw54wj7d","createdAt":"2021-06-05T07:05:37.594Z","updatedAt":"2021-08-31T12:03:57.429Z","publishedAt":"2021-06-05T07:05:37.594Z","revisedAt":"2021-08-31T12:03:57.429Z","topics":"Python","logo":"/logos/python.png","needs_title":false}],"content":"# はじめに\n\n公式ドキュメント <a href=\"https://docs.microsoft.com/ja-jp/azure/static-web-apps/local-development\" target=\"_blank\">Azure Static Web Apps 用にローカル開発環境を設定する</a> を参考に、Azure Static Web Apps をローカル環境でデバッグできるまでの手順をやってみましたので、以下にまとめます。  \nローカル環境とは、インターネットに接続しないオフライン環境のことです。  \nAzure のアカウントを持っていなくても、動作確認できます。\n\n<br />\n\n以下の図のように、  \nフロントエンド：React & TypeScript  \nバックエンド：Python (Azure Functions)  \nの構成を前提とします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/swa.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/swa.png\" alt=\"swa 構成 図\" width=\"813\" height=\"636\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>バックエンドは、<code>api/</code> ディレクトリに入れて、Azure Static Web Apps にデプロイすると、API として構築されます。個別に Azure Functions にデプロイするわけではありません。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>承認と認証のエミュレーションは、今回実施していません。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>インストール中は、インターネット接続が必要です。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS</code></p>\n<p>　<code>node 14.20.0</code></p>\n<p>　<code>npm 6.14.17</code></p>\n<p>　<code>React 18.2.0</code></p>\n<p>　<code>eslint 8.20.0</code></p>\n<p>　<code>prettier 2.7.1</code></p>\n<p>　<code>Python 3.8.10</code></p>\n<p>　<code>black 22.6.0</code></p>\n<p>　<code>flake8 4.0.1</code></p>\n<p><code>Windows 10 Pro x64</code></p>\n<p>　<code>拡張機能 Azure Functions 1.7.4</code></p>\n</blockquote>\n\n<br />\n\n# 基本環境\n\n<span style=\"color: red;\"><strong>別記事</strong></span>  \n<span style=\"color: red;\"><strong><a href=\"https://itc-engineering-blog.netlify.app/blogs/eslint-prettier\" target=\"_blank\">React TypeScript ESLint Prettier VSCode のプロジェクト作成</a></strong></span>  \n<span style=\"color: red;\"><strong><a href=\"https://itc-engineering-blog.netlify.app/blogs/black-flake8\" target=\"_blank\">VS Code で Python のコードフォーマッター(black)、リンター(flake8)をセットアップ</a></strong></span>  \n<span style=\"color: red;\"><strong>を実施済みとします。</strong></span>\n\n<br />\n\nこれにより、以下の環境をスタート地点とします。  \n・node, npm インストール済み  \n・Ubuntu - VS Code SSH 接続で開発中  \n・`sample-project` という React & TypeScript のプロジェクトを作成済み  \n・ESLint Prettier をセットアップ済み  \n・python3, pip3 インストール済み  \n・VS Code の Python 拡張機能インストール済み  \n・VS Code で Python のコードフォーマッター(black)、リンター(flake8)をセットアップ済み  \n・Python（バックエンド）の実装は、まだ何もしていない\n\n```shellsession\n$ which black\n/home/admin/.local/bin/black\n$ which flake8\n/home/admin/.local/bin/flake8\n```\n\n<br />\n\nsettings.json は、以下の状態とします。\n\n```shellsession\n$ cat .vscode-server/data/Machine/settings.json\n```\n\n```json\n{\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.formatOnSave\": true,\n  \"python.formatting.provider\": \"black\",\n  \"python.linting.pylintEnabled\": false,\n  \"python.linting.flake8Enabled\": true,\n  \"python.linting.flake8Args\": [\n    \"--max-line-length=88\",\n    \"--ignore=E203,W503,W504\"\n  ]\n}\n```\n\n<br />\n\n```shellsession\n$ cd sample-project\n$ npm start\n```\n\nで、  \n何の変哲もないこの状態です。  \n↓  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/react.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/react.png\" alt=\"React App プロジェクト作成直後\" width=\"865\" height=\"630\" loading=\"lazy\"></a>\n\n<br />\n\n# Functions API 作成\n\n`api/` にプログラムを作成すると、バックエンド側（API）になるのですが、ちゃんと動くようになるには、作法が有ります。\n\n<br />\n\nMicrosoft 公式ドキュメント <a href=\"https://docs.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-python\" target=\"_blank\">クイックスタート: Visual Studio Code と Python を使用して Azure に関数を作成する</a> の手順で、その作法に従ったものが自動的に作成されますので、この手順でいきます。\n\n<blockquote class=\"warn\">\n<p>公式ドキュメントは、Azure Functions のサーバーレス環境にデプロイしていますが、今回、その話では無いため、そこまでは実施しないです。</p>\n<p>また、Azure Functions にデプロイして、Azure Static Web Apps に持ち込む話でもないです。</p>\n</blockquote>\n\n<br />\n\nサーバーに **Azure Functions Core Tools** をインストールします。\n\n```shellsession\n$ cd ~\n$ curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg\n$ sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg\n$ sudo sh -c 'echo \"deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main\" > /etc/apt/sources.list.d/dotnetdev.list'\n$ sudo apt update\n$ sudo apt install azure-functions-core-tools-4\n```\n\n<br />\n\nVS Code に  \n**Azure Functions 拡張機能**  \nをインストールします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image1.png\" alt=\"Azure Functions 拡張機能 インストール\" width=\"456\" height=\"380\" loading=\"lazy\"></a>\n\n<br />\n\n`api/` ディレクトリを作成します。`sample-project/api` です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image2.png\" alt=\"sample-project/api 作成\" width=\"432\" height=\"209\" loading=\"lazy\"></a>\n\n<br />\n\nフォルダーを開く... → `sample-project/api` を開きます。\n\n<blockquote class=\"info\">\n<p><code>sample-project</code> は、API のディレクトリでは無いため、<code>sample-project/api</code> を API のトップディレクトリとし、以降の作業に進めます。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image3.png\" alt=\"フォルダーを開く...\" width=\"439\" height=\"274\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image4.png\" alt=\"sample-project/api を開く\" width=\"971\" height=\"149\" loading=\"lazy\"></a>\n\n<br />\n\n`python3.8-venv` をインストールしておきます。\n\n```shellsession\n$ sudo apt install python3.8-venv\n```\n\nここで、python3.8-venv をインストールしておかないと、この後の手順で以下のエラーになります。\n\n```sh\n9:35:13 PM: Running command: \"python3 -m venv .venv\"...\nThe virtual environment was not created successfully because ensurepip is not\navailable.  On Debian/Ubuntu systems, you need to install the python3-venv\npackage using the following command.\n\n    apt install python3.8-venv\n\nYou may need to use sudo with that command.  After installing the python3-venv\npackage, recreate your virtual environment.\n\nFailing command: ['/home/admin/sample-project/api/.venv/bin/python3', '-Im', 'ensurepip', '--upgrade', '--default-pip']\n\n9:35:14 PM: Error: Failed to run \"python3\" command. Check output window for more details.\n```\n\n<br />\n\nAzure マークが現れるので、クリックして、**WORKSPACE** Local の ＋ ボタンをクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image5.png\" alt=\"WORKSPACE ＋ ボタンをクリック\" width=\"467\" height=\"456\" loading=\"lazy\"></a>\n\n<br />\n\n**Create Function** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image6.png\" alt=\"Create Function をクリック\" width=\"523\" height=\"446\" loading=\"lazy\"></a>\n\n<br />\n\n**Yes** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image7.png\" alt=\"Yes をクリック\" width=\"402\" height=\"116\" loading=\"lazy\"></a>\n\n<br />\n\n**Python** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image8.png\" alt=\"Python をクリック\" width=\"971\" height=\"320\" loading=\"lazy\"></a>\n\n<br />\n\n**Python3** 3.8.10 をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image9.png\" alt=\"Python3 3.8.10 をクリック\" width=\"968\" height=\"205\" loading=\"lazy\"></a>\n\n<br />\n\n**HTTP trigger** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image10.png\" alt=\"HTTP trigger をクリック\" width=\"971\" height=\"387\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>何がきっかけで起動する API なのかを選択します。今回、HTTP GET で起動する API を作成するため、HTTP trigger になります。</p>\n</blockquote>\n\n<br />\n\n**HttpTrigger1** を入力してエンターキーを押します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image11.png\" alt=\"HttpTrigger1 を入力してエンターキー\" width=\"964\" height=\"152\" loading=\"lazy\"></a>\n\n<br />\n\n**Anonymous** をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image12.png\" alt=\"Anonymous をクリック\" width=\"967\" height=\"205\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>【 承認レベルについて 】</p>\n<p><code>Anonymous</code> 以外を選択すると、API キーが必要になるのですが、Azure Static Web Apps に API キーを設定する方法が見つからなかったため、おそらく、<code>Anonymous</code> にしないといけないと思われます。（API キーを設定する方法が有るかもしれませんが、とにかく今回は、必要無しとして進めます。）</p>\n</blockquote>\n\n<br />\n\n`__init__.py` 他、基本的な Azure Function API の実装と設定が作成されます。\n\n<br />\n\nここで、flake8 が venv にインストールされていないため、VS Code がインストールを促してきます。  \nインストールします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image13.png\" alt=\"flake8 インストール\" width=\"563\" height=\"159\" loading=\"lazy\"></a>\n\n<br />\n\nまた、`__init__.py` を保存すると、black のインストールも促されるため、これもインストールします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image14.png\" alt=\"black インストール\" width=\"562\" height=\"149\" loading=\"lazy\"></a>\n\n<br />\n\n`__init__.py` のままだと１行が長すぎてリンターに引っかかっていますので、修正します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image15.png\" alt=\"１行が長すぎてリンターエラー　修正前\" width=\"1478\" height=\"145\" loading=\"lazy\"></a>\n\n↓\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image16.png\" alt=\"１行が長すぎてリンターエラー　修正後\" width=\"722\" height=\"165\" loading=\"lazy\"></a>\n\n<br />\n\n# Functions API デバッグ\n\nFunctions API 単独でデバッグします。\n\n<br />\n\n`__init__.py` を開いているところで、F5 キーを押します。\n\n<blockquote class=\"info\">\n<p>Azure Functions Core Tools がインストールされていない場合、ここで以下のエラーになります。</p>\n<p><span style=\"background-color: cornsilk; color: red;\">You must have the Azure Functions Core Tools installed to debug your local functions.</span></p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image17.png\" alt=\"Azure Functions Core Tools がインストールされていない場合 エラー\" width=\"515\" height=\"116\" loading=\"lazy\"></a>\n</blockquote>\n\n<br />\n\nAPI が起動して、デバッグできる状態になりました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image18.png\" alt=\"API が起動して、デバッグできる状態\" width=\"953\" height=\"273\" loading=\"lazy\"></a>\n\n<br />\n\n`name` パラメータ有り無しで、レスポンスが変わります。レスポンスはただの文字列です。\n\n```shellsession\n$ curl http://localhost:7071/api/HttpTrigger1\nThis HTTP triggered function executed successfully.Pass a name in the query string or in the request body for a personalized response.\n$ curl http://localhost:7071/api/HttpTrigger1?name=John\nHello, John. This HTTP triggered function executed successfully.\n```\n\n<br />\n\nブレークポイントで止められます。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image19.png\" alt=\"API ブレークポイント\" width=\"831\" height=\"165\" loading=\"lazy\"></a>\n\n<br />\n\n# swa インストール\n\nFunctions API 単独の動作確認が完了しましたので、上のディレクトリ `sample-project` を開きなおします。  \n以下の警告が有る場合、**`Don't warn again`** を選択します。  \n<span style=\"background-color: cornsilk; color: red;\">`Detected an Azure Functions Project in folder \"sample-project\" that may have been created outside of VS Code.Initialize for optimal use with VS Code?`</span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image20.png\" alt=\"Don't warn again\" width=\"566\" height=\"211\" loading=\"lazy\"></a>\n\nここで、`Yes` を選択すると、  \n`sample-project/.venv/`  \n`sample-project/.vscode/`  \n`sample-project/.funcignore`  \nが親フォルダにも反映されて、API 仕様の構成になります。\n\n<br />\n\n以下のようにして、`swa` コマンド（`@azure/static-web-apps-cli`）をインストールします。\n\n```shellsession\n$ sudo npm install -g @azure/static-web-apps-cli --unsafe-perm\n$ npm run build\n$ swa start\n```\n\n<blockquote class=\"warn\">\n<p><span style=\"background-color: cornsilk; color: red;\">prebuild-install warn install EACCES: permission denied, access '/root/.npm'</span></p>\n<p>のエラーになったため、<code>--unsafe-perm</code> オプションを付けました。</p>\n<p><a href=\"https://docs.microsoft.com/ja-jp/azure/static-web-apps/local-development\" target=\"_blank\">MS公式ドキュメント</a>の場合、<code>azure-functions-core-tools</code> もインストールしていますが、</p>\n<p>既にインストール済みのため、スキップしています。</p>\n</blockquote>\n\n<br />\n\nビルドして、起動してみます。\n\n```shellsession\n$ npm run build\n$ swa start ./build --api-location ./api\n```\n\n<blockquote class=\"info\">\n<p><code>npm run build</code> でビルドすると、<code>index.html</code> が <code>./build</code> にできるため、<code>./build</code> を明示しています。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image21.png\" alt=\"swa start ./build --api-location ./api\" width=\"1070\" height=\"359\" loading=\"lazy\"></a>\n\n<br />\n\n起動ヨシ！\n\n<br />\n\nAPI は、7071 ポートで起動しますが、以下の図のように 4280 ポートで起動できます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image22.png\" alt=\"4280 ポートで起動 図\" width=\"813\" height=\"561\" loading=\"lazy\"></a>\n\n```shellsession\n$ curl http://localhost:7071/api/HttpTrigger1\nThis HTTP triggered function executed successfully.Pass a name in the query string or in the request body for a personalized response.\n$ curl http://localhost:4280/api/HttpTrigger1\nThis HTTP triggered function executed successfully.Pass a name in the query string or in the request body for a personalized response.\n```\n\n<br />\n\n# API 呼び出しボタン追加\n\n画面から API を呼び出すボタンを追加します。  \n<span style=\"color: red;\"><strong>URL は、自分の API のため、`/api/HttpTrigger1` になります。</strong></span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image23.png\" alt=\"画面から API を呼び出すボタンを追加\" width=\"478\" height=\"453\" loading=\"lazy\"></a>\n\n<br />\n\n`axios` をインストールして、`App.tsx` を以下の内容に書き換えます。\n\n```\n$ npm install axios\n```\n\n```tsx:App.tsx\nimport React, { useRef, useState } from 'react';\nimport axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';\nimport logo from './logo.svg';\nimport './App.css';\n\nconst options: AxiosRequestConfig = {\n  url: `/api/HttpTrigger1`,\n  method: 'GET',\n};\nfunction App() {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [response, setResponse] = useState('');\n  const handleAPIGet = () => {\n    axios({\n      ...options,\n      params: {\n        name: inputRef.current?.value || '',\n      },\n    })\n      .then((res: AxiosResponse<string>) => {\n        const { data } = res;\n        setResponse(data);\n      })\n      .catch((e: AxiosError<{ error: string }>) => {\n        console.log(e.message); // eslint-disable-line no-console\n      });\n  };\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.tsx</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n        <div>\n          <input ref={inputRef} type=\"text\" name=\"name\" />\n          <button type=\"submit\" onClick={handleAPIGet}>\n            API Get!\n          </button>\n          <p>Response : {response}</p>\n        </div>\n      </header>\n    </div>\n  );\n}\n\nexport default App;\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image24.png\" alt=\"ソースコード diff\" width=\"1627\" height=\"1203\" loading=\"lazy\"></a>\n\n<br />\n\n# swa デバッグ\n\nさて、いよいよ、swa で起動した状態でデバッグします。  \nフロントエンドを 3000 ポートで起動します。\n\n```shellsession\n$ cd ~/sample-project\n$ npm start\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image25.png\" alt=\"フロントエンドを 3000 ポートで起動\" width=\"725\" height=\"323\" loading=\"lazy\"></a>\n\n<br />\n\nバックエンドを 7071 ポートで起動します。  \nこれは、API 単独でデバッグした時と同じく、  \n`__init__.py` を開いているところで、F5 キーを押します。  \n（<span style=\"color: red;\"><strong>注意：`sample-project/api` を別のウィンドウで開いた状態です。</strong></span>）\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image26.png\" alt=\"バックエンドを 7071 ポートで起動\" width=\"862\" height=\"285\" loading=\"lazy\"></a>\n\n<br />\n\n`sample-project` のウィンドウに戻って、（teraterm などでも良いですが。）別セッションで、swa を起動します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image27.png\" alt=\"別セッションで、swa を起動\" width=\"1131\" height=\"322\" loading=\"lazy\"></a>\n\n```\n$ cd ~/sample-project\n$ swa start http://localhost:3000 --api-location http://localhost:7071\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image28.png\" alt=\"swa起動１\" width=\"1085\" height=\"127\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image29.png\" alt=\"swa起動２\" width=\"1078\" height=\"383\" loading=\"lazy\"></a>\n\n<br />\n\n`App.tsx` を開いているところで、F5 キーを押します。\n\nデバッガー を Chrome とします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image30.png\" alt=\"デバッガー を Chrome\" width=\"872\" height=\"225\" loading=\"lazy\"></a>\n\n<br />\n\nポートを 4280 にします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image31.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image31.png\" alt=\"ポートを 4280\" width=\"641\" height=\"372\" loading=\"lazy\"></a>\n\n<br />\n\nフロントエンド側：ボタンクリック  \nバックエンド側：リクエスト受信  \nのところにブレークポイントを貼って止められるか、試します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image32.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image32.png\" alt=\"フロントエンド バックエンド ブレークポイント１\" width=\"2039\" height=\"1110\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image33.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image33.png\" alt=\"フロントエンド バックエンド ブレークポイント２\" width=\"2037\" height=\"1107\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image34.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/image34.png\" alt=\"フロントエンド バックエンド ブレークポイント３\" width=\"2042\" height=\"1111\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ！！\n","description":"swaコマンドを使って、Azure Static Web Appsをローカル環境でデバッグしてみました。その手順をまとめました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"k20gu7two","createdAt":"2022-07-31T11:43:39.360Z","updatedAt":"2022-07-31T11:43:39.360Z","publishedAt":"2022-07-31T11:43:39.360Z","revisedAt":"2022-07-31T11:43:39.360Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/azure-swa/ITC_Engineering_Blog.png","alt":"【swa】Azure Static Web Appsをローカル環境でデバッグ","width":1200,"height":630}],"seo_authors":[]},{"id":"ejbca-build-install","createdAt":"2024-02-04T12:21:29.307Z","updatedAt":"2024-02-04T13:30:15.081Z","publishedAt":"2024-02-04T12:21:29.307Z","revisedAt":"2024-02-04T13:30:15.081Z","title":"EJBCA(PKIおよび証明書管理アプリ)をビルドしてインストールしてみた","category":{"id":"ik0y39076","createdAt":"2024-02-04T12:20:33.135Z","updatedAt":"2024-02-04T12:20:33.135Z","publishedAt":"2024-02-04T12:20:33.135Z","revisedAt":"2024-02-04T12:20:33.135Z","topics":"Java","logo":"/logos/Java.png","needs_title":false},"topics":[{"id":"ik0y39076","createdAt":"2024-02-04T12:20:33.135Z","updatedAt":"2024-02-04T12:20:33.135Z","publishedAt":"2024-02-04T12:20:33.135Z","revisedAt":"2024-02-04T12:20:33.135Z","topics":"Java","logo":"/logos/Java.png","needs_title":false},{"id":"umqsrvfrv7","createdAt":"2021-08-29T10:56:17.442Z","updatedAt":"2021-08-31T12:02:21.915Z","publishedAt":"2021-08-29T10:56:17.442Z","revisedAt":"2021-08-31T12:02:21.915Z","topics":"Unix/Linux","logo":"/logos/Linux.png","needs_title":true},{"id":"bcluojl_o","createdAt":"2021-02-18T07:36:53.394Z","updatedAt":"2021-08-31T12:08:52.380Z","publishedAt":"2021-02-18T07:36:53.394Z","revisedAt":"2021-08-31T12:08:52.380Z","topics":"ubuntu","logo":"/logos/ubuntu.png","needs_title":false}],"content":"# はじめに\n\nEJBCA（EJBCA Community）というオープンソースの公開鍵基盤（PKI）および認証局（CA）ソフトウェアをビルドしてインストールしました。  \n本記事では、その手順を紹介していきたいと思います。\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image7.png\" alt=\"EJBCA Web 管理画面\" width=\"879\" height=\"599\" loading=\"lazy\"></a>\n\n<br />\n\n公式 Docker コンテナ がありますので、Docker（Docker Compose）を使った方が良いと思いますが、あえてのビルドです。  \n公式ビルド手順は、<a href=\"https://doc.primekey.com/ejbca/ejbca-installation\" target=\"_blank\">`https://doc.primekey.com/ejbca/ejbca-installation`</a> にあり、今回の記事では、アプリケーションサーバー、DB など、いろいろな選択肢の中の一つです。動けばヨシで、細かい設定、チューニングは極力無しです。  \nこの記事では、インストールして Web 管理画面（`/ejbca/adminweb/`）が表示されたら終わりです。\n\n<blockquote class=\"info\">\n<p>Java ラー以外の方でも進められるようになるべく意味を紐解きながら進めます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>Docker のやり方は、こちらにあります。EJBCA を動かすだけなら、「Docker って何？」から始まらない限り絶対こっちの方が良いと思います。</p>\n<p><a href=\"https://doc.primekey.com/ejbca/tutorials-and-guides/tutorial-start-out-with-ejbca-docker-container\" target=\"_blank\">https://doc.primekey.com/ejbca/tutorials-and-guides/tutorial-start-out-with-ejbca-docker-container</a></p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>以下の環境です。少しでも環境が異なると、途中で詰まるかもしれません。</p>\n<p>Ubuntu Desktop 22.04.3 LTS</p>\n<p>　 EJBCA 8.2.0.1 Community</p>\n<p>　 openjdk version \"11.0.21\" 2023-10-17</p>\n<p>　 wildfly 26.0.0.Final</p>\n<p>　 mysql Ver 15.1 Distrib 10.6.16-MariaDB</p>\n<p>　 Apache Ant(TM) version 1.10.12</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 EJBCA 】</p>\n<p>EJBCA は、エンタープライズ向けの公開鍵基盤（PKI）認証局のツールです。証明書の発行、失効、鍵管理などを行うことができます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 PKI 】</p>\n<p>PKI（Public Key Infrastructure、公開鍵基盤）は、公開鍵暗号やデジタル署名をインターネット上で安全に運用するための社会的基盤です。</p>\n<p>これにより、公開鍵の配布、認証、鍵の管理などが行われます。具体的には、信頼できる第三者（認証局）がデジタル証明書を発行し、その証明書を通じて公開鍵が安全に配布されます。</p>\n<p>これにより、通信の安全性が保証されます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 CA 】</p>\n<p>CA（Certificate Authority）は、インターネット上で接続者の身分を証明する電子証明書の発行と管理を行う認証局のことです。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。</strong></span></p>\n</blockquote>\n\n<br />\n\n# ホスト名設定\n\n<blockquote class=\"warn\">\n<p>root 権限で作業します。</p>\n</blockquote>\n\nとりあえず、ホスト名を `ejbcatest` と最初に決めておきます。（この作業は、省略しても良いです。）\n\n```shellsession\n$ su\n# hostnamectl set-hostname ejbcatest\n# vi /etc/hosts\n127.0.0.1\tejbcatest\n192.168.11.6\tejbcatest\n```\n\n<br />\n\n# MariaDB インストール\n\nMariaDB をインストールします。PostgreSQL など、その他の選択肢もありますが、今回は、MariaDB です。\n\n<blockquote class=\"info\">\n<p>【 MariaDB 】</p>\n<p>MariaDB は、MySQL から派生したオープンソースのリレーショナルデータベース管理システム（RDBMS）です。MySQL と高い互換性を持ちつつ、新機能の追加やソースコードの改善が行われています。</p>\n</blockquote>\n\n```shellsession\n# apt update\n# apt -y upgrade\n# apt install mariadb-server -y\n# mysql_secure_installation\nEnter current password for root (enter for none): エンター\nSwitch to unix_socket authentication [Y/n] n\nChange the root password? [Y/n] n\nRemove anonymous users? [Y/n] エンター\nDisallow root login remotely? [Y/n] エンター\nRemove test database and access to it? [Y/n] エンター\nReload privilege tables now? [Y/n] エンター\n# mysql --version\nmysql  Ver 15.1 Distrib 10.6.16-MariaDB, for debian-linux-gnu (x86_64) using  EditLine wrapper\n```\n\n<br />\n\n# ejbca ユーザー作成\n\n公式ドキュメント（<a href=\"https://doc.primekey.com/ejbca/ejbca-installation/creating-the-database\" target=\"_blank\">`https://doc.primekey.com/ejbca/ejbca-installation/creating-the-database`</a>）に以下の記述があるため、それに従います。  \n`\"MySQL バージョン 8 以降、GRANT コマンドを使用してユーザーを暗黙的に作成することはできません。\"`  \n`\"MySQL バージョン 8 以降を実行している場合は、  次の例に従って最初にejbcaユーザーを作成します。\"`\n\n```shellsession\n# mysql -u root -p\nEnter password: エンター（空のパスワード）\nMariaDB [(none)]> CREATE USER 'ejbca'@'localhost' IDENTIFIED BY 'ejbca';\nMariaDB [(none)]> GRANT ALL PRIVILEGES ON ejbca.* TO 'ejbca'@'localhost';\nMariaDB [(none)]> FLUSH PRIVILEGES;\nMariaDB [(none)]> exit;\n```\n\n**１． `mysql -u root -p`**\n\n<div style=\"padding-left: 1em;\">MariaDB サーバーに root ユーザーとしてログインします。<br /><code>-p</code> オプションはパスワードを求めるためのものです。</div>\n\n**２． `CREATE USER 'ejbca'@'localhost' IDENTIFIED BY 'ejbca';`**\n\n<div style=\"padding-left: 1em;\"><code>ejbca</code> という名前の新しいユーザーを作成します。このユーザーは <code>localhost</code> からの接続のみを許可し、パスワードは <code>ejbca</code> に設定されます。</div>\n\n**３． `GRANT ALL PRIVILEGES ON ejbca.* TO 'ejbca'@'localhost';`**\n\n<div style=\"padding-left: 1em;\">新しく作成した <code>ejbca</code> ユーザーに、<code>ejbca</code> データベースの全てのテーブルに対する全権限を付与します。</div>\n\n**４．`FLUSH PRIVILEGES;`**\n\n<div style=\"padding-left: 1em;\">変更した権限設定を即時反映させます。</div>\n\n<br />\n\n```shellsession\n# mysql -u root -p\nEnter password:エンター\nMariaDB [(none)]> CREATE DATABASE ejbca CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\nMariaDB [(none)]> GRANT ALL PRIVILEGES ON ejbca.* TO 'ejbca'@'localhost' IDENTIFIED BY 'ejbca';\nMariaDB [(none)]> exit;\n```\n\n**１． `CREATE DATABASE ejbca CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`**\n\n<div style=\"padding-left: 1em;\"><code>ejbca</code> という名前の新しいデータベースを作成します。文字セットと照合順序はそれぞれ <code>utf8mb4</code> と <code>utf8mb4_unicode_ci</code> に設定されます。</div>\n\n**２． `GRANT ALL PRIVILEGES ON ejbca.* TO 'ejbca'@'localhost' IDENTIFIED BY 'ejbca';`**\n\n<div style=\"padding-left: 1em;\"><code>ejbca</code> ユーザーに、新しく作成した <code>ejbca</code> データベースの全てのテーブルに対する全権限を付与します。このユーザーは <code>localhost</code> からの接続のみを許可し、パスワードは <code>ejbca</code> に設定されます。</div>\n\n<br />\n\n# wildfly26 インストール\n\nwildfly26 をインストールします。  \n公式ドキュメントは、こちらです。  \n<a href=\"https://doc.primekey.com/ejbca/ejbca-installation/application-servers/wildfly-26\" target=\"_blank\">`https://doc.primekey.com/ejbca/ejbca-installation/application-servers/wildfly-26`</a>\n\n<br />\n\nZIP パッケージを使用してインストールします。  \nインストール先は、/opt/wildfly です。\n\n<br />\n\n```shellsession\n# wget https://github.com/wildfly/wildfly/releases/download/26.0.0.Final/wildfly-26.0.0.Final.zip -O /home/admin/wildfly-26.0.0.Final.zip\n# unzip -q /home/admin/wildfly-26.0.0.Final.zip -d /opt/\n# ln -snf /opt/wildfly-26.0.0.Final /opt/wildfly\n```\n\n<blockquote class=\"info\">\n<p>【 WildFly 】</p>\n<p>WildFly は、Java EE 準拠のオープンソースアプリケーションサーバで、高速、軽量、高機能、柔軟な特性を持っています。元々は「JBoss Application Server」という名前でしたが、現在は「WildFly」に改名されています。</p>\n</blockquote>\n\n<br />\n\nRESTEasy-Crypto を削除します。\n\n```shellsession\n# sed -i '/.*org.jboss.resteasy.resteasy-crypto.*/d' /opt/wildfly/modules/system/layers/base/org/jboss/as/jaxrs/main/module.xml\n# rm -rf /opt/wildfly/modules/system/layers/base/org/jboss/resteasy/resteasy-crypto/\n```\n\n<blockquote class=\"info\">\n<p>【 RESTEasy 】</p>\n<p>RESTEasy は、JBoss Enterprise Application Platform（EAP）の一部です。JBoss が提供するフレームワークで、Java で RESTful Web サービスを簡単に作成できるようにするものです。</p>\n</blockquote>\n\n<br />\n\nカスタム構成の作成を行います。\n\n```shellsession\n# cp -p /opt/wildfly/bin/standalone.conf /opt/wildfly/bin/standalone.conf.org\n# sed -i -e 's/{{ HEAP_SIZE }}/2048/g' /opt/wildfly/bin/standalone.conf\n# sed -i -e \"s/{{ TX_NODE_ID }}/$(od -A n -t d -N 1 /dev/urandom | tr -d ' ')/g\" /opt/wildfly/bin/standalone.conf\n```\n\n**１． `sed -i -e 's/{{ HEAP_SIZE }}/2048/g' /opt/wildfly/bin/standalone.conf`**\n\n<div style=\"padding-left: 1em;\">standalone.conf ファイル内の <code>{{ HEAP_SIZE }}</code> という文字列を 2048 に置換します。これは、Java のヒープサイズを設定するためのもので、通常は Java アプリケーションのパフォーマンスを調整するために使用されます。</div>\n\n**２． `sed -i -e \"s/{{ TX_NODE_ID }}/$(od -A n -t d -N 1 /dev/urandom | tr -d ' ')/g\" /opt/wildfly/bin/standalone.conf`**\n\n<div style=\"padding-left: 1em;\">standalone.conf ファイル内の <code>{{ TX_NODE_ID }}</code> という文字列をランダムな数値に置換します。この数値は /dev/urandom デバイスから生成され、トランザクションノード ID を一意に識別するために使用されます。</div>\n\n<br />\n\nWildFly をサービスとして構成します。\n\n```shellsession\n# cp /opt/wildfly/docs/contrib/scripts/systemd/launch.sh /opt/wildfly/bin\n# cp /opt/wildfly/docs/contrib/scripts/systemd/wildfly.service /etc/systemd/system\n# mkdir /etc/wildfly\n# cp /opt/wildfly/docs/contrib/scripts/systemd/wildfly.conf /etc/wildfly\n# systemctl daemon-reload\n# useradd -r -s /bin/false wildfly\n# chown -R wildfly:wildfly /opt/wildfly-26.0.0.Final/\n```\n\n<br />\n\nWildFly を開始します。\n\n```shellsession\n# systemctl start wildfly\n```\n\n<br />\n\nElytron 認証情報ストアを作成します。\n\n```shellsession\n# echo '#!/bin/sh' > /usr/bin/wildfly_pass\n# echo \"echo '$(openssl rand -base64 24)'\" >> /usr/bin/wildfly_pass\n# chown wildfly:wildfly /usr/bin/wildfly_pass\n# chmod 700 /usr/bin/wildfly_pass\n```\n\n<blockquote class=\"info\">\n<p>【 Elytron 認証情報ストア 】</p>\n<p>Elytron 認証情報ストアは、JBoss EAP の Elytron サブシステムで導入されたセキュアなストレージ機能で、認証情報を安全に保存できます。</p>\n<p>これは、設定ファイル外で機密文字列を暗号化し、キーストアに格納することができます。また、JBoss EAP 管理 CLI 内での認証情報の管理が容易になります。</p>\n</blockquote>\n\n<br />\n\n# openjdk11 インストール\n\nOpenJDK 11 をインストールします。  \n11 なのは、公式ドキュメントに `\"Supported and recommended.\"` と書いてあるからです。  \nこの後の手順に出てくる `/opt/wildfly/bin/jboss-cli.sh` の実行に必要です。\n\n```shellsession\n# apt update;apt install -y openjdk-11-jdk\n# which java\n/usr/bin/java\n# java -version\nopenjdk version \"11.0.21\" 2023-10-17\nOpenJDK Runtime Environment (build 11.0.21+9-post-Ubuntu-0ubuntu122.04)\nOpenJDK 64-Bit Server VM (build 11.0.21+9-post-Ubuntu-0ubuntu122.04, mixed mode, sharing)\n```\n\n<br />\n\n<blockquote class=\"info\">\n<p>【 jboss-cli.sh 】</p>\n<p>jboss-cli.sh は、JBoss EAP（Enterprise Application Platform）のコマンドライン管理ツールです。このツールを使用すると、サーバーの起動や停止、アプリケーションのデプロイ（展開）やアンデプロイ（取り外し）、システムの設定など、さまざまな管理タスクを行うことができます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 余談：なぜ wildfly-cli.sh ではないのか 】</p>\n<p>WildFly は、元々 JBoss Application Server という名前で開発されていました。そのため、多くのツールや設定ファイルは、まだ「JBoss」の名前を使用しています。jboss-cli.sh もその一つで、WildFly の管理を行うためのコマンドラインツールです。したがって、wildfly-cli.sh ではなく jboss-cli.sh という名前が使われています。</p>\n</blockquote>\n\n<br />\n\n# 認証情報ストア作成\n\nWildFly の Elytron サブシステムで認証情報ストアを作成します。\n\n```shellsession\n# mkdir /opt/wildfly/standalone/configuration/keystore\n# chown wildfly:wildfly /opt/wildfly/standalone/configuration/keystore\n```\n\n<br />\n\nここで、なぜか wildfly が止まっていたため、再起動が必要でした。\n\n```shellsession\n# systemctl status wildfly\n○ wildfly.service - The WildFly Application Server\n     Loaded: loaded (/etc/systemd/system/wildfly.service; disabled; vendor preset: enabled)\n     Active: inactive (dead)\n\n# systemctl start wildfly\n```\n\n<br />\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/credential-store=defaultCS:add(path=keystore/credentials, relative-to=jboss.server.config.dir, credential-reference={clear-text=\"{EXT}/usr/bin/wildfly_pass\", type=\"COMMAND\"}, create=true)'\n{\"outcome\" => \"success\"}\n```\n\n**１． `jboss-cli.sh --connect`**\n\n<div style=\"padding-left: 1em;\">WildFly の管理 CLI に接続します。</div>\n\n**２． `/subsystem=elytron/credential-store=defaultCS:add(...)`**\n\n<div style=\"padding-left: 1em;\">Elytron サブシステム内で <code>defaultCS</code> という名前の認証情報ストアを作成します。</div>\n\n**３． `path=keystore/credentials, relative-to=jboss.server.config.dir`**\n\n<div style=\"padding-left: 1em;\">認証情報ストアの物理的な場所を指定します。これは、WildFly の設定ディレクトリ内の <code>keystore/credentials</code> というパスになります。</div>\n\n**４． `credential-reference={clear-text=\"{EXT}/usr/bin/wildfly_pass\", type=\"COMMAND\"}`**\n\n<div style=\"padding-left: 1em;\">認証情報ストアのマスターパスワードを指定します。このパスワードは、<code>/usr/bin/wildfly_pass</code> というコマンドの出力から取得されます。</div>\n\n**５． `create=true`**\n\n<div style=\"padding-left: 1em;\">認証情報ストアがまだ存在しない場合に作成します。\n\n<br />\n\n# データベースドライバーの追加\n\nデータベースに接続するための JDBC ドライバを WildFly サーバーに配置します。\n\n<blockquote class=\"warn\">\n<p>mariadb-java-client.jar 以外は、念のため、入れておきます。</p>\n</blockquote>\n\n/opt/wildfly/standalone/deployments/mariadb-java-client.jar  \n/opt/wildfly/standalone/deployments/postgresql-jdbc4.jar  \n/opt/wildfly/standalone/deployments/mssql-jdbc.jre11.jar  \nをインストールします。\n\n```shellsession\n# wget https://dlm.mariadb.com/1157496/Connectors/java/connector-java-2.7.0/mariadb-java-client-2.7.0.jar -O /opt/wildfly/standalone/deployments/mariadb-java-client.jar\n# wget https://jdbc.postgresql.org/download/postgresql-42.2.18.jar -O /opt/wildfly/standalone/deployments/postgresql-jdbc4.jar\n# wget https://github.com/microsoft/mssql-jdbc/releases/download/v12.2.0/mssql-jdbc-12.2.0.jre11.jar -O /opt/wildfly/standalone/deployments/mssql-jdbc.jre11.jar\n```\n\n<blockquote class=\"info\">\n<p>【 JDBC 】</p>\n<p>JDBC（Java Database Connectivity）は、Java プログラムからデータベースへのアクセスを行うための API（Application Programming Interface）です。これにより、Java プログラムはデータベースへの接続、データの読み書きなどの操作を行うことができます。JDBC はデータベースの種類に関わらず同じ手順で接続し、データを操作することが可能です。</p>\n</blockquote>\n\n<br />\n\n# データソースの追加\n\nデータソースを追加します。\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/credential-store=defaultCS:add-alias(alias=dbPassword, secret-value=\"ejbca\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect 'data-source add --name=ejbcads --connection-url=\"jdbc:mysql://127.0.0.1:3306/ejbca\" --jndi-name=\"java:/EjbcaDS\" --use-ccm=true --driver-name=\"mariadb-java-client.jar\" --driver-class=\"org.mariadb.jdbc.Driver\" --user-name=\"ejbca\" --credential-reference={store=defaultCS, alias=dbPassword} --validate-on-match=true --background-validation=false --prepared-statements-cache-size=50 --share-prepared-statements=true --min-pool-size=5 --max-pool-size=150 --pool-prefill=true --transaction-isolation=TRANSACTION_READ_COMMITTED --check-valid-connection-sql=\"select 1;\"'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':reload'\n```\n\n**１． `/opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/credential-store=defaultCS:add-alias(alias=dbPassword, secret-value=\"ejbca\")'`**\n\n<div style=\"padding-left: 1em;\">Elytronサブシステムの <code>defaultCS</code> という名前の認証情報ストアに、<code>dbPassword</code> というエイリアスを追加します。このエイリアスの秘密値は <code>ejbca</code> に設定されます。なお、エイリアスとは、パスワードが入った入れ物の名前のようなものです。</div>\n\n**２． `/opt/wildfly/bin/jboss-cli.sh --connect 'data-source add （...略...）`**\n\n<div style=\"padding-left: 1em;\">新しいデータソース <code>ejbcads</code> を作成します。このデータソースは、MariaDB の ejbca データベースに接続するためのもので、JNDI 名は <code>java:/EjbcaDS</code> に設定されます。また、CCM（Cached Connection Manager）を使用し、ドライバ名は <code>mariadb-java-client.jar</code>、ドライバクラスは <code>org.mariadb.jdbc.Driver</code> に設定されます。ユーザ名はejbcaで、パスワードは先ほど作成した認証情報ストアのエイリアス <code>dbPassword</code> を参照します。その他、様々なデータソースの設定が行われています。</div>\n\n**３． `/opt/wildfly/bin/jboss-cli.sh --connect ':reload'`**\n\n<div style=\"padding-left: 1em;\">WildFly サーバーをリロードします。これにより、上記の設定変更が反映されます。</div>\n\n<br />\n\n# WildFly リモート処理の構成\n\nWildFly リモート処理の構成を行います。  \n全体としてこの操作は、新たな接続ポイントを作成し、その接続ポイントが特定の IP アドレスとポートで HTTP リクエストを待ち受けるように設定する、ということを行っています。\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=remoting/http-connector=http-remoting-connector:write-attribute(name=connector-ref,value=remoting)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=remoting:add(port=4447,interface=management)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/http-listener=remoting:add(socket-binding=remoting,enable-http2=true)'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':reload'\n```\n\n**１． `/opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=remoting/（...略...）`**\n\n<div style=\"padding-left: 1em;\"><code>http-remoting-connector</code> という名前のHTTPコネクタの <code>connector-ref</code> 属性を <code>remoting</code> に設定します。これにより、HTTP コネクタが remoting コネクタを参照するようになります。</div>\n\n**２． `/opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/（...略...）`**\n\n<div style=\"padding-left: 1em;\"><code>standard-sockets</code> という名前のソケットバインディンググループに、<code>remoting</code> という名前の新しいソケットバインディングを追加します。このソケットバインディングは、management インターフェース上の 4447 ポートを使用します。</div>\n\n**３． `/opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/（...略...）`**\n\n<div style=\"padding-left: 1em;\"><code>default-server</code> という名前のサーバーに、<code>remoting</code> という名前の新しい HTTP リスナーを追加します。この HTTP リスナーは、先ほど作成した remoting ソケットバインディングを使用し、HTTP/2 を有効にします。</div>\n\n<br />\n\n# ロギングの構成\n\n公式ドキュメントに載っているログ関係の操作です。（説明は省略します。）\n\n<br />\n\n`オプション 1 - 推奨されるログ記録`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.ejbca:add(level=INFO)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore:add(level=INFO)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=com.keyfactor:add(level=INFO)'\n```\n\n<br />\n\n`追加のロギング構成`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.jboss.as.config:write-attribute(name=level, value=WARN)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.jboss:add(level=WARN)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.wildfly:add(level=WARN)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.xnio:add(level=WARN)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.hibernate:add(level=WARN)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.apache.cxf:add(level=WARN)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.config.ConfigurationHolder:add(level=WARN)'\n```\n\n<br />\n\n`アクセスログの追加`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/host=default-host/setting=access-log:add(pattern=\"%h %t \\\"%r\\\" %s \\\"%{i,User-Agent}\\\"\", relative-to=jboss.server.log.dir, directory=access-logs)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=io.undertow.accesslog:add(level=INFO)'\n```\n\n<br />\n\n`コンソールハンドラーを削除する`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/root-logger=ROOT:remove-handler(name=CONSOLE)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/console-handler=CONSOLE:remove()'\n```\n\n<br />\n\n`古いログファイルを削除する`\n\n```shellsession\n# vi /etc/cron.daily/remove-old-wildfly-logs.sh\n----- ここから\n#!/bin/sh\n# Remove log files older than 7 days\nfind /opt/wildfly/standalone/log/ -type f -mtime +7 -name 'server.log*' -execdir rm -- '{}' \\;\n----- ここまで\n# chmod +x /etc/cron.daily/remove-old-wildfly-logs.sh\n```\n\n<br />\n\n`Syslog 送信を有効にする`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/json-formatter=logstash:add(exception-output-type=formatted, key-overrides=[timestamp=\"@timestamp\"],meta-data=[@version=1])'\n# /opt/wildfly/bin/jboss-cli.sh --connect \"/subsystem=logging/syslog-handler=syslog-shipping:add(app-name=EJBCA,enabled=true,facility=local-use-0,hostname=$(hostname -f),level=INFO,named-formatter=logstash,port=514,server-address=syslog.server,syslog-format=RFC5424)\"\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/root-logger=ROOT:add-handler(name=syslog-shipping)'\n```\n\n<br />\n\n`ファイルへの監査ログを有効にする`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/size-rotating-file-handler=cesecore-audit-log:add(file={path=cesecore-audit.log, relative-to=jboss.server.log.dir}, max-backup-index=1, rotate-size=128m)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.audit.impl.log4j.Log4jDevice:add'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.audit.impl.log4j.Log4jDevice:add-handler(name=cesecore-audit-log)'\n```\n\n<br />\n\n`OCSP ログの構成`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.certificates.ocsp.logging.TransactionLogger:add(use-parent-handlers=false)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.certificates.ocsp.logging.TransactionLogger:write-attribute(name=level, value=INFO)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-tx-async:add(queue-length=\"100\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-tx-async:write-attribute(name=level, value=DEBUG)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-tx-async:write-attribute(name=\"overflow-action\", value=\"BLOCK\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.certificates.ocsp.logging.TransactionLogger:add-handler(name=ocsp-tx-async)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/periodic-rotating-file-handler=ocsp-tx:add(autoflush=true, append=true, suffix=\".yyyy-MM-dd\", file={path=ocsp-tx.log,relative-to=jboss.server.log.dir})'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-tx-async:add-handler(name=ocsp-tx)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.certificates.ocsp.logging.AuditLogger:add(use-parent-handlers=false)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.certificates.ocsp.logging.AuditLogger:write-attribute(name=level, value=INFO)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-audit-async:add(queue-length=\"100\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-audit-async:write-attribute(name=level, value=DEBUG)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-audit-async:write-attribute(name=\"overflow-action\", value=\"BLOCK\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/logger=org.cesecore.certificates.ocsp.logging.AuditLogger:add-handler(name=ocsp-audit-async)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/periodic-rotating-file-handler=ocsp-audit:add(autoflush=true, append=true, suffix=\".yyyy-MM-dd\", file={path=ocsp-audit.log,relative-to=jboss.server.log.dir})'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=logging/async-handler=ocsp-audit-async:add-handler(name=ocsp-audit)'\n# vi /etc/cron.daily/archive-rotated-ocsp-logs.sh\n----- ここから\n#!/bin/sh\n# Compress the OCSP audit log and the OCSP transaction log after rotation\nxz /opt/wildfly/standalone/log/ocsp-tx.log.*\nxz /opt/wildfly/standalone/log/ocsp-audit.log.*\n----- ここまで\n# chmod +x /etc/cron.daily/archive-rotated-ocsp-logs.sh\n```\n\n<blockquote class=\"info\">\n<p>【 OCSP 】</p>\n<p>OCSP（Online Certificate Status Protocol）は、デジタル証明書（公開鍵証明書）の有効性を TCP/IP ネットワークを通じて問い合わせるためのプロトコルです。これにより、デジタル証明書が何らかの理由で有効期限前に失効している場合でも、その情報を迅速に知ることができます。</p>\n</blockquote>\n\n<br />\n\n# HTTP(S) 構成\n\nHTTP/HTTPS の構成を行います。  \nこの記事では、管理コンソールに、`https://ejbcatest:8443/ejbca/adminweb/` で接続します。\n\n<br />\n\n既存の TLS および HTTP 構成を削除します。  \n以下の操作により、不要なリスナーやソケットバインディングが削除されます。\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/http-listener=default:remove()'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=http:remove()'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/https-listener=https:remove()'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=https:remove()'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':reload'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':read-attribute(name=server-state)'\n```\n\n**１． `/opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/http-listener=default:remove()'`**\n\n<div style=\"padding-left: 1em;\"><code>default-server</code> という名前のサーバーから、<code>default</code> という名前の HTTP リスナーを削除します。</div>\n\n**２． `/opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=http:remove()'`**\n\n<div style=\"padding-left: 1em;\"><code>standard-sockets</code> という名前のソケットバインディンググループから、<code>http</code> という名前のソケットバインディングを削除します。</div>\n\n**３． `/opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/https-listener=https:remove()'`**\n\n<div style=\"padding-left: 1em;\"><code>default-server</code> という名前のサーバーから、<code>https</code> という名前の HTTPS リスナーを削除します。</div>\n\n**４． `/opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=https:remove()'`**\n\n<div style=\"padding-left: 1em;\"><code>standard-sockets</code> という名前のソケットバインディンググループから、<code>https</code> という名前のソケットバインディングを削除します。</div>\n\n**５． `/opt/wildfly/bin/jboss-cli.sh --connect ':reload'`**\n\n<div style=\"padding-left: 1em;\">WildFly サーバーをリロードします。これにより、上記の設定変更が反映されます。</div>\n\n**６． `/opt/wildfly/bin/jboss-cli.sh --connect ':read-attribute(name=server-state)'`**\n\n<div style=\"padding-left: 1em;\">WildFly サーバーの状態を読み取ります。これにより、サーバーが正常に起動しているかどうかを確認できます。</div>\n\n<br />\n\n3 ポート分離を使用します。  \nこれは、公式ドキュメントで以下のように説明されている部分です。内部的な役割によって、ポートを分ける設定のようです。8443 は Web 管理コンソールです。  \n`\"3 ポート分離で Undertow をセットアップする方法について説明します。ポート 8080 は HTTP (非暗号化トラフィック) に使用され、ポート 8442 はサーバー認証のみの HTTPS (暗号化) トラフィックに、ポート 8443 はサーバー認証とクライアント認証の両方を含む HTTPS (暗号化) トラフィックに使用されます。\"`  \n2 ポート分離の手順もありますが、そちらではなく、3 ポート分離で進めます。\n\n<br />\n\n新しいインターフェースとソケットバインディングが作成されます。これにより、サーバーは新たな IP アドレスとポートで接続を受け付けることができます。\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/interface=http:add(inet-address=\"0.0.0.0\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/interface=httpspub:add(inet-address=\"0.0.0.0\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/interface=httpspriv:add(inet-address=\"0.0.0.0\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=http:add(port=\"8080\",interface=\"http\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=httpspub:add(port=\"8442\",interface=\"httpspub\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=httpspriv:add(port=\"8443\",interface=\"httpspriv\")'\n```\n\n**１． `/opt/wildfly/bin/jboss-cli.sh --connect '/interface=http:add(inet-address=\"0.0.0.0\")'`**\n\n<div style=\"padding-left: 1em;\"><code>http</code> という名前の新しいインターフェースを作成します。このインターフェースは、すべてのIPアドレス（<code>0.0.0.0</code>）で接続を受け付けます。</div>\n\n**２． `/opt/wildfly/bin/jboss-cli.sh --connect '/interface=httpspub:add(inet-address=\"0.0.0.0\")'`**\n\n<div style=\"padding-left: 1em;\"><code>httpspub</code> という名前の新しいインターフェースを作成します。このインターフェースも、すべてのIPアドレスで接続を受け付けます。</div>\n\n**３． `/opt/wildfly/bin/jboss-cli.sh --connect '/interface=httpspriv:add(inet-address=\"0.0.0.0\")'`**\n\n<div style=\"padding-left: 1em;\"><code>httpspriv</code> という名前の新しいインターフェースを作成します。このインターフェースも、すべてのIPアドレスで接続を受け付けます。</div>\n\n**４． `/opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=http:add(port=\"8080\",interface=\"http\")'`**\n\n<div style=\"padding-left: 1em;\"><code>standard-sockets</code> という名前のソケットバインディンググループに、<code>http</code> という名前の新しいソケットバインディングを追加します。このソケットバインディングは、http インターフェース上の 8080 ポートを使用します。</div>\n\n**５． `/opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=httpspub:add(port=\"8442\",interface=\"httpspub\")'`**\n\n<div style=\"padding-left: 1em;\"><code>standard-sockets</code> ソケットバインディンググループに、<code>httpspub</code> という名前の新しいソケットバインディングを追加します。このソケットバインディングは、httpspub インターフェース上の 8442 ポートを使用します。</div>\n\n**６． `/opt/wildfly/bin/jboss-cli.sh --connect '/socket-binding-group=standard-sockets/socket-binding=httpspriv:add(port=\"8443\",interface=\"httpspriv\")'`**\n\n<div style=\"padding-left: 1em;\"><code>standard-sockets</code> ソケットバインディンググループに、<code>httpspriv</code> という名前の新しいソケットバインディングを追加します。このソケットバインディングは、httpspriv インターフェース上の 8443 ポートを使用します。</div>\n\n<blockquote class=\"info\">\n<p>【 Undertow 】</p>\n<p>Undertow は、Java で書かれた軽量で高性能な Web サーバーです。非ブロッキングのアーキテクチャを採用しており、大量の同時接続を効率的に処理することができます。また、Servlet 3.1、WebSockets、HTTP/2 などの最新の Web 技術をサポートしています。WildFly などのアプリケーションサーバーの内部で使用されることが多いです。</p>\n</blockquote>\n\n<br />\n\n# TLS の構成\n\nTLS の構成を設定します。\n\n<span style=\"color: red;\"><strong>これにより、Web 管理画面（`/ejbca/adminweb/`）へのアクセスは、クライアント証明書が必要になります。</strong></span>\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/credential-store=defaultCS:add-alias(alias=httpsKeystorePassword, secret-value=\"serverpwd\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/credential-store=defaultCS:add-alias(alias=httpsTruststorePassword, secret-value=\"changeit\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/key-store=httpsKS:add(path=\"keystore/keystore.p12\",relative-to=jboss.server.config.dir,credential-reference={store=defaultCS, alias=httpsKeystorePassword},type=PKCS12)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/key-store=httpsTS:add(path=\"keystore/truststore.p12\",relative-to=jboss.server.config.dir,credential-reference={store=defaultCS, alias=httpsTruststorePassword},type=PKCS12)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/key-manager=httpsKM:add(key-store=httpsKS,algorithm=\"SunX509\",credential-reference={store=defaultCS, alias=httpsKeystorePassword})'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/trust-manager=httpsTM:add(key-store=httpsTS)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/server-ssl-context=httpspub:add(key-manager=httpsKM,protocols=[\"TLSv1.3\",\"TLSv1.2\"],use-cipher-suites-order=false,cipher-suite-filter=\"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",cipher-suite-names=\"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/server-ssl-context=httpspriv:add(key-manager=httpsKM,protocols=[\"TLSv1.3\",\"TLSv1.2\"],use-cipher-suites-order=false,cipher-suite-filter=\"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",cipher-suite-names=\"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256\",trust-manager=httpsTM,need-client-auth=true)'\n```\n\n**１． `/opt/wildfly/bin/jboss-cli.sh（...略...）defaultCS:add-alias(alias=httpsKeystorePassword, secret-value=\"serverpwd\")'`**\n\n<div style=\"padding-left: 1em;\"><code>defaultCS</code> という名前のクレデンシャルストアに <code>httpsKeystorePassword</code> というエイリアスを追加しています。このエイリアスは <code>serverpwd</code> という秘密の値を保持します。\n\n**２． `/opt/wildfly/bin/jboss-cli.sh（...略...）defaultCS:add-alias(alias=httpsTruststorePassword, secret-value=\"changeit\")'`**\n\n<div style=\"padding-left: 1em;\"><code>defaultCS</code> という名前のクレデンシャルストアに <code>httpsTruststorePassword</code> というエイリアスを追加しています。このエイリアスは <code>changeit</code> という秘密の値を保持します。</div>\n\n**３． `/opt/wildfly/bin/jboss-cli.sh（...略...）httpsKS:add(path=\"keystore/keystore.p12（...略...）store=defaultCS, alias=httpsKeystorePassword},type=PKCS12)'`**\n\n<div style=\"padding-left: 1em;\"><code>httpsKS</code> という名前のキーストアを作成し、そのパスを <code>keystore/keystore.p12</code> に設定しています。このキーストアは <code>defaultCS</code> クレデンシャルストアの <code>httpsKeystorePassword</code> エイリアスを使用します。</div>\n\n**４． `/opt/wildfly/bin/jboss-cli.sh（...略...）httpsTS:add(path=\"keystore/truststore.p12\"（...略...）store=defaultCS, alias=httpsTruststorePassword},type=PKCS12)'`**\n\n<div style=\"padding-left: 1em;\"><code>httpsTS</code> という名前のキーストアを作成し、そのパスを <code>keystore/truststore.p12</code> に設定しています。このキーストアは <code>defaultCS</code> クレデンシャルストアの <code>httpsTruststorePassword</code> エイリアスを使用します。</div>\n\n**５． `/opt/wildfly/bin/jboss-cli.sh（...略...）httpsKM:add(key-store=httpsKS,algorithm=\"SunX509\"（...略...）store=defaultCS, alias=httpsKeystorePassword})'`**\n\n<div style=\"padding-left: 1em;\"><code>httpsKM</code> という名前のキーマネージャーを作成し、<code>httpsKS</code> キーストアと <code>SunX509</code> アルゴリズムを使用します。このキーマネージャーは <code>defaultCS</code> クレデンシャルストアの <code>httpsKeystorePassword</code> エイリアスを使用します。</div>\n\n**６． `/opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=elytron/trust-manager=httpsTM:add(key-store=httpsTS)'`**\n\n<div style=\"padding-left: 1em;\"><code>httpsTM</code> という名前のトラストマネージャーを作成し、<code>httpsTS</code> キーストアを使用します。</div>\n\n**７． `/opt/wildfly/bin/jboss-cli.sh（...略...）httpspub:add(key-manager=httpsKM,protocols=[\"TLSv1.3\",\"TLSv1.2\"]（...略...）:TLS_CHACHA20_POLY1305_SHA256\")'`**\n\n<div style=\"padding-left: 1em;\"><code>httpspub</code> という名前のサーバーSSLコンテキストを作成します。このコンテキストは<code>httpsKM</code> キーマネージャーを使用し、特定のプロトコルと暗号スイートを指定します。</div>\n\n**８． `/opt/wildfly/bin/jboss-cli.sh（...略...）httpspriv:add(key-manager=httpsKM,protocols=[\"TLSv1.3\",\"TLSv1.2\"]（...略...）TLS_CHACHA20_POLY1305_SHA256\",trust-manager=httpsTM,need-client-auth=true)'`**\n\n<div style=\"padding-left: 1em;\"><code>httpspriv</code> という名前のサーバーSSLコンテキストを作成します。このコンテキストは<code>httpsKM</code> キーマネージャーと<code>httpsTM</code> トラストマネージャーを使用し、特定のプロトコルと暗号スイートを指定します。さらに、クライアント認証が必要とされています。</div>\n\n<blockquote class=\"info\">\n<p>【 クライアント認証 】</p>\n<p>クライアント認証とは、コンピューターシステムやネットワーク、サービスなどにアクセスする際、アクセス元となるユーザーまたはクライアントがインストール済みのクライアント証明書をアクセス先に提示して、アクセス先が正当なユーザーであることを認証するための仕組みです。</p>\n<p>具体的には、クライアント証明書という電子証明書を使用します。この証明書は、認証局によって個人や組織、あるいは端末を認証し発行されます。この証明書がインストールされていない端末からのアクセスは防止できます。</p>\n</blockquote>\n\n<br />\n\n# HTTP(S) リスナーの追加\n\nHTTP(S) リスナーの追加を行います。\nこれらのコマンドにより、HTTP と HTTPS の通信を適切にハンドリングするための設定が行われます。\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/http-listener=http:add(socket-binding=\"http\", redirect-socket=\"httpspriv\")'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/https-listener=httpspub:add(socket-binding=\"httpspub\", ssl-context=\"httpspub\", max-parameters=2048)'\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=undertow/server=default-server/https-listener=httpspriv:add(socket-binding=\"httpspriv\", ssl-context=\"httpspriv\", max-parameters=2048)'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':reload'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':read-attribute(name=server-state)'\n```\n\n**１． `/opt/wildfly/bin/jboss-cli.sh（...略...）http-listener=http:add(socket-binding=\"http\", redirect-socket=\"httpspriv\")'`**\n\n<div style=\"padding-left: 1em;\">HTTP リスナーを追加しています。<code>socket-binding=\"http\"</code> は、HTTP 通信を待ち受けるソケットを指定しています。また、<code>redirect-socket=\"httpspriv\"</code> は、HTTPS 通信へのリダイレクトを行うソケットを指定しています。</div>\n\n**２． `/opt/wildfly/bin/jboss-cli.sh（...略...）https-listener=httpspub:add(socket-binding=\"httpspub\", ssl-context=\"httpspub\", max-parameters=2048)'`**\n\n<div style=\"padding-left: 1em;\">HTTPS リスナーを追加しています。<code>socket-binding=\"httpspub\"</code> は、HTTPS 通信を待ち受けるソケットを指定しています。<code>ssl-context=\"httpspub\"</code> は、SSL コンテキストを指定しています。これは、SSL 通信の設定（鍵や証明書など）を含んでいます。<code>max-parameters=2048</code> は、リクエストパラメータの最大数を指定しています。</div>\n\n**３． `/opt/wildfly/bin/jboss-cli.sh（...略...）https-listener=httpspriv:add(socket-binding=\"httpspriv\", ssl-context=\"httpspriv\", max-parameters=2048)'`**\n\n<div style=\"padding-left: 1em;\">このコマンドも HTTPS リスナーを追加していますが、異なるソケットと SSL コンテキストを使用しています。</div>\n\n<br />\n\n<span style=\"color: red;\"><strong>この後、公式ドキュメントでは、HSM の設定手順の記述がありますが、スキップします。</strong></span>\n\n<blockquote class=\"info\">\n<p>【 HSM 】</p>\n<p>HSM（ハードウェアセキュリティモジュール）は、一般的に電子証明書の暗号鍵と鍵管理に関する国際規格を取得しているデバイスです。</p>\n<p>具体的な規格としては、以下のものがあります：</p>\n<p>FIPS 140-2：米国の連邦情報処理標準です。暗号モジュールのセキュリティ要件を定めています。</p>\n<p>Common Criteria（コモンクライテリア）：IT製品のセキュリティ機能を評価するための国際標準です。</p>\n<p>これらの規格は、HSMが暗号鍵の生成、保護、管理などのプロセスを安全に行うことを保証します。</p>\n<p>また、これらの規格に準拠しているHSMは、テスト、検証、および認定を受けています。これにより、HSMは高度なセキュリティ要件を満たすことができます。</p>\n</blockquote>\n\n<br />\n\n# HTTP プロトコルの動作構成\n\nHTTP プロトコルに関するもろもろの設定を行います。\n\n<br />\n\n`URI エンコーディングを UTF-8 に設定`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/system-property=org.apache.catalina.connector.URI_ENCODING:add(value=\"UTF-8\")'\n```\n\n<br />\n\n`クエリストリングのエンコーディングにボディエンコーディングを使用する`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/system-property=org.apache.catalina.connector.USE_BODY_ENCODING_FOR_QUERY_STRING:add(value=true)'\n```\n\n<br />\n\n`エンコードされたスラッシュ（%2Fまたは%5C）を許可`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/system-property=org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH:add(value=true)'\n```\n\n<br />\n\n`リクエストパラメータの最大数を 2048 に設定`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/system-property=org.apache.tomcat.util.http.Parameters.MAX_COUNT:add(value=2048)'\n```\n\n<br />\n\n`バックスラッシュ（\\）を許可`\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/system-property=org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH:add(value=true)'\n```\n\n<br />\n\n`Web サービスサブシステムの wsdl-host 属性を’jbossws.undefined.host’に設定`  \nクライアントが Web サービスを呼び出す際に使用する URL を動的に変更するためのものです。これにより、同じ Web サービスが異なるホストや環境で動作している場合でも、正しいエンドポイントがクライアントに通知されます。\n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=webservices:write-attribute(name=wsdl-host, value=jbossws.undefined.host)'\n```\n\n<br />\n\n`Web サービスサブシステムの modify-wsdl-address 属性を’true’に設定`  \n\n```shellsession\n# /opt/wildfly/bin/jboss-cli.sh --connect '/subsystem=webservices:write-attribute(name=modify-wsdl-address, value=true)'\n# /opt/wildfly/bin/jboss-cli.sh --connect ':reload'\n```\n\n<br />\n\n公式ドキュメントの手順  \nGalleon Specific Configuration（Galleon 特有の構成）  \nOptional Configuration（オプションの構成）  \nは、スキップします。\n\n<blockquote class=\"info\">\n<p>【 Galleon 】</p>\n<p>Galleonは、Java のアプリケーションサーバーである WildFly の機能の一つで、特定のバージョンや設定の WildFly サーバーを簡単に作成、管理するためのツールです。Galleon を使用すると、必要な機能だけを含むカスタムな WildFly サーバーを作成でき、不要なリソースを消費することなく、効率的にアプリケーションを実行できます。また、Galleonはアップデートやパッチの適用もサポートしています。これにより、WildFly サーバーのライフサイクル管理が容易になります。</p>\n<p>※今回は使っていません。</p>\n</blockquote>\n\n<br />\n\n# Deploying EJBCA\n\nさて、いよいよ、と言うかやっとですが...EJBCA ソースコードを取得して、ビルドに取り掛かります。\n\n<br />\n\nejbca-ce を `git clone` して、/opt/ejbca/ に配置します。  \nここで、`ejbca` というユーザーを作成して、シェルを `bash` に変更しています。\n\n```shellsession\n# systemctl enable wildfly\n# apt install git -y\n# useradd -m -U -r -d /opt/ejbca ejbca\n# passwd ejbca\nejbca\n# usermod -aG sudo ejbca\n# su - ejbca -c \"chsh -s $(which bash)\"\n# git clone https://github.com/Keyfactor/ejbca-ce.git\n# mv ejbca-ce/* /opt/ejbca/\n# rm -rf ejbca-ce\n```\n\n<br />\n\n設定を配置します。  \nここで、<span style=\"color: red;\"><strong>設定は、全てデフォルト（サンプル設定のまま）とします。</strong></span>\n\n```shellsession\n# cd /opt/ejbca\n# cp conf/ejbca.properties.sample            conf/ejbca.properties\n# cp conf/cache.properties.sample            conf/cache.properties\n# cp conf/logdevices/log4j.properties.sample conf/logdevices/log4j.properties\n# cp conf/cesecore.properties.sample         conf/cesecore.properties\n# cp conf/ocsp.properties.sample             conf/ocsp.properties\n# cp conf/systemtests.properties.sample      conf/systemtests.properties\n# cp conf/custom.properties.sample           conf/custom.properties\n# cp conf/mail.properties.sample             conf/mail.properties\n# cp conf/catoken.properties.sample          conf/catoken.properties\n# cp conf/batchtool.properties.sample        conf/batchtool.properties\n# cp conf/jaxws.properties.sample            conf/jaxws.properties\n# cp conf/web.properties.sample              conf/web.properties\n# cp conf/install.properties.sample          conf/install.properties\n# cp conf/va.properties.sample               conf/va.properties\n# cp conf/database.properties.sample         conf/database.properties\n# cp conf/va-publisher.properties.sample     conf/va-publisher.properties\n```\n\n<br />\n\n環境変数をセットします。\n\n```shellsession\n# apt install ant -y\n# vi /etc/environment\nAPPSRV_HOME=/opt/wildfly\nEJBCA_HOME=/opt/ejbca\nJBOSS_HOME=/opt/wildfly\n# export APPSRV_HOME=/opt/wildfly\n# export EJBCA_HOME=/opt/ejbca\n# export JBOSS_HOME=/opt/wildfly\n```\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>環境変数を適切にセットしていないと、この後の、</strong></span></p>\n<p><span style=\"color: red;\"><strong><code>ant -q clean deployear</code> にて、以下のエラーになり、ビルドに失敗します。</strong></span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">BUILD FAILED</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">/opt/ejbca/build.xml:829: The following error occurred while executing this line:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">/opt/ejbca/propertyDefaults.xml:226: 'appserver.type' could not be detected or is not configured. Glassfish 3.1, Glassfish 4.0, JBoss 7.1.1, JBoss EAP 6.1, 6.2, 6.3, 6.4, WildFly 8, 9, 10 can be detected. (Is 'appserver.home' configured?)</span></p>\n</blockquote>\n\n<br />\n\n設定は、全てデフォルト（サンプル設定のまま）としましたが、<span style=\"color: red;\"><strong>ホスト名と DB 関連設定だけ変更</strong></span>します。\n\n```shellsession\n# vi conf/web.properties\n```\n\n```properties:/opt/ejbca/conf/web.properties\nhttpsserver.hostname=ejbcatest\n```\n\n```shellsession\n# vi conf/database.properties\n```\n\n```properties:/opt/ejbca/conf/database.properties\ndatabase.name=mysql\ndatabase.url=jdbc:mysql://127.0.0.1:3306/ejbca\ndatabase.driver=org.mariadb.jdbc.Driver\n```\n\n<blockquote class=\"info\">\n<p>それぞれ、20, 50, 66 行目をコメントアウトする形になります。</p>\n</blockquote>\n\n<br />\n\nビルドし、デプロイします。\n\n<blockquote class=\"info\">\n<p>【 <code>ant -q clean deployear</code> 】</p>\n<p>以下の2つのタスクを実行します：</p>\n<p><code>clean</code>：プロジェクトのビルド時に生成されたファイルを削除します。これにより、次回のビルドがクリーンな状態から始まることを保証します。</p>\n<p><code>deployear</code>：EAR（Enterprise Archive）ファイルをビルドし、それをアプリケーションサーバーにデプロイ（配布）します。これにより、新しいまたは更新されたアプリケーションがサーバー上で利用可能になります。</p>\n<p><code>-q</code> オプションは、「quiet」（静かな）モードを指定します。このモードでは、Ant は必要最低限の情報しか出力しません。これにより、ビルドのログがすっきりとします。</p>\n</blockquote>\n\n<br />\n\n# TLS キーストアを WildFly にデプロイする\n\nTLS キーストアを WildFly にデプロイし、`/ejbca/adminweb/` へのクライアント認証を可能とします。  \n<span style=\"color: red;\"><strong>公式手順書には、`ant deploy-keystore` とだけ説明されているのですが、JKS 形式のキーストアを PKCS12 形式のキーストアに変換する必要があります。</strong></span>  \n<span style=\"color: red;\"><strong>これは、2024 年 2 月時点では公式手順書（`https://doc.primekey.com/ejbca/ejbca-installation`）に書かれていませんでした。</strong></span>  \n<span style=\"color: red;\"><strong>変換する手順は、`https://github.com/Keyfactor/ejbca-ce/discussions/46` に書かれていました。</strong></span>\n\n<blockquote class=\"info\">\n<p>【 JKS, PKCS12 】</p>\n<p>JKS (Java KeyStore) は、鍵と証明書を保存するための Java の専用フォーマットです。これは Java アプリケーションでよく使われます。</p>\n<p>PKCS12 (Public Key Cryptography Standards #12) は、鍵と証明書を保存するための一般的なフォーマットです。これは多くの異なるタイプのアプリケーションで使われます。</p>\n<p>これらのフォーマットは、鍵と証明書を安全に保存するために使われます。これらは、通信を暗号化したり、デジタル署名を検証したりするために必要です。それぞれが異なる目的や要件に合わせて設計されています。</p>\n<p>例えば、JKS は Java アプリケーション用に設計されていますが、PKCS12 はより広範な用途に対応しています。また、PKCS12 は JKS よりも新しく、一般的にはより安全とされています。そのため、JKS から PKCS12 への変換が推奨されることがあります。</p>\n</blockquote>\n\n<br />\n\nデプロイし、JKS 形式のキーストアを PKCS12 形式のキーストアに変換します。  \nなお、このとき、`p12/superadmin.p12` が生成されて、クライアント証明書として、ブラウザ（今回は、Ubuntu22 の Firefox）に設定する必要があるため、ユーザーの /home/ 配下にコピーしています。\n\n```shellsession\n# cp -p $EJBCA_HOME/p12/tomcat.jks $JBOSS_HOME/standalone/configuration/keystore/keystore.jks\n# ant deploy-keystore\n# cd /opt/wildfly/standalone/configuration/keystore\n# keytool -importkeystore -srckeystore keystore.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore keystore.p12\n→serverpwd×3回入力\n# keytool -importkeystore -srckeystore truststore.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore truststore.p12\n→changeit×3回入力\n# cd /opt/ejbca\n# cp p12/superadmin.p12 /home/admin/\n# chown admin /home/admin/superadmin.p12\n```\n\n**１． `keytool -importkeystore -srckeystore keystore.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore keystore.p12`**\n\n<div style=\"padding-left: 1em;\">JKS 形式のキーストア(keystore.jks) を PKCS12 形式のキーストア(keystore.p12) に変換します。</div>\n\n**２． `keytool -importkeystore -srckeystore truststore.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore truststore.p12`**\n\n<div style=\"padding-left: 1em;\">JKS 形式のトラストストア(truststore.jks)を  PKCS12 形式のトラストストア(truststore.p12) に変換します。</div>\n\n<br />\n\n# クライアント証明書インポート\n\nUbuntu22 の Firefox にクライアント証明書をインポートします。  \nなお、これを行わないと、Web 管理画面（`/ejbca/adminweb/`）にアクセスしたときに、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">安全な接続ができませんでした</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">ejbcatset:8443 への接続中にエラーが発生しました。SSL peer cannot verify your certificate.</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">エラーコード: SSL_ERROR_BAD_CERT_ALERT</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">受信したデータの真正性を検証できなかったため、このページは表示できませんでした。</span>  \n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image1.png\" alt=\"安全な接続ができませんでした\" width=\"882\" height=\"550\" loading=\"lazy\"></a>\n\n<br />\n\nFirefox を起動して、**設定** をクリックします。  \n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image2.png\" alt=\"Firefoxハンバーガーメニュー\" width=\"955\" height=\"177\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image3.png\" alt=\"Firefox設定\" width=\"953\" height=\"692\" loading=\"lazy\"></a>\n\n<br />\n\n**プライバシーとセキュリティ** → **証明書を表示** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image4.png\" alt=\"Firefox プライバシーとセキュリティ 証明書を表示\" width=\"952\" height=\"457\" loading=\"lazy\"></a>\n\n<br />\n\n**あなたの証明書** タブをクリックして、**インポート** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image5.png\" alt=\"あなたの証明書 インポート\" width=\"955\" height=\"602\" loading=\"lazy\"></a>\n\n<br />\n\nホーム（/home/admin）/superadmin.p12 を選択します。  \nこの時、パスワードを聞かれますが、`ejbca` を入力します。\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image6.png\" alt=\"superadmin.p12 を選択\" width=\"917\" height=\"385\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"info\">\n<p>Windows の場合、Edge や Chrome は、OS の機能で証明書が管理されます。一方、Firefox の場合、Windows であっても、ブラウザ独自に証明書を管理します。そのため、公式ドキュメントでも Firefox の利用が推奨されていました。</p>\n</blockquote>\n\n<br />\n\n# Web 管理画面\n\n```shellsession\n# systemctl restart wildfly\n```\n\nとして、  \nURL = `https://ejbcatest:8443/ejbca/adminweb`  \nまたは、  \nURL = `https://localhost:8443/ejbca/adminweb`  \nでアクセスします。\n\n<blockquote class=\"warn\">\n<p><code>ejbcatest</code> は今回の手順の場合です。</p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/ejbca-build-install/image7.png\" alt=\"EJBCA Web 管理画面\" width=\"879\" height=\"599\" loading=\"lazy\"></a>\n\n<br />\n\nヨシっ！\n","description":"EJBCA（EJBCA Community）というオープンソースの公開鍵基盤（PKI）および認証局（CA）ソフトウェアをビルドしてインストールしました。最初の画面を表示するまでの全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"pd0ebqu8t61","createdAt":"2024-02-04T12:17:33.611Z","updatedAt":"2024-02-04T12:17:46.460Z","publishedAt":"2024-02-04T12:17:33.611Z","revisedAt":"2024-02-04T12:17:46.460Z","url":"https://itc-engineering-blog.imgix.net/ejbca-build-install/ITC_Engineering_Blog.png","alt":"EJBCA(PKIおよび証明書管理アプリ)をビルドしてインストールしてみた","width":1200,"height":630}],"seo_authors":[]}],"totalCount":26,"offset":0,"limit":100}